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 resolution
Section titled “Conflict resolution”// 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;}Quickstart local
Section titled “Quickstart local”// 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;}Remote sync
Section titled “Remote sync”// 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;}Second device
Section titled “Second device”// 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;}Transactions
Section titled “Transactions”// 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 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 <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;}