Examples
Go: also in Python · Node · Rust · Swift · Dart · Java and Kotlin · .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 resolution
Section titled “Conflict resolution”// Conflict is data, not an exception: detect it, resolve it, lose nothing.//// A losing sync returns a Conflict value, not an error. Errors are for// things that went wrong; losing a race is an ordinary outcome with a// defined next step.package main
import ( "fmt" "log" "os" "path/filepath"
potluckdb "github.com/potluckdb/potluckdb-go")
func main() { scratch, err := os.MkdirTemp("", "potluckdb-") if err != nil { log.Fatal(err) } defer os.RemoveAll(scratch)
remote, err := potluckdb.RemoteFromDirectory(filepath.Join(scratch, "remote"), nil) if err != nil { log.Fatal(err) } manual := potluckdb.DefaultOpenOptions() manual.SyncPolicy = potluckdb.ManualSync()
writer, err := potluckdb.Open(filepath.Join(scratch, "writer"), remote, manual) if err != nil { log.Fatal(err) } if _, err := writer.Execute( "CREATE TABLE item(id INTEGER PRIMARY KEY, value TEXT)", ); err != nil { log.Fatal(err) } if _, err := writer.Sync(); err != nil { log.Fatal(err) }
// 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, err := potluckdb.Open(filepath.Join(scratch, "rival"), remote, manual) if err != nil { log.Fatal(err) } if _, err := rival.Execute( "INSERT INTO item(value) VALUES('from rival')", ); err != nil { log.Fatal(err) } rivalOutcome, err := rival.Sync() if err != nil { log.Fatal(err) } if _, published := rivalOutcome.(potluckdb.Pushed); !published { log.Fatalf("expected the rival to publish, got %T", rivalOutcome) }
// The writer races the same head with its own local work. if _, err := writer.Execute( "INSERT INTO item(value) VALUES('from writer')", ); err != nil { log.Fatal(err) } contested, err := writer.Sync() if err != nil { log.Fatal(err) } if _, lost := contested.(potluckdb.Conflict); !lost { log.Fatalf("expected the writer to be told it lost, got %T", contested) }
resolution, err := writer.ResolveConflict() if err != nil { log.Fatal(err) } if resolution.Revision == nil { log.Fatal("resolution did not report the revision it adopted") } settled, err := writer.Sync() if err != nil { log.Fatal(err) } // Pushed, not "Pushed or UpToDate": resolving stages the replayed // statement, so there is always something left to publish. if _, published := settled.(potluckdb.Pushed); !published { log.Fatalf("expected the writer to publish what it resolved, got %T", settled) }
// Both sides' rows survive: the winner was adopted and the safe local // statement replayed on top of it. rows, err := writer.Query("SELECT id, value FROM item ORDER BY id") if err != nil { log.Fatal(err) } if len(rows.Rows) != 2 || rows.Rows[0].Values[1] != "from rival" || rows.Rows[1].Values[1] != "from writer" { log.Fatalf("work was lost: %v", rows) }
for _, closer := range []func() error{writer.Close, rival.Close, remote.Close} { if err := closer(); err != nil { log.Fatal(err) } } fmt.Println("conflict_resolution: ok")}Quickstart local
Section titled “Quickstart local”// Local use needs no configuration: open, write, read, close.//// A statement's values follow its SQL as ordinary arguments, in order; a// statement with no values takes none. Named values travel as one// potluckdb.Named argument.package main
import ( "fmt" "log" "os" "path/filepath"
potluckdb "github.com/potluckdb/potluckdb-go")
func main() { scratch, err := os.MkdirTemp("", "potluckdb-") if err != nil { log.Fatal(err) } defer os.RemoveAll(scratch)
database, err := potluckdb.Open(filepath.Join(scratch, "app.potluckdb"), nil, nil) if err != nil { log.Fatal(err) }
if _, err := database.Execute( "CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT NOT NULL)", ); err != nil { log.Fatal(err) }
inserted, err := database.Execute( "INSERT INTO note(body) VALUES(?)", "hello", ) if err != nil { log.Fatal(err) } if inserted.RowsAffected != 1 { log.Fatalf("expected one row affected, got %d", inserted.RowsAffected) } // LastInsertID is a pointer: a statement that inserted nothing has no // identifier to report, and nil says so without inventing a zero. if inserted.LastInsertID == nil || *inserted.LastInsertID != 1 { log.Fatalf("expected first row to have id 1, got %v", inserted.LastInsertID) }
rows, err := database.Query("SELECT id, body FROM note ORDER BY id") if err != nil { log.Fatal(err) } // Values arrive as any; SQLite integers are int64. if len(rows.Rows) != 1 { log.Fatalf("unexpected rows: %v", rows) } if rows.Rows[0].At(0) != int64(1) || rows.Rows[0].Get("body") != "hello" { log.Fatalf("unexpected rows: %v", rows) }
if err := database.Close(); err != nil { log.Fatal(err) } if err := database.Close(); err != nil { // close is idempotent log.Fatal(err) } fmt.Println("quickstart_local: ok")}Remote sync
Section titled “Remote sync”// Two devices share one remote: write on one, sync, read on the other.//// Sync outcomes are distinct types rather than a status string, so a type// switch covers every case and the compiler notices when one is missed.package main
import ( "fmt" "log" "os" "path/filepath"
potluckdb "github.com/potluckdb/potluckdb-go")
func main() { scratch, err := os.MkdirTemp("", "potluckdb-") if err != nil { log.Fatal(err) } defer os.RemoveAll(scratch)
remote, err := potluckdb.RemoteFromDirectory(filepath.Join(scratch, "remote"), nil) if err != nil { log.Fatal(err) } manual := potluckdb.DefaultOpenOptions() manual.SyncPolicy = potluckdb.ManualSync()
laptop, err := potluckdb.Open(filepath.Join(scratch, "laptop"), remote, manual) if err != nil { log.Fatal(err) } if _, err := laptop.Execute( "CREATE TABLE task(id INTEGER PRIMARY KEY, title TEXT)", ); err != nil { log.Fatal(err) } if _, err := laptop.Execute( "INSERT INTO task(title) VALUES(?)", "ship the release", ); err != nil { log.Fatal(err) } outcome, err := laptop.Sync() if err != nil { log.Fatal(err) } if _, published := outcome.(potluckdb.Pushed); !published { log.Fatalf("expected the first sync to publish, got %T", outcome) }
phone, err := potluckdb.Open(filepath.Join(scratch, "phone"), remote, manual) if err != nil { log.Fatal(err) } rows, err := phone.Query("SELECT title FROM task") if err != nil { log.Fatal(err) } if len(rows.Rows) != 1 || rows.Rows[0].Values[0] != "ship the release" { log.Fatalf("the other device did not see the row: %v", rows) }
// An unchanged head is reported as exactly that. again, err := phone.Sync() if err != nil { log.Fatal(err) } if _, unchanged := again.(potluckdb.UpToDate); !unchanged { log.Fatalf("expected an unchanged head, got %T", again) }
for _, closer := range []func() error{laptop.Close, phone.Close, remote.Close} { if err := closer(); err != nil { log.Fatal(err) } } fmt.Println("remote_sync: ok")}Second device
Section titled “Second device”// Adding a second device, including one that opens before anything is published.package main
import ( "fmt" "log" "os" "path/filepath"
potluckdb "github.com/potluckdb/potluckdb-go")
func main() { scratch, err := os.MkdirTemp("", "potluckdb-") if err != nil { log.Fatal(err) } defer os.RemoveAll(scratch)
remote, err := potluckdb.RemoteFromDirectory(filepath.Join(scratch, "remote"), nil) if err != nil { log.Fatal(err) } manual := potluckdb.DefaultOpenOptions() manual.SyncPolicy = potluckdb.ManualSync()
// The first device publishes before any other device opens. laptop, err := potluckdb.Open(filepath.Join(scratch, "laptop"), remote, manual) if err != nil { log.Fatal(err) } if _, err := laptop.Execute( "CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)", ); err != nil { log.Fatal(err) } if _, err := laptop.Execute( "INSERT INTO note VALUES(1, 'from the laptop')", ); err != nil { log.Fatal(err) } outcome, err := laptop.Sync() if err != nil { log.Fatal(err) } if _, published := outcome.(potluckdb.Pushed); !published { log.Fatalf("expected the first sync to publish, got %T", outcome) }
// Opening now bootstraps from that published head, so the phone // already has the table. phone, err := potluckdb.Open(filepath.Join(scratch, "phone"), remote, manual) if err != nil { log.Fatal(err) } rows, err := phone.Query("SELECT body FROM note") if err != nil { log.Fatal(err) } if len(rows.Rows) != 1 || rows.Rows[0].Values[0] != "from the laptop" { log.Fatalf("the other device did not see the row: %v", rows) } if err := phone.Close(); err != nil { log.Fatal(err) }
// 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. earlyRemote, err := potluckdb.RemoteFromDirectory(filepath.Join(scratch, "early-remote"), nil) if err != nil { log.Fatal(err) } first, err := potluckdb.Open(filepath.Join(scratch, "first"), earlyRemote, manual) if err != nil { log.Fatal(err) } tablet, err := potluckdb.Open(filepath.Join(scratch, "tablet"), earlyRemote, manual) if err != nil { log.Fatal(err) } status, err := tablet.Status() if err != nil { log.Fatal(err) } if status.PendingWrites != 0 { log.Fatalf("a device that has written nothing has pending writes: %d", status.PendingWrites) } if _, err := first.Execute( "CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)", ); err != nil { log.Fatal(err) } if _, err := first.Execute( "INSERT INTO note VALUES(1, 'from the first device')", ); err != nil { log.Fatal(err) } firstOutcome, err := first.Sync() if err != nil { log.Fatal(err) } if _, published := firstOutcome.(potluckdb.Pushed); !published { log.Fatalf("expected the first device to publish, got %T", firstOutcome) }
pulled, err := tablet.Sync() if err != nil { log.Fatal(err) } if _, ok := pulled.(potluckdb.Pulled); !ok { log.Fatalf("expected a pristine device to pull, got %T", pulled) } visible, err := tablet.Query("SELECT body FROM note") if err != nil { log.Fatal(err) } if len(visible.Rows) != 1 || visible.Rows[0].Values[0] != "from the first device" { log.Fatalf("the tablet did not see the published row: %v", visible) }
for _, closer := range []func() error{ first.Close, tablet.Close, earlyRemote.Close, laptop.Close, remote.Close, } { if err := closer(); err != nil { log.Fatal(err) } } fmt.Println("second_device: ok")}Transactions
Section titled “Transactions”// Managed transactions: commit on success, roll back on failure.//// The scope is a function returning error. Returning nil commits;// returning anything else rolls back and the error reaches the caller,// so a failed batch cannot be mistaken for a successful one.package main
import ( "errors" "fmt" "log" "os" "path/filepath"
potluckdb "github.com/potluckdb/potluckdb-go")
var errAbort = errors.New("abort this batch")
func main() { scratch, err := os.MkdirTemp("", "potluckdb-") if err != nil { log.Fatal(err) } defer os.RemoveAll(scratch)
database, err := potluckdb.Open(filepath.Join(scratch, "app.potluckdb"), nil, nil) if err != nil { log.Fatal(err) } if _, err := database.Execute( "CREATE TABLE ledger(id INTEGER PRIMARY KEY, amount INTEGER)", ); err != nil { log.Fatal(err) }
if err := database.Transaction(func(transaction *potluckdb.Transaction) error { if _, err := transaction.Execute( "INSERT INTO ledger(amount) VALUES(?)", int64(100), ); err != nil { return err } if _, err := transaction.Execute( "INSERT INTO ledger(amount) VALUES(?)", int64(-40), ); err != nil { return err } rows, err := transaction.Query("SELECT SUM(amount) FROM ledger") if err != nil { return err } if rows.Rows[0].Values[0] != int64(60) { return fmt.Errorf("running total was %v", rows.Rows[0].Values[0]) } return nil }); err != nil { log.Fatal(err) }
counted, err := database.Query("SELECT COUNT(*) FROM ledger") if err != nil { log.Fatal(err) } if counted.Rows[0].Values[0] != int64(2) { log.Fatalf("expected two committed rows, got %v", counted.Rows[0].Values[0]) }
// A scope that returns an error rolls back, and that error is returned // unchanged so it can be matched with errors.Is. err = database.Transaction(func(transaction *potluckdb.Transaction) error { if _, err := transaction.Execute( "INSERT INTO ledger(amount) VALUES(?)", int64(999), ); err != nil { return err } return errAbort }) if !errors.Is(err, errAbort) { log.Fatalf("expected the abort to reach the caller, got %v", err) }
after, err := database.Query("SELECT COUNT(*) FROM ledger") if err != nil { log.Fatal(err) } if after.Rows[0].Values[0] != int64(2) { log.Fatalf("the failed batch left rows behind: %v", after.Rows[0].Values[0]) }
if err := database.Close(); err != nil { log.Fatal(err) } fmt.Println("transactions: ok")}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.package main
import ( "fmt" "log" "os" "path/filepath"
potluckdb "github.com/potluckdb/potluckdb-go")
func main() { scratch, err := os.MkdirTemp("", "potluckdb-") if err != nil { log.Fatal(err) } defer os.RemoveAll(scratch)
remote, err := potluckdb.RemoteFromDirectory(filepath.Join(scratch, "remote"), nil) if err != nil { log.Fatal(err) } manual := potluckdb.DefaultOpenOptions() manual.SyncPolicy = potluckdb.ManualSync()
laptop, err := potluckdb.Open(filepath.Join(scratch, "laptop"), remote, manual) if err != nil { log.Fatal(err) } if _, err := laptop.Execute( "CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)", ); err != nil { log.Fatal(err) } if _, err := laptop.Sync(); err != nil { log.Fatal(err) }
// 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. phone, err := potluckdb.Open(filepath.Join(scratch, "phone"), remote, manual) if err != nil { log.Fatal(err) }
// 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. if _, err := laptop.Execute( "INSERT INTO note(body) VALUES(?)", "buy milk", ); err != nil { log.Fatal(err) } if _, err := phone.Execute( "INSERT INTO note(body) VALUES(?)", "call the dentist", ); err != nil { log.Fatal(err) }
published, err := laptop.Sync() if err != nil { log.Fatal(err) } if _, ok := published.(potluckdb.Pushed); !ok { log.Fatalf("expected the laptop to publish, got %T", published) }
// The phone published second, so it finds the head has moved. This is // the expected result of concurrent writing, not a failure. lost, err := phone.Sync() if err != nil { log.Fatal(err) } if _, ok := lost.(potluckdb.Conflict); !ok { log.Fatalf("expected the head to have moved under the phone, got %T", lost) } resolution, err := phone.ResolveConflict() if err != nil { log.Fatal(err) } if len(resolution.Quarantined) != 0 { log.Fatalf("nothing should have been given up: %v", resolution.Quarantined) } settled, err := phone.Sync() if err != nil { log.Fatal(err) } if _, ok := settled.(potluckdb.Pushed); !ok { log.Fatalf("expected the phone to publish what it resolved, got %T", settled) }
// The laptop pulls what the phone resolved and published. caught, err := laptop.Sync() if err != nil { log.Fatal(err) } if _, ok := caught.(potluckdb.Pulled); !ok { log.Fatalf("expected the laptop to pull, got %T", caught) }
for name, device := range map[string]*potluckdb.Database{"laptop": laptop, "phone": phone} { rows, err := device.Query("SELECT body FROM note ORDER BY body") if err != nil { log.Fatal(err) } if len(rows.Rows) != 2 || rows.Rows[0].Values[0] != "buy milk" || rows.Rows[1].Values[0] != "call the dentist" { log.Fatalf("the %s did not converge: %v", name, rows) } }
for _, closer := range []func() error{laptop.Close, phone.Close, remote.Close} { if err := closer(); err != nil { log.Fatal(err) } } fmt.Println("two_writers: ok")}