Examples
C++: also in Python · Node · Go · Rust · Swift · Dart · Java and Kotlin · .NET · Objective-C
Every program here is executed against the published package on each release, so an example that stopped working would fail the release rather than mislead you.
Conflict resolution
Section titled “Conflict resolution”// Conflict is data, not an exception: detect it, resolve it, lose nothing.//// A losing sync returns a Conflict alternative rather than throwing.// Exceptions are for things that went wrong; losing a race is an ordinary// outcome with a defined next step.
#include <cstdlib>#include <cstdio>#include <cstdint>#include <filesystem>#include <string>#include <variant>
#include "potluckdb.hpp"
namespace {// Not `assert`: NDEBUG erases that, and an example that prints "ok"// without having checked anything is worse than no example.void expect(bool condition, const char *description) { if (!condition) { std::fprintf(stderr, "expected: %s\n", description); std::abort(); }}} // namespace
int main() { auto scratch = std::filesystem::temp_directory_path() / "potluckdb-conflict"; std::filesystem::remove_all(scratch); std::filesystem::create_directories(scratch);
{ auto remote = potluckdb::Remote::from_directory((scratch / "remote").string()); potluckdb::OpenOptions manual; manual.sync_policy = potluckdb::SyncPolicy::manual();
auto writer = potluckdb::open((scratch / "writer").string(), remote, manual); writer.execute("CREATE TABLE item(id INTEGER PRIMARY KEY, value TEXT)"); writer.sync();
auto rival = potluckdb::open((scratch / "rival").string(), remote, manual); // No id is named: the database assigns one. That matters more than it // looks. Resolving a conflict replays local statements on top of the // winner, and a statement whose key the winner already used cannot be // replayed -- it is quarantined instead, and its write does not land. // Two devices that both pick their own ids collide the moment they // pick the same one. Left to the database, they cannot. rival.execute("INSERT INTO item(value) VALUES('from rival')"); expect(std::holds_alternative<potluckdb::Pushed>(rival.sync()), "the rival publishes first");
// The writer races the same head with its own local work. writer.execute("INSERT INTO item(value) VALUES('from writer')"); expect(std::holds_alternative<potluckdb::Conflict>(writer.sync()), "the writer is told it lost");
auto resolution = writer.resolve_conflict(); expect(resolution.replayed.size() + resolution.already_published.size() > 0, "resolution reports what it did with the local work"); // Pushed, not "Pushed or UpToDate": resolving stages the replayed // statement, so there is always something left to publish. expect(std::holds_alternative<potluckdb::Pushed>(writer.sync()), "the writer publishes what it resolved");
// Both sides' rows survive: the winner was adopted and the safe // local statement replayed on top of it. auto rows = writer.query("SELECT id, value FROM item ORDER BY id"); expect(rows.size() == 2, "nothing was lost"); expect(std::get<std::string>(rows[0][1]) == "from rival", "the winner is there"); expect(std::get<std::string>(rows[1][1]) == "from writer", "and so is the loser");
writer.close(); rival.close(); remote.close(); }
std::filesystem::remove_all(scratch); std::puts("conflict_resolution: ok"); return 0;}Quickstart local
Section titled “Quickstart local”// A local database: create it, write to it, read it back.//// No remote is involved, so nothing is uploaded and nothing is fetched.// This is the whole API for a database that never leaves the machine.
#include <cstdlib>#include <cstdio>#include <filesystem>#include <string>
#include "potluckdb.hpp"
namespace {// Not `assert`: NDEBUG erases that, and an example that prints "ok"// without having checked anything is worse than no example.void expect(bool condition, const char *description) { if (!condition) { std::fprintf(stderr, "expected: %s\n", description); std::abort(); }}} // namespace
int main() { auto scratch = std::filesystem::temp_directory_path() / "potluckdb-quickstart"; std::filesystem::remove_all(scratch); std::filesystem::create_directories(scratch);
{ auto database = potluckdb::open((scratch / "app.potluckdb").string()); database.execute("CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)");
auto written = database.execute( "INSERT INTO note(body) VALUES(?)", potluckdb::Bindings::positional({std::string("first note")}) ); expect(written.rows_affected == 1, "one row was written"); expect(written.last_insert_id.has_value(), "the new row has an identifier");
auto rows = database.query("SELECT id, body FROM note"); expect(rows.columns() == std::vector<std::string>{"id", "body"}, "the result names its columns"); expect(rows.size() == 1, "the row is there"); expect(std::get<std::string>(rows[0]["body"]) == "first note", "and reads back by name");
database.close(); }
std::filesystem::remove_all(scratch); std::puts("quickstart_local: ok"); return 0;}Remote sync
Section titled “Remote sync”// Two devices share one remote: write on one, sync, read on the other.//// SyncResult is a std::variant, so the compiler knows the outcome set is// closed and std::holds_alternative names the case you meant.
#include <cstdlib>#include <cstdio>#include <filesystem>#include <string>#include <variant>
#include "potluckdb.hpp"
namespace {// Not `assert`: NDEBUG erases that, and an example that prints "ok"// without having checked anything is worse than no example.void expect(bool condition, const char *description) { if (!condition) { std::fprintf(stderr, "expected: %s\n", description); std::abort(); }}} // namespace
int main() { auto scratch = std::filesystem::temp_directory_path() / "potluckdb-remote-sync"; std::filesystem::remove_all(scratch); std::filesystem::create_directories(scratch);
{ auto remote = potluckdb::Remote::from_directory((scratch / "remote").string()); potluckdb::OpenOptions manual; manual.sync_policy = potluckdb::SyncPolicy::manual();
auto laptop = potluckdb::open((scratch / "laptop").string(), remote, manual); laptop.execute("CREATE TABLE task(id INTEGER PRIMARY KEY, title TEXT)"); laptop.execute("INSERT INTO task(title) VALUES(?)", potluckdb::Bindings::positional({std::string("ship the release")})); expect(std::holds_alternative<potluckdb::Pushed>(laptop.sync()), "the first sync publishes");
auto phone = potluckdb::open((scratch / "phone").string(), remote, manual); auto rows = phone.query("SELECT title FROM task"); expect(rows.size() == 1, "the other device sees the row"); expect(std::get<std::string>(rows[0][0]) == "ship the release", "with its value");
// An unchanged head is reported as exactly that. expect(std::holds_alternative<potluckdb::UpToDate>(phone.sync()), "an unchanged head is reported");
laptop.close(); phone.close(); remote.close(); }
std::filesystem::remove_all(scratch); std::puts("remote_sync: ok"); return 0;}Second device
Section titled “Second device”// Adding a second device, including one that opens before anything is published.
#include <unistd.h>
#include <cstdlib>#include <cstdio>#include <filesystem>#include <string>#include <variant>
#include "potluckdb.hpp"
namespace {void expect(bool condition, const char *description) { if (!condition) { std::fprintf(stderr, "expected: %s\n", description); std::abort(); }}} // namespace
int main() { auto scratch = std::filesystem::temp_directory_path() / ("potluckdb-second-device-" + std::to_string(::getpid())); std::filesystem::remove_all(scratch); std::filesystem::create_directories(scratch);
{ auto remote = potluckdb::Remote::from_directory((scratch / "remote").string()); potluckdb::OpenOptions manual; manual.sync_policy = potluckdb::SyncPolicy::manual();
auto laptop = potluckdb::open((scratch / "laptop").string(), remote, manual); laptop.execute("CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)"); laptop.execute("INSERT INTO note VALUES(1, 'from the laptop')"); expect(std::holds_alternative<potluckdb::Pushed>(laptop.sync()), "the first sync publishes");
auto phone = potluckdb::open((scratch / "phone").string(), remote, manual); auto rows = phone.query("SELECT body FROM note"); expect(rows.size() == 1, "the other device sees the row"); expect(std::get<std::string>(rows[0][0]) == "from the laptop", "with its value"); phone.close();
auto early_remote = potluckdb::Remote::from_directory( (scratch / "early-remote").string()); auto first = potluckdb::open((scratch / "first").string(), early_remote, manual); auto tablet = potluckdb::open((scratch / "tablet").string(), early_remote, manual); expect(tablet.status().pending_writes == 0, "opening records nothing to publish"); first.execute("CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)"); first.execute("INSERT INTO note VALUES(1, 'from the first device')"); expect(std::holds_alternative<potluckdb::Pushed>(first.sync()), "the first device publishes");
expect(std::holds_alternative<potluckdb::Pulled>(tablet.sync()), "a pristine device pulls"); auto visible = tablet.query("SELECT body FROM note"); expect(visible.size() == 1, "the tablet sees the published row"); expect(std::get<std::string>(visible[0][0]) == "from the first device", "with its value");
first.close(); tablet.close(); early_remote.close(); laptop.close(); remote.close(); }
std::filesystem::remove_all(scratch); std::puts("second_device: ok"); return 0;}Transactions
Section titled “Transactions”// Managed transactions: commit on success, rollback on failure.//// The callback owns the scope. Returning commits; throwing rolls back.// There is no way to leave a transaction open by forgetting to end it.
#include <cstdlib>#include <cstdio>#include <cstdint>#include <filesystem>#include <stdexcept>#include <string>
#include "potluckdb.hpp"
namespace {// Not `assert`: NDEBUG erases that, and an example that prints "ok"// without having checked anything is worse than no example.void expect(bool condition, const char *description) { if (!condition) { std::fprintf(stderr, "expected: %s\n", description); std::abort(); }}} // namespace
int main() { auto scratch = std::filesystem::temp_directory_path() / "potluckdb-transactions"; std::filesystem::remove_all(scratch); std::filesystem::create_directories(scratch);
{ auto database = potluckdb::open((scratch / "app.potluckdb").string()); database.execute("CREATE TABLE ledger(id INTEGER PRIMARY KEY, amount INTEGER)");
database.transaction([](potluckdb::Transaction &transaction) { transaction.execute("INSERT INTO ledger(amount) VALUES(?)", potluckdb::Bindings::positional({std::int64_t{100}})); transaction.execute("INSERT INTO ledger(amount) VALUES(?)", potluckdb::Bindings::positional({std::int64_t{-40}})); auto total = transaction.query("SELECT SUM(amount) FROM ledger"); expect(std::get<std::int64_t>(total[0][0]) == 60, "the batch adds up"); }); // The scope returned, so both rows committed. auto committed = database.query("SELECT COUNT(*) FROM ledger"); expect(std::get<std::int64_t>(committed[0][0]) == 2, "both rows committed");
try { database.transaction([](potluckdb::Transaction &transaction) { transaction.execute("INSERT INTO ledger(amount) VALUES(?)", potluckdb::Bindings::positional({std::int64_t{999}})); throw std::runtime_error("abort this batch"); }); expect(false && "the throw should have escaped", "the throw escapes the scope"); } catch (const std::runtime_error &) { // Expected: the exception leaves the scope after rolling back. } // The failed scope rolled back: still two rows. auto after = database.query("SELECT COUNT(*) FROM ledger"); expect(std::get<std::int64_t>(after[0][0]) == 2, "the rollback left two rows");
database.close(); }
std::filesystem::remove_all(scratch); std::puts("transactions: ok"); return 0;}Two writers
Section titled “Two writers”// Two devices that both write, and end up agreeing.//// This is what a local-first database is for, and it is the one sequence// worth learning: write on each device, publish, and let the second one// resolve. Concurrent writes are the ordinary case, not the exceptional// one -- whichever device publishes second finds the head has moved.
#include <unistd.h>
#include <cstdlib>#include <cstdio>#include <filesystem>#include <string>#include <variant>
#include "potluckdb.hpp"
namespace {// Not `assert`: NDEBUG erases that, and an example that prints "ok"// without having checked anything is worse than no example.void expect(bool condition, const char *description) { if (!condition) { std::fprintf(stderr, "expected: %s\n", description); std::abort(); }}} // namespace
int main() { auto scratch = std::filesystem::temp_directory_path() / ("potluckdb-two-writers-" + std::to_string(::getpid())); std::filesystem::remove_all(scratch); std::filesystem::create_directories(scratch);
{ auto remote = potluckdb::Remote::from_directory((scratch / "remote").string()); potluckdb::OpenOptions manual; manual.sync_policy = potluckdb::SyncPolicy::manual();
auto laptop = potluckdb::open((scratch / "laptop").string(), remote, manual); laptop.execute("CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)"); laptop.sync();
// A second device bootstraps from the published head, schema and all. // Creating the table again here is not a no-op -- it fails with // table already exists, because it does. auto phone = potluckdb::open((scratch / "phone").string(), remote, manual);
// Both write before either publishes. Neither names an id, so each // side assigns its own -- and both pick 1, because both are looking at // the same head. That is fine precisely because the statement does not // name it: replaying it on top of the winner assigns a fresh one. laptop.execute("INSERT INTO note(body) VALUES('buy milk')"); phone.execute("INSERT INTO note(body) VALUES('call the dentist')");
expect(std::holds_alternative<potluckdb::Pushed>(laptop.sync()), "the laptop publishes first");
// The phone published second, so it finds the head has moved. This // is the expected result of concurrent writing, not a failure. expect(std::holds_alternative<potluckdb::Conflict>(phone.sync()), "the head moved under the phone"); auto resolution = phone.resolve_conflict(); expect(resolution.quarantined.empty(), "nothing was given up to resolve this"); expect(std::holds_alternative<potluckdb::Pushed>(phone.sync()), "the phone publishes what it resolved");
// The laptop pulls what the phone resolved and published. expect(std::holds_alternative<potluckdb::Pulled>(laptop.sync()), "the laptop takes the phone's work");
for (auto *device : {&laptop, &phone}) { auto rows = device->query("SELECT body FROM note ORDER BY body"); expect(rows.size() == 2, "both devices see both notes"); expect(std::get<std::string>(rows[0][0]) == "buy milk", "the laptop's note"); expect(std::get<std::string>(rows[1][0]) == "call the dentist", "the phone's note"); }
laptop.close(); phone.close(); remote.close(); }
std::filesystem::remove_all(scratch); std::printf("two_writers: ok\n"); return 0;}