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.
Conflictresolution
Section titled “Conflictresolution”// 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. } }); } }}Quickstartlocal
Section titled “Quickstartlocal”// 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. } }); } }}Remotesync
Section titled “Remotesync”// 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. } }); } }}Seconddevice
Section titled “Seconddevice”// 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. } }); } }}Transactions
Section titled “Transactions”// 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. } }); } }}Twowriters
Section titled “Twowriters”// 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); } }}