Skip to content
potluckdb

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