Skip to content
potluckdb

Examples

Java and Kotlin: also in Python · Node · Go · Rust · Swift · Dart · .NET · C++ · 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 SyncResult.Conflict rather than throwing.
// Exceptions are for things that went wrong; losing a race is an ordinary
// outcome with a defined next step.
import dev.potluckdb.Database;
import dev.potluckdb.OpenOptions;
import dev.potluckdb.PotluckDB;
import dev.potluckdb.Remote;
import dev.potluckdb.Rows;
import dev.potluckdb.SyncPolicy;
import dev.potluckdb.SyncResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class ConflictResolution {
public static void main(String[] args) throws Exception {
Path scratch = Files.createTempDirectory("potluckdb-");
try (Remote remote = Remote.fromDirectory(scratch.resolve("remote").toString())) {
OpenOptions manual = OpenOptions.builder()
.syncPolicy(SyncPolicy.manual())
.build();
try (Database writer = PotluckDB.open(
scratch.resolve("writer").toString(), remote, manual);
Database rival = PotluckDB.open(
scratch.resolve("rival").toString(), remote, manual)) {
writer.execute("CREATE TABLE item(id INTEGER PRIMARY KEY, value TEXT)");
writer.sync();
rival.sync();
// 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(rival.sync() instanceof SyncResult.Pushed,
"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(writer.sync() instanceof SyncResult.Conflict,
"the writer is told it lost");
// Qualified: this example's own class shares the name.
dev.potluckdb.ConflictResolution resolution = writer.resolveConflict();
expect(resolution.revision() != null,
"resolution reports what it adopted");
// Pushed, not "Pushed or UpToDate": resolving stages the
// replayed statement, so there is always something to publish.
expect(writer.sync() instanceof SyncResult.Pushed,
"the writer publishes what it resolved");
// Both sides' rows survive: the winner was adopted and the
// safe local statement replayed on top of it.
Rows rows =
writer.query("SELECT id, value FROM item ORDER BY id");
expect(rows.size() == 2, "nothing was lost");
expect("from rival".equals(rows.get(0).get(1)),
"the winner is there");
expect("from writer".equals(rows.get(1).get(1)),
"and so is the loser");
}
} finally {
delete(scratch);
}
System.out.println("conflict_resolution: ok");
}
private static void expect(boolean condition, String description) {
// Not `assert`: the JVM ignores those unless started with -ea, and
// an example that prints "ok" having checked nothing is worse than
// no example.
if (!condition) {
throw new AssertionError(description);
}
}
private static void delete(Path root) throws Exception {
try (var walk = Files.walk(root)) {
walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (java.io.IOException ignored) {
// A scratch directory that outlives the example is not
// worth failing the example over.
}
});
}
}
}
// 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.
import dev.potluckdb.Bindings;
import dev.potluckdb.Database;
import dev.potluckdb.ExecuteResult;
import dev.potluckdb.PotluckDB;
import dev.potluckdb.Rows;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class QuickstartLocal {
public static void main(String[] args) throws Exception {
Path scratch = Files.createTempDirectory("potluckdb-");
try (Database database = PotluckDB.open(scratch.resolve("app.potluckdb").toString())) {
database.execute("CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)");
ExecuteResult written = database.execute(
"INSERT INTO note(body) VALUES(?)", Bindings.positional("first note"));
expect(written.rowsAffected() == 1, "one row was written");
expect(written.lastInsertId().isPresent(), "the new row has an identifier");
Rows rows = database.query("SELECT id, body FROM note");
expect(rows.columns().equals(List.of("id", "body")), "the result names its columns");
expect(rows.size() == 1, "the row is there");
expect("first note".equals(rows.get(0).get("body")), "and reads back by name");
} finally {
delete(scratch);
}
System.out.println("quickstart_local: ok");
}
static void expect(boolean condition, String description) {
// Not `assert`: the JVM ignores those unless started with -ea, and
// an example that prints "ok" having checked nothing is worse than
// no example.
if (!condition) {
throw new AssertionError(description);
}
}
static void delete(Path root) throws Exception {
try (var walk = Files.walk(root)) {
walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (java.io.IOException ignored) {
// A scratch directory that outlives the example is not
// worth failing the example over.
}
});
}
}
}
// Two devices share one remote: write on one, sync, read on the other.
//
// SyncResult is a closed hierarchy, so `instanceof` names the outcome you
// meant rather than comparing a string that might be spelled wrong.
import dev.potluckdb.Bindings;
import dev.potluckdb.Database;
import dev.potluckdb.OpenOptions;
import dev.potluckdb.PotluckDB;
import dev.potluckdb.Remote;
import dev.potluckdb.Rows;
import dev.potluckdb.SyncPolicy;
import dev.potluckdb.SyncResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class RemoteSync {
public static void main(String[] args) throws Exception {
Path scratch = Files.createTempDirectory("potluckdb-");
try (Remote remote = Remote.fromDirectory(scratch.resolve("remote").toString())) {
OpenOptions manual = OpenOptions.builder()
.syncPolicy(SyncPolicy.manual())
.build();
try (Database laptop = PotluckDB.open(
scratch.resolve("laptop").toString(), remote, manual);
Database phone = PotluckDB.open(
scratch.resolve("phone").toString(), remote, manual)) {
laptop.execute("CREATE TABLE task(id INTEGER PRIMARY KEY, title TEXT)");
laptop.execute("INSERT INTO task(title) VALUES(?)",
Bindings.positional("ship the release"));
expect(laptop.sync() instanceof SyncResult.Pushed,
"the first sync publishes");
expect(phone.sync() instanceof SyncResult.Pulled,
"the other device fetches it");
Rows rows = phone.query("SELECT title FROM task");
expect(rows.size() == 1, "the other device sees the row");
expect("ship the release".equals(rows.get(0).get(0)),
"with its value");
// An unchanged head is reported as exactly that.
expect(phone.sync() instanceof SyncResult.UpToDate,
"an unchanged head is reported");
}
} finally {
delete(scratch);
}
System.out.println("remote_sync: ok");
}
private static void expect(boolean condition, String description) {
// Not `assert`: the JVM ignores those unless started with -ea, and
// an example that prints "ok" having checked nothing is worse than
// no example.
if (!condition) {
throw new AssertionError(description);
}
}
private static void delete(Path root) throws Exception {
try (var walk = Files.walk(root)) {
walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (java.io.IOException ignored) {
// A scratch directory that outlives the example is not
// worth failing the example over.
}
});
}
}
}
// Adding a second device, including one that opens before anything is published.
import dev.potluckdb.Database;
import dev.potluckdb.OpenOptions;
import dev.potluckdb.PotluckDB;
import dev.potluckdb.Remote;
import dev.potluckdb.Rows;
import dev.potluckdb.SyncPolicy;
import dev.potluckdb.SyncResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class SecondDevice {
public static void main(String[] args) throws Exception {
Path scratch = Files.createTempDirectory("potluckdb-");
try (Remote remote = Remote.fromDirectory(scratch.resolve("remote").toString())) {
OpenOptions manual = OpenOptions.builder()
.syncPolicy(SyncPolicy.manual())
.build();
try (Database laptop = PotluckDB.open(
scratch.resolve("laptop").toString(), remote, manual)) {
laptop.execute("CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)");
laptop.execute("INSERT INTO note VALUES(1, 'from the laptop')");
expect(laptop.sync() instanceof SyncResult.Pushed,
"the first sync publishes");
// Opening now bootstraps from that published head, so the phone
// already has the table.
try (Database phone = PotluckDB.open(
scratch.resolve("phone").toString(), remote, manual)) {
Rows rows = phone.query("SELECT body FROM note");
expect(rows.size() == 1, "the other device sees the row");
expect("from the laptop".equals(rows.get(0).get(0)),
"with its value");
}
}
}
try (Remote earlyRemote = Remote.fromDirectory(
scratch.resolve("early-remote").toString())) {
OpenOptions manual = OpenOptions.builder()
.syncPolicy(SyncPolicy.manual())
.build();
// Devices may be opened in any order. One that opens against an
// empty remote has nothing to copy and nothing of its own to
// publish, so its first sync is an ordinary pull.
try (Database first = PotluckDB.open(
scratch.resolve("first").toString(), earlyRemote, manual);
Database tablet = PotluckDB.open(
scratch.resolve("tablet").toString(), earlyRemote, manual)) {
expect(tablet.status().pendingWrites() == 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(first.sync() instanceof SyncResult.Pushed,
"the first device publishes");
expect(tablet.sync() instanceof SyncResult.Pulled,
"a pristine device pulls");
Rows rows = tablet.query("SELECT body FROM note");
expect(rows.size() == 1, "the tablet sees the published row");
expect("from the first device".equals(rows.get(0).get(0)),
"with its value");
}
} finally {
delete(scratch);
}
System.out.println("second_device: ok");
}
private static void expect(boolean condition, String description) {
// Not `assert`: the JVM ignores those unless started with -ea, and
// an example that prints "ok" having checked nothing is worse than
// no example.
if (!condition) {
throw new AssertionError(description);
}
}
private static void delete(Path root) throws Exception {
try (var walk = Files.walk(root)) {
walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (java.io.IOException ignored) {
// A scratch directory that outlives the example is not
// worth failing the example over.
}
});
}
}
}
// Managed transactions: commit on success, rollback on failure.
//
// The function owns the scope. Returning commits; throwing rolls back.
// There is no way to leave a transaction open by forgetting to end it.
import dev.potluckdb.Bindings;
import dev.potluckdb.Database;
import dev.potluckdb.PotluckDB;
import java.nio.file.Files;
import java.nio.file.Path;
public final class Transactions {
public static void main(String[] args) throws Exception {
Path scratch = Files.createTempDirectory("potluckdb-");
try (Database database = PotluckDB.open(scratch.resolve("app.potluckdb").toString())) {
database.execute("CREATE TABLE ledger(id INTEGER PRIMARY KEY, amount INTEGER)");
long total = database.transaction(transaction -> {
transaction.execute("INSERT INTO ledger(amount) VALUES(?)",
Bindings.positional(100L));
transaction.execute("INSERT INTO ledger(amount) VALUES(?)",
Bindings.positional(-40L));
return (Long) transaction.query("SELECT SUM(amount) FROM ledger")
.get(0).get(0);
});
expect(total == 60, "the batch adds up");
// The function returned, so both rows committed.
expect(count(database) == 2, "both rows committed");
try {
database.transaction(transaction -> {
transaction.execute("INSERT INTO ledger(amount) VALUES(?)",
Bindings.positional(999L));
throw new IllegalStateException("abort this batch");
});
expect(false, "the throw escapes the scope");
} catch (IllegalStateException expected) {
// The exception leaves the scope after rolling back.
}
// The failed scope rolled back: still two rows.
expect(count(database) == 2, "the rollback left two rows");
} finally {
delete(scratch);
}
System.out.println("transactions: ok");
}
private static long count(Database database) {
return (Long) database.query("SELECT COUNT(*) FROM ledger").get(0).get(0);
}
private static void expect(boolean condition, String description) {
// Not `assert`: the JVM ignores those unless started with -ea, and
// an example that prints "ok" having checked nothing is worse than
// no example.
if (!condition) {
throw new AssertionError(description);
}
}
private static void delete(Path root) throws Exception {
try (var walk = Files.walk(root)) {
walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (java.io.IOException ignored) {
// A scratch directory that outlives the example is not
// worth failing the example over.
}
});
}
}
}
// 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.
import dev.potluckdb.ConflictResolution;
import dev.potluckdb.Database;
import dev.potluckdb.OpenOptions;
import dev.potluckdb.PotluckDB;
import dev.potluckdb.Remote;
import dev.potluckdb.Rows;
import dev.potluckdb.SyncPolicy;
import dev.potluckdb.SyncResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class TwoWriters {
public static void main(String[] args) throws Exception {
Path scratch = Files.createTempDirectory("potluckdb-");
try (Remote remote = Remote.fromDirectory(scratch.resolve("remote").toString())) {
OpenOptions manual = OpenOptions.builder()
.syncPolicy(SyncPolicy.manual())
.build();
try (Database laptop = PotluckDB.open(
scratch.resolve("laptop").toString(), 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.
try (Database phone = PotluckDB.open(
scratch.resolve("phone").toString(), 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(laptop.sync() instanceof SyncResult.Pushed,
"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(phone.sync() instanceof SyncResult.Conflict,
"the head moved under the phone");
ConflictResolution resolution = phone.resolveConflict();
expect(resolution.quarantined().isEmpty(),
"nothing was given up to resolve this");
expect(phone.sync() instanceof SyncResult.Pushed,
"the phone publishes what it resolved");
// The laptop pulls what the phone resolved and published.
expect(laptop.sync() instanceof SyncResult.Pulled,
"the laptop takes the phone's work");
for (Database device : List.of(laptop, phone)) {
Rows rows =
device.query("SELECT body FROM note ORDER BY body");
expect(rows.size() == 2
&& "buy milk".equals(rows.get(0).get(0))
&& "call the dentist".equals(rows.get(1).get(0)),
"both devices see both notes");
}
}
}
} finally {
try (var walk = Files.walk(scratch)) {
walk.sorted(java.util.Comparator.reverseOrder())
.forEach(path -> path.toFile().delete());
}
}
System.out.println("two_writers: ok");
}
private static void expect(boolean condition, String description) {
// Not `assert`: the JVM ignores those unless started with -ea, and
// an example that prints "ok" having checked nothing is worse than
// no example.
if (!condition) {
throw new AssertionError(description);
}
}
}