Examples
Dart: also in Python · Node · Go · Rust · Swift · 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, not a thrown error. Errors are// for things that went wrong; losing a race is an ordinary outcome with a// defined next step.
import 'dart:io';
import 'package:potluckdb/potluckdb.dart';
void main() { final scratch = Directory.systemTemp.createTempSync('potluckdb-'); try { final remote = Remote.fromDirectory('${scratch.path}/remote'); final manual = OpenOptions(syncPolicy: SyncPolicy.manual());
final writer = open( '${scratch.path}/writer', remote: remote, options: manual, ); writer.execute('CREATE TABLE item(id INTEGER PRIMARY KEY, value TEXT)'); writer.sync();
final rival = open( '${scratch.path}/rival', remote: remote, options: 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 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 Conflict, 'the writer is told it lost');
final resolution = writer.resolveConflict(); _expect(resolution.revision != null, 'resolution reports what it adopted'); // Pushed, not "pushed or up to date": resolving stages the replayed // statement, so there is always something left to publish. _expect(writer.sync() is 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. final rows = writer.query('SELECT id, value FROM item ORDER BY id'); _expect( rows.length == 2 && rows[0].columnAt(1) == 'from rival' && rows[1].columnAt(1) == 'from writer', 'nothing was lost', );
writer.close(); rival.close(); remote.close(); print('conflict_resolution: ok'); } finally { scratch.deleteSync(recursive: true); }}
void _expect(bool condition, String description) { if (!condition) throw StateError(description);}Quickstart local
Section titled “Quickstart local”// Local use needs no configuration: open, write, read, close.
import 'dart:io';
import 'package:potluckdb/potluckdb.dart';
void main() { final scratch = Directory.systemTemp.createTempSync('potluckdb-'); try { final database = open('${scratch.path}/app.potluckdb');
database.execute( 'CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT NOT NULL)', );
final 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. _expect(inserted.lastInsertId == 1, 'first row has id 1');
final rows = database.query('SELECT id, body FROM note ORDER BY id'); _expect( rows.columns.join(',') == 'id,body', 'the result names its columns', ); _expect( rows.length == 1 && rows[0].columnAt(0) == 1 && rows[0]['body'] == 'hello', 'the row reads back', );
database.close(); database.close(); // close is idempotent print('quickstart_local: ok'); } finally { scratch.deleteSync(recursive: true); }}
void _expect(bool condition, String description) { if (!condition) throw StateError(description);}Remote sync
Section titled “Remote sync”// Two devices share one remote: write on one, sync, read on the other.//// SyncResult is a sealed class, so a switch over its subtypes is checked// for exhaustiveness: adding an outcome breaks the code that ignores it.
import 'dart:io';
import 'package:potluckdb/potluckdb.dart';
void main() { final scratch = Directory.systemTemp.createTempSync('potluckdb-'); try { final remote = Remote.fromDirectory('${scratch.path}/remote'); final manual = OpenOptions(syncPolicy: SyncPolicy.manual());
final laptop = open( '${scratch.path}/laptop', remote: remote, options: 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 Pushed, 'the first sync publishes');
final phone = open( '${scratch.path}/phone', remote: remote, options: manual, ); final rows = phone.query('SELECT title FROM task'); _expect( rows.length == 1 && rows[0].columnAt(0) == 'ship the release', 'the other device sees the row', );
// An unchanged head is reported as exactly that. _expect(phone.sync() is UpToDate, 'an unchanged head is reported');
laptop.close(); phone.close(); remote.close(); print('remote_sync: ok'); } finally { scratch.deleteSync(recursive: true); }}
void _expect(bool condition, String description) { if (!condition) throw StateError(description);}Second device
Section titled “Second device”// Adding a second device, including one that opens before anything is published.
import 'dart:io';
import 'package:potluckdb/potluckdb.dart';
void main() { final scratch = Directory.systemTemp.createTempSync('potluckdb-'); try { final remote = Remote.fromDirectory('${scratch.path}/remote'); final manual = OpenOptions(syncPolicy: SyncPolicy.manual());
// The first device publishes before any other device opens. final laptop = open( '${scratch.path}/laptop', remote: remote, options: 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 Pushed, 'the first sync publishes');
// Opening now bootstraps from that published head, so the phone // already has the table. final phone = open( '${scratch.path}/phone', remote: remote, options: manual, ); final phoneRows = phone.query('SELECT body FROM note'); _expect( phoneRows.length == 1 && phoneRows[0].columnAt(0) == 'from the laptop', 'the other device sees the row', ); phone.close();
// 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. final earlyRemote = Remote.fromDirectory('${scratch.path}/early-remote'); final first = open( '${scratch.path}/first', remote: earlyRemote, options: manual, ); final tablet = open( '${scratch.path}/tablet', remote: earlyRemote, options: 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 Pushed, 'the first device publishes');
_expect(tablet.sync() is Pulled, 'a pristine device pulls'); final visible = tablet.query('SELECT body FROM note'); _expect( visible.length == 1 && visible[0].columnAt(0) == 'from the first device', 'the tablet sees the published row', );
first.close(); tablet.close(); earlyRemote.close(); laptop.close(); remote.close(); print('second_device: ok'); } finally { scratch.deleteSync(recursive: true); }}
void _expect(bool condition, String description) { if (!condition) throw StateError(description);}Transactions
Section titled “Transactions”// Managed transactions: commit on success, roll back on failure.//// The scope is a callback. Returning normally commits; throwing rolls// back and the error reaches the caller unchanged, so a failed batch// cannot be mistaken for a successful one.
import 'dart:io';
import 'package:potluckdb/potluckdb.dart';
class AbortBatch implements Exception {}
void main() { final scratch = Directory.systemTemp.createTempSync('potluckdb-'); try { final database = open('${scratch.path}/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([100]), ); transaction.execute( 'INSERT INTO ledger(amount) VALUES(?)', Bindings.positional([-40]), ); final total = transaction.query('SELECT SUM(amount) FROM ledger'); _expect( total[0].columnAt(0) == 60, 'the running total is visible inside the scope', ); });
final committed = database.query('SELECT COUNT(*) FROM ledger'); _expect(committed[0].columnAt(0) == 2, 'two rows committed');
var aborted = false; try { database.transaction((transaction) { transaction.execute( 'INSERT INTO ledger(amount) VALUES(?)', Bindings.positional([999]), ); throw AbortBatch(); }); } on AbortBatch { aborted = true; } _expect(aborted, 'the throwing scope reported failure');
final after = database.query('SELECT COUNT(*) FROM ledger'); _expect(after[0].columnAt(0) == 2, 'the failed batch left nothing behind');
database.close(); print('transactions: ok'); } finally { scratch.deleteSync(recursive: true); }}
void _expect(bool condition, String description) { if (!condition) throw StateError(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.
import 'dart:io';
import 'package:potluckdb/potluckdb.dart';
void main() { final scratch = Directory.systemTemp.createTempSync('potluckdb-'); try { final remote = Remote.fromDirectory('${scratch.path}/remote'); final manual = OpenOptions(syncPolicy: SyncPolicy.manual());
final laptop = open( '${scratch.path}/laptop', remote: remote, options: 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. final phone = open( '${scratch.path}/phone', remote: remote, options: 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 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() is Conflict, 'the head moved under the phone'); final resolution = phone.resolveConflict(); _expect( resolution.quarantined.isEmpty, 'nothing was given up to resolve this', ); _expect(phone.sync() is Pushed, 'the phone publishes what it resolved');
// The laptop pulls what the phone resolved and published. _expect(laptop.sync() is Pulled, 'the laptop takes the phone\'s work');
for (final device in [laptop, phone]) { final rows = device.query('SELECT body FROM note ORDER BY body'); _expect( rows.length == 2 && rows[0].columnAt(0) == 'buy milk' && rows[1].columnAt(0) == 'call the dentist', 'both devices see both notes', ); }
laptop.close(); phone.close(); remote.close(); print('two_writers: ok'); } finally { scratch.deleteSync(recursive: true); }}
// Not `assert`: Dart erases those outside checked mode, and an example// that prints "ok" without having checked anything is worse than none.void _expect(bool condition, String description) { if (!condition) { stderr.writeln('expected: $description'); exit(1); }}