Skip to content
potluckdb

Examples

Objective-C: also in Python · Node · Go · Rust · Swift · Dart · Java and Kotlin · .NET · 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 error: detect it, resolve it, lose nothing.
//
// A losing sync returns a result whose outcome is Conflict, and leaves
// the NSError untouched. An out-parameter error is for things that went
// wrong; losing a race is an ordinary outcome with a defined next step.
#import <Foundation/Foundation.h>
#import "PotluckDBObjC.h"
// Not NSCAssert: NS_BLOCK_ASSERTIONS erases that, and an example that
// prints "ok" without having checked anything is worse than no example.
static void expect(BOOL condition, NSString *description) {
if (!condition) {
fprintf(stderr, "expected: %s\n", description.UTF8String);
abort();
}
}
int main(void) {
@autoreleasepool {
NSString *scratch = [NSTemporaryDirectory()
stringByAppendingPathComponent:@"potluckdb-conflict"];
NSFileManager *files = NSFileManager.defaultManager;
[files removeItemAtPath:scratch error:nil];
[files createDirectoryAtPath:scratch
withIntermediateDirectories:YES attributes:nil error:nil];
NSError *error = nil;
PLKRemote *remote = [PLKRemote
remoteFromDirectory:[scratch stringByAppendingPathComponent:@"remote"]
options:nil error:&error];
PLKOpenOptions *manual = [[PLKOpenOptions alloc] init];
manual.syncPolicy = [PLKSyncPolicy manualPolicy];
PLKDatabase *writer = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"writer"]
remote:remote options:manual error:&error];
expect(writer != nil, @"open failed");
[writer execute:@"CREATE TABLE item(id INTEGER PRIMARY KEY, value TEXT)"
bindings:nil error:&error];
[writer syncWithError:&error];
// 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.
PLKDatabase *rival = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"rival"]
remote:remote options:manual error:&error];
[rival execute:@"INSERT INTO item(value) VALUES('from rival')"
bindings:nil error:&error];
PLKSyncResult *won = [rival syncWithError:&error];
expect(won.outcome == PLKSyncOutcomePushed, @"the rival publishes first");
// The writer races the same head with its own local work.
[writer execute:@"INSERT INTO item(value) VALUES('from writer')"
bindings:nil error:&error];
PLKSyncResult *lost = [writer syncWithError:&error];
expect(lost.outcome == PLKSyncOutcomeConflict, @"the writer is told it lost");
expect(error == nil, @"losing a race is not an error");
PLKConflictResolution *resolution = [writer resolveConflictWithError:&error];
expect(resolution.revision != nil, @"resolution reports what it adopted");
PLKSyncResult *settled = [writer syncWithError:&error];
// Pushed, not "Pushed or UpToDate": resolving stages the replayed
// statement, so there is always something left to publish.
expect(settled.outcome == PLKSyncOutcomePushed,
@"the writer publishes what it resolved");
// Both sides' rows survive: the winner was adopted and the safe
// local statement replayed on top of it.
PLKRows *rows = [writer query:@"SELECT id, value FROM item ORDER BY id"
bindings:nil error:&error];
expect(rows.count == 2, @"nothing was lost");
expect([rows[0][1] isEqualToString:@"from rival"], @"the winner is there");
expect([rows[1][1] isEqualToString:@"from writer"], @"and so is the loser");
[writer closeWithError:&error];
[rival closeWithError:&error];
[remote closeWithError:&error];
[files removeItemAtPath:scratch error:nil];
printf("conflict_resolution: ok\n");
}
return 0;
}
// A local database: create it, write to it, read it back.
//
// No remote is involved, so nothing is uploaded and nothing is fetched.
// This is the whole API for a database that never leaves the machine.
#import <Foundation/Foundation.h>
#import "PotluckDBObjC.h"
// Not NSCAssert: NS_BLOCK_ASSERTIONS erases that, and an example that
// prints "ok" without having checked anything is worse than no example.
static void expect(BOOL condition, NSString *description) {
if (!condition) {
fprintf(stderr, "expected: %s\n", description.UTF8String);
abort();
}
}
int main(void) {
@autoreleasepool {
NSString *scratch = [NSTemporaryDirectory()
stringByAppendingPathComponent:@"potluckdb-quickstart"];
NSFileManager *files = NSFileManager.defaultManager;
[files removeItemAtPath:scratch error:nil];
[files createDirectoryAtPath:scratch
withIntermediateDirectories:YES attributes:nil error:nil];
NSError *error = nil;
PLKDatabase *database = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"app.potluckdb"]
remote:nil
options:nil
error:&error];
expect(database != nil, @"open failed");
[database execute:@"CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)"
bindings:nil
error:&error];
PLKExecuteResult *written =
[database execute:@"INSERT INTO note(body) VALUES(?)"
bindings:[PLKBindings positional:@[@"first note"]]
error:&error];
expect(written.rowsAffected == 1, @"one row was written");
expect(written.lastInsertID != nil, @"the new row has an identifier");
PLKRows *rows = [database query:@"SELECT id, body FROM note"
bindings:nil
error:&error];
expect([rows.columns isEqual:@[ @"id", @"body" ]], @"the result names its columns");
expect(rows.count == 1, @"the row is there");
expect([rows[0][@"body"] isEqualToString:@"first note"], @"and reads back by name");
[database closeWithError:&error];
[files removeItemAtPath:scratch error:nil];
printf("quickstart_local: ok\n");
}
return 0;
}
// Two devices share one remote: write on one, sync, read on the other.
//
// PLKSyncResult carries an outcome enumeration rather than a string, so
// the compiler checks the case you meant against the cases that exist.
#import <Foundation/Foundation.h>
#import "PotluckDBObjC.h"
// Not NSCAssert: NS_BLOCK_ASSERTIONS erases that, and an example that
// prints "ok" without having checked anything is worse than no example.
static void expect(BOOL condition, NSString *description) {
if (!condition) {
fprintf(stderr, "expected: %s\n", description.UTF8String);
abort();
}
}
int main(void) {
@autoreleasepool {
NSString *scratch = [NSTemporaryDirectory()
stringByAppendingPathComponent:@"potluckdb-remote-sync"];
NSFileManager *files = NSFileManager.defaultManager;
[files removeItemAtPath:scratch error:nil];
[files createDirectoryAtPath:scratch
withIntermediateDirectories:YES attributes:nil error:nil];
NSError *error = nil;
PLKRemote *remote = [PLKRemote
remoteFromDirectory:[scratch stringByAppendingPathComponent:@"remote"]
options:nil
error:&error];
expect(remote != nil, @"remote failed");
PLKOpenOptions *manual = [[PLKOpenOptions alloc] init];
manual.syncPolicy = [PLKSyncPolicy manualPolicy];
PLKDatabase *laptop = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"laptop"]
remote:remote options:manual error:&error];
expect(laptop != nil, @"open failed");
[laptop execute:@"CREATE TABLE task(id INTEGER PRIMARY KEY, title TEXT)"
bindings:nil error:&error];
[laptop execute:@"INSERT INTO task(title) VALUES(?)"
bindings:[PLKBindings positional:@[@"ship the release"]] error:&error];
PLKSyncResult *published = [laptop syncWithError:&error];
expect(published.outcome == PLKSyncOutcomePushed, @"the first sync publishes");
PLKDatabase *phone = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"phone"]
remote:remote options:manual error:&error];
PLKRows *rows = [phone query:@"SELECT title FROM task"
bindings:nil error:&error];
expect(rows.count == 1, @"the other device sees the row");
expect([rows[0][0] isEqualToString:@"ship the release"], @"with its value");
// An unchanged head is reported as exactly that.
PLKSyncResult *again = [phone syncWithError:&error];
expect(again.outcome == PLKSyncOutcomeUpToDate, @"an unchanged head is reported");
[laptop closeWithError:&error];
[phone closeWithError:&error];
[remote closeWithError:&error];
[files removeItemAtPath:scratch error:nil];
printf("remote_sync: ok\n");
}
return 0;
}
// Adding a second device, including one that opens before anything is published.
#import <Foundation/Foundation.h>
#import "PotluckDBObjC.h"
static void expect(BOOL condition, NSString *description) {
if (!condition) {
fprintf(stderr, "expected: %s\n", description.UTF8String);
abort();
}
}
int main(void) {
@autoreleasepool {
NSString *scratch = [NSTemporaryDirectory()
stringByAppendingPathComponent:@"potluckdb-second-device"];
NSFileManager *files = NSFileManager.defaultManager;
[files removeItemAtPath:scratch error:nil];
[files createDirectoryAtPath:scratch
withIntermediateDirectories:YES attributes:nil error:nil];
NSError *error = nil;
PLKRemote *remote = [PLKRemote
remoteFromDirectory:[scratch stringByAppendingPathComponent:@"remote"]
options:nil
error:&error];
expect(remote != nil, @"remote failed");
PLKOpenOptions *manual = [[PLKOpenOptions alloc] init];
manual.syncPolicy = [PLKSyncPolicy manualPolicy];
PLKDatabase *laptop = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"laptop"]
remote:remote options:manual error:&error];
expect(laptop != nil, @"open failed");
[laptop execute:@"CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)"
bindings:nil error:&error];
[laptop execute:@"INSERT INTO note VALUES(1, 'from the laptop')"
bindings:nil error:&error];
PLKSyncResult *published = [laptop syncWithError:&error];
expect(published.outcome == PLKSyncOutcomePushed, @"the first sync publishes");
PLKDatabase *phone = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"phone"]
remote:remote options:manual error:&error];
expect(phone != nil, @"phone open failed");
PLKRows *rows = [phone query:@"SELECT body FROM note"
bindings:nil error:&error];
expect(rows.count == 1, @"the other device sees the row");
expect([rows[0][0] isEqualToString:@"from the laptop"], @"with its value");
[phone closeWithError:&error];
PLKRemote *earlyRemote = [PLKRemote
remoteFromDirectory:[scratch stringByAppendingPathComponent:@"early-remote"]
options:nil
error:&error];
expect(earlyRemote != nil, @"early remote failed");
PLKDatabase *first = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"first"]
remote:earlyRemote options:manual error:&error];
expect(first != nil, @"first open failed");
PLKDatabase *tablet = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"tablet"]
remote:earlyRemote options:manual error:&error];
expect(tablet != nil, @"tablet open failed");
PLKDatabaseStatus *status = [tablet statusWithError:&error];
expect(status.pendingWrites == 0, @"opening records nothing to publish");
[first execute:@"CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)"
bindings:nil error:&error];
[first execute:@"INSERT INTO note VALUES(1, 'from the first device')"
bindings:nil error:&error];
PLKSyncResult *firstSync = [first syncWithError:&error];
expect(firstSync.outcome == PLKSyncOutcomePushed, @"the first device publishes");
PLKSyncResult *pulled = [tablet syncWithError:&error];
expect(pulled.outcome == PLKSyncOutcomePulled, @"a pristine device pulls");
PLKRows *visible = [tablet query:@"SELECT body FROM note"
bindings:nil error:&error];
expect(visible.count == 1, @"the tablet sees the published row");
expect([visible[0][0] isEqualToString:@"from the first device"],
@"with its value");
[first closeWithError:&error];
[tablet closeWithError:&error];
[earlyRemote closeWithError:&error];
[laptop closeWithError:&error];
[remote closeWithError:&error];
[files removeItemAtPath:scratch error:nil];
printf("second_device: ok\n");
}
return 0;
}
// Managed transactions: commit on success, rollback on failure.
//
// The block owns the scope. Returning YES commits; returning NO rolls
// back. Objective-C reports failure by return value rather than by
// unwinding, so the block says which it meant.
#import <Foundation/Foundation.h>
#import "PotluckDBObjC.h"
// Not NSCAssert: NS_BLOCK_ASSERTIONS erases that, and an example that
// prints "ok" without having checked anything is worse than no example.
static void expect(BOOL condition, NSString *description) {
if (!condition) {
fprintf(stderr, "expected: %s\n", description.UTF8String);
abort();
}
}
int main(void) {
@autoreleasepool {
NSString *scratch = [NSTemporaryDirectory()
stringByAppendingPathComponent:@"potluckdb-transactions"];
NSFileManager *files = NSFileManager.defaultManager;
[files removeItemAtPath:scratch error:nil];
[files createDirectoryAtPath:scratch
withIntermediateDirectories:YES attributes:nil error:nil];
NSError *error = nil;
PLKDatabase *database = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"app.potluckdb"]
remote:nil options:nil error:&error];
expect(database != nil, @"open failed");
[database execute:@"CREATE TABLE ledger(id INTEGER PRIMARY KEY, amount INTEGER)"
bindings:nil error:&error];
BOOL committed = [database
transactionWithMode:PLKTransactionModeImmediate
error:&error
block:^BOOL(PLKTransaction *transaction, NSError **blockError) {
[transaction execute:@"INSERT INTO ledger(amount) VALUES(?)"
bindings:[PLKBindings positional:@[@100]] error:blockError];
[transaction execute:@"INSERT INTO ledger(amount) VALUES(?)"
bindings:[PLKBindings positional:@[@(-40)]] error:blockError];
PLKRows *total =
[transaction query:@"SELECT SUM(amount) FROM ledger"
bindings:nil error:blockError];
expect([total[0][0] longLongValue] == 60, @"the batch adds up");
return YES;
}];
expect(committed, @"the scope succeeded");
PLKRows *rows = [database query:@"SELECT COUNT(*) FROM ledger"
bindings:nil error:&error];
expect([rows[0][0] longLongValue] == 2, @"both rows committed");
// A block that returns NO rolls back everything it did.
BOOL abandoned = [database
transactionWithMode:PLKTransactionModeImmediate
error:&error
block:^BOOL(PLKTransaction *transaction, NSError **blockError) {
[transaction execute:@"INSERT INTO ledger(amount) VALUES(?)"
bindings:[PLKBindings positional:@[@999]] error:blockError];
return NO;
}];
expect(!abandoned, @"the scope reported that it abandoned the batch");
rows = [database query:@"SELECT COUNT(*) FROM ledger" bindings:nil error:&error];
expect([rows[0][0] longLongValue] == 2, @"the rollback left two rows");
[database closeWithError:&error];
[files removeItemAtPath:scratch error:nil];
printf("transactions: ok\n");
}
return 0;
}
// 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 <Foundation/Foundation.h>
#import "PotluckDBObjC.h"
// Not NSCAssert: NS_BLOCK_ASSERTIONS erases that, and an example that
// prints "ok" without having checked anything is worse than no example.
static void expect(BOOL condition, NSString *description) {
if (!condition) {
fprintf(stderr, "expected: %s\n", description.UTF8String);
abort();
}
}
int main(void) {
@autoreleasepool {
NSString *scratch = [NSTemporaryDirectory() stringByAppendingPathComponent:
[NSString stringWithFormat:@"potluckdb-two-writers-%d", getpid()]];
NSFileManager *files = NSFileManager.defaultManager;
[files removeItemAtPath:scratch error:nil];
[files createDirectoryAtPath:scratch
withIntermediateDirectories:YES attributes:nil error:nil];
NSError *error = nil;
PLKRemote *remote = [PLKRemote
remoteFromDirectory:[scratch stringByAppendingPathComponent:@"remote"]
options:nil error:&error];
PLKOpenOptions *manual = [[PLKOpenOptions alloc] init];
manual.syncPolicy = [PLKSyncPolicy manualPolicy];
PLKDatabase *laptop = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"laptop"]
remote:remote options:manual error:&error];
expect(laptop != nil, @"open failed");
[laptop execute:@"CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)"
bindings:nil error:&error];
[laptop syncWithError:&error];
// 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.
PLKDatabase *phone = [PLKDatabase
openAtPath:[scratch stringByAppendingPathComponent:@"phone"]
remote:remote options:manual error:&error];
expect(phone != nil, @"the second device opened");
// 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')"
bindings:nil error:&error];
[phone execute:@"INSERT INTO note(body) VALUES('call the dentist')"
bindings:nil error:&error];
PLKSyncResult *published = [laptop syncWithError:&error];
expect(published.outcome == PLKSyncOutcomePushed, @"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.
PLKSyncResult *moved = [phone syncWithError:&error];
expect(moved.outcome == PLKSyncOutcomeConflict, @"the head moved under the phone");
expect(error == nil, @"losing a race is not an error");
PLKConflictResolution *resolution = [phone resolveConflictWithError:&error];
expect(resolution.quarantined.count == 0, @"nothing was given up to resolve this");
PLKSyncResult *settled = [phone syncWithError:&error];
expect(settled.outcome == PLKSyncOutcomePushed, @"the phone publishes what it resolved");
// The laptop pulls what the phone resolved and published.
PLKSyncResult *caught = [laptop syncWithError:&error];
expect(caught.outcome == PLKSyncOutcomePulled, @"the laptop takes the phone's work");
for (PLKDatabase *device in @[laptop, phone]) {
PLKRows *rows = [device query:@"SELECT body FROM note ORDER BY body"
bindings:nil error:&error];
expect(rows.count == 2, @"both devices see both notes");
expect([rows[0][0] isEqual:@"buy milk"], @"the laptop's note");
expect([rows[1][0] isEqual:@"call the dentist"], @"the phone's note");
}
[laptop closeWithError:&error];
[phone closeWithError:&error];
[remote closeWithError:&error];
[files removeItemAtPath:scratch error:nil];
printf("two_writers: ok\n");
}
return 0;
}