Skip to content
potluckdb

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