Skip to content
potluckdb

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