Examples
.NET: also in Python · Node · Go · Rust · Swift · Dart · Java and Kotlin · 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 resolution
Section titled “Conflict resolution”// Conflict is data, not an exception: detect it, resolve it, lose nothing.//// A losing sync returns a SyncResult.Conflict, not a thrown exception.// Exceptions are for things that went wrong; losing a race is an ordinary// outcome with a defined next step.
using System;using System.IO;using PotluckDB;
internal static class Program{ private static void Main() { var scratch = Directory.CreateTempSubdirectory("potluckdb-"); try { using var remote = Remote.FromDirectory(Path.Combine(scratch.FullName, "remote")); var manual = new OpenOptions(syncPolicy: SyncPolicy.Manual());
using var writer = Database.Open( Path.Combine(scratch.FullName, "writer"), remote, manual); writer.Execute("CREATE TABLE item(id INTEGER PRIMARY KEY, value TEXT)"); writer.Sync();
using var rival = Database.Open( Path.Combine(scratch.FullName, "rival"), 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(rival.Sync() is 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() is SyncResult.Conflict, "the writer is told it lost");
var 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() is 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. var rows = writer.Query("SELECT id, value FROM item ORDER BY id"); Expect(rows.Count == 2 && (string)rows[0][1] == "from rival" && (string)rows[1][1] == "from writer", "nothing was lost");
writer.Close(); rival.Close(); remote.Close(); Console.WriteLine("conflict_resolution: ok"); } finally { scratch.Delete(recursive: true); } }
private static void Expect(bool condition, string description) { if (!condition) throw new InvalidOperationException(description); }}Quickstart local
Section titled “Quickstart local”// Local use needs no configuration: open, write, read, close.
using System;using System.IO;using PotluckDB;
internal static class Program{ private static void Main() { var scratch = Directory.CreateTempSubdirectory("potluckdb-"); try { using var database = Database.Open(Path.Combine(scratch.FullName, "app.potluckdb"));
database.Execute("CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT NOT NULL)");
var inserted = database.Execute( "INSERT INTO note(body) VALUES(?)", Bindings.Positional("hello")); Expect(inserted.RowsAffected == 1, "one row inserted"); // LastInsertId is nullable: a statement that inserted nothing has // no identifier to report, and null says so without inventing a // zero that reads like a real row. Expect(inserted.LastInsertId == 1, "first row has id 1");
var rows = database.Query("SELECT id, body FROM note ORDER BY id"); Expect(string.Join(",", rows.Columns) == "id,body", "the result names its columns"); Expect(rows.Count == 1 && (long)rows[0][0] == 1 && (string)rows[0]["body"] == "hello", "the row reads back");
database.Close(); database.Close(); // close is idempotent Console.WriteLine("quickstart_local: ok"); } finally { scratch.Delete(recursive: true); } }
private static void Expect(bool condition, string description) { if (!condition) throw new InvalidOperationException(description); }}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 a pattern match names the outcome// you meant rather than comparing a string.
using System;using System.IO;using PotluckDB;
internal static class Program{ private static void Main() { var scratch = Directory.CreateTempSubdirectory("potluckdb-"); try { using var remote = Remote.FromDirectory(Path.Combine(scratch.FullName, "remote")); var manual = new OpenOptions(syncPolicy: SyncPolicy.Manual());
using var laptop = Database.Open( Path.Combine(scratch.FullName, "laptop"), 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() is SyncResult.Pushed, "the first sync publishes");
using var phone = Database.Open( Path.Combine(scratch.FullName, "phone"), remote, manual); var rows = phone.Query("SELECT title FROM task"); Expect(rows.Count == 1 && (string)rows[0][0] == "ship the release", "the other device sees the row");
// An unchanged head is reported as exactly that. Expect(phone.Sync() is SyncResult.UpToDate, "an unchanged head is reported");
laptop.Close(); phone.Close(); remote.Close(); Console.WriteLine("remote_sync: ok"); } finally { scratch.Delete(recursive: true); } }
private static void Expect(bool condition, string description) { if (!condition) throw new InvalidOperationException(description); }}Second device
Section titled “Second device”// Adding a second device, including one that opens before anything is published.
using System;using System.IO;using PotluckDB;
internal static class Program{ private static void Main() { var scratch = Directory.CreateTempSubdirectory("potluckdb-"); try { using var remote = Remote.FromDirectory(Path.Combine(scratch.FullName, "remote")); var manual = new OpenOptions(syncPolicy: SyncPolicy.Manual());
using var laptop = Database.Open( Path.Combine(scratch.FullName, "laptop"), 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() is SyncResult.Pushed, "the first sync publishes");
using (var phone = Database.Open( Path.Combine(scratch.FullName, "phone"), remote, manual)) { var rows = phone.Query("SELECT body FROM note"); Expect(rows.Count == 1 && (string)rows[0][0] == "from the laptop", "the other device sees the row"); }
using var earlyRemote = Remote.FromDirectory( Path.Combine(scratch.FullName, "early-remote")); using var first = Database.Open( Path.Combine(scratch.FullName, "first"), earlyRemote, manual); using var tablet = Database.Open( Path.Combine(scratch.FullName, "tablet"), 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() is SyncResult.Pushed, "the first device publishes");
Expect(tablet.Sync() is SyncResult.Pulled, "a pristine device pulls"); var visible = tablet.Query("SELECT body FROM note"); Expect(visible.Count == 1 && (string)visible[0][0] == "from the first device", "the tablet sees the published row");
first.Close(); tablet.Close(); earlyRemote.Close(); laptop.Close(); remote.Close(); Console.WriteLine("second_device: ok"); } finally { scratch.Delete(recursive: true); } }
private static void Expect(bool condition, string description) { if (!condition) throw new InvalidOperationException(description); }}Transactions
Section titled “Transactions”// Managed transactions: commit on success, roll back on failure.//// The scope is a delegate. Returning normally commits; throwing rolls// back and the exception reaches the caller unchanged, so a failed batch// cannot be mistaken for a successful one.
using System;using System.IO;using PotluckDB;
internal sealed class AbortBatch : Exception { }
internal static class Program{ private static void Main() { var scratch = Directory.CreateTempSubdirectory("potluckdb-"); try { using var database = Database.Open(Path.Combine(scratch.FullName, "app.potluckdb")); database.Execute("CREATE TABLE ledger(id INTEGER PRIMARY KEY, amount INTEGER)");
database.Transaction(transaction => { transaction.Execute( "INSERT INTO ledger(amount) VALUES(?)", Bindings.Positional(100L)); transaction.Execute( "INSERT INTO ledger(amount) VALUES(?)", Bindings.Positional(-40L)); var total = transaction.Query("SELECT SUM(amount) FROM ledger"); Expect((long)total[0][0] == 60, "the running total is visible inside the scope"); });
var committed = database.Query("SELECT COUNT(*) FROM ledger"); Expect((long)committed[0][0] == 2, "two rows committed");
var aborted = false; try { database.Transaction(transaction => { transaction.Execute( "INSERT INTO ledger(amount) VALUES(?)", Bindings.Positional(999L)); throw new AbortBatch(); }); } catch (AbortBatch) { aborted = true; } Expect(aborted, "the throwing scope reported failure");
var after = database.Query("SELECT COUNT(*) FROM ledger"); Expect((long)after[0][0] == 2, "the failed batch left nothing behind");
database.Close(); Console.WriteLine("transactions: ok"); } finally { scratch.Delete(recursive: true); } }
private static void Expect(bool condition, string description) { if (!condition) throw new InvalidOperationException(description); }}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.
using System;using System.IO;using PotluckDB;
internal static class Program{ private static void Main() { var scratch = Directory.CreateTempSubdirectory("potluckdb-"); try { using var remote = Remote.FromDirectory(Path.Combine(scratch.FullName, "remote")); var manual = new OpenOptions(syncPolicy: SyncPolicy.Manual());
using var laptop = Database.Open( Path.Combine(scratch.FullName, "laptop"), 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. using var phone = Database.Open( Path.Combine(scratch.FullName, "phone"), 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() is 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. Expect(phone.Sync() is SyncResult.Conflict, "the head moved under the phone"); var resolution = phone.ResolveConflict(); Expect(resolution.Quarantined.Count == 0, "nothing was given up to resolve this"); Expect(phone.Sync() is SyncResult.Pushed, "the phone publishes what it resolved");
// The laptop pulls what the phone resolved and published. Expect(laptop.Sync() is SyncResult.Pulled, "the laptop takes the phone's work");
foreach (var device in new[] { laptop, phone }) { var rows = device.Query("SELECT body FROM note ORDER BY body"); Expect(rows.Count == 2 && (string)rows[0][0] == "buy milk" && (string)rows[1][0] == "call the dentist", "both devices see both notes"); }
laptop.Close(); phone.Close(); remote.Close(); Console.WriteLine("two_writers: ok"); } finally { scratch.Delete(recursive: true); } }
private static void Expect(bool condition, string description) { if (!condition) throw new InvalidOperationException(description); }}