Skip to content
potluckdb

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 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;
}
// 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;
}
// 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;
}
// 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;
}
// 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 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;
}