Examples
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 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. } }); } }}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.
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. } }); } }}Remote sync
Section titled “Remote sync”// 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. } }); } }}Second device
Section titled “Second device”// 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. } }); } }}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.
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); } }}