Skip to content
potluckdb

Quickstart for Objective-C

Python · Node · Go · Rust · Swift · Dart · JVM · .NET · C++ · Objective-C

This program shows two devices synchronizing through one remote. It resolves a conflict so both devices finish with the same row. See Concepts for the database and synchronization terms.

TermMeaning in this program
Local storeThe database files on one device.
RemoteThe shared copy of a database that devices synchronize with.
RevisionOne published database state that does not change.
HeadThe remote’s newest published revision.
PushSend local writes to the remote.
PullReceive writes from the remote.
PendingLocal writes that the device has not confirmed as published.
ReplayRun local statements again on the remote revision.
QuarantinePreserve a transaction that cannot replay safely, for the application to handle.

The two databases represent a laptop and a phone. Both change the same row before receiving the other’s change. The remote uses a temporary directory that the program removes when it exits.

  1. Complete the installation.
  2. Save this program as quickstart.m.
  3. Link potluckdbObjC.
  4. Run the binary.

The program prints two lines:

OutputMeaning
Use query for this statement.The program passes a read statement to execute:bindings:error: on purpose. The call fails with read_in_execute, and the program prints the error’s action from userInfo[PLKErrorActionKey].
quickstart: okThe program completed all checks, including checking that both devices contain the same row.

Without an accepted license key, a trial notice on standard error is expected. See license notices for the text. If the program fails or omits these lines, check the installation and any changes to the program.

// Two devices, one remote, one conflict, and its resolution.
#import <Foundation/Foundation.h>
#import "PotluckDBObjC.h"
// Every sync attempt reports an outcome, UpToDate included; wait for the requested one.
@interface Mailbox : NSObject
- (void)deliver:(PLKSyncResult *)outcome;
- (PLKSyncResult *)waitForOutcome:(PLKSyncOutcome)wanted;
@end
@implementation Mailbox {
NSMutableArray<PLKSyncResult *> *_outcomes;
dispatch_semaphore_t _arrived;
}
- (instancetype)init {
if ((self = [super init])) {
_outcomes = [NSMutableArray array];
_arrived = dispatch_semaphore_create(0);
}
return self;
}
- (void)deliver:(PLKSyncResult *)outcome {
@synchronized(self) { [_outcomes addObject:outcome]; }
dispatch_semaphore_signal(_arrived);
}
- (PLKSyncResult *)waitForOutcome:(PLKSyncOutcome)wanted {
while (YES) {
if (dispatch_semaphore_wait(_arrived, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)) != 0) {
fputs("no sync outcome within ten seconds\n", stderr);
exit(1);
}
PLKSyncResult *outcome;
@synchronized(self) {
outcome = _outcomes.firstObject;
[_outcomes removeObjectAtIndex:0];
}
if (outcome.outcome == PLKSyncOutcomeFailed) {
fprintf(stderr, "%s\n", outcome.error.localizedDescription.UTF8String);
exit(1);
}
if (outcome.outcome == wanted) return outcome;
}
}
@end
static void expect(BOOL condition, NSString *message) {
if (!condition) {
fprintf(stderr, "%s\n", message.UTF8String);
exit(1);
}
}
// One Pushed is one automatic cycle's work, not proof that everything written
// so far is published; a device is finished when it has nothing pending.
static void waitUntilPublished(PLKDatabase *device, Mailbox *events) {
while (YES) {
NSError *error = nil;
PLKDatabaseStatus *status = [device statusWithError:&error];
expect(status != nil, error.localizedDescription);
if (status.pendingWrites == 0) return;
[events waitForOutcome:PLKSyncOutcomePushed];
}
}
int main(void) {
@autoreleasepool {
NSString *scratch = [NSTemporaryDirectory()
stringByAppendingPathComponent:[NSString stringWithFormat:@"potluckdb-%d", getpid()]];
[[NSFileManager defaultManager] createDirectoryAtPath:scratch withIntermediateDirectories:YES attributes:nil error:NULL];
NSError *error = nil;
PLKRemote *remote = [PLKRemote remoteFromDirectory:[scratch stringByAppendingPathComponent:@"remote"]
options:nil
error:&error]; // a bucket replaces this in production; see the installation page
expect(remote != nil, error.localizedDescription);
// The laptop synchronizes on its own: automatic is the default policy.
// onSync delivers every outcome to the program;
// this program stores them in a mailbox.
Mailbox *laptopEvents = [[Mailbox alloc] init];
PLKOpenOptions *laptopOptions = [[PLKOpenOptions alloc] init];
laptopOptions.onSync = ^(PLKSyncResult *outcome) { [laptopEvents deliver:outcome]; };
PLKDatabase *laptop = [PLKDatabase openAtPath:[scratch stringByAppendingPathComponent:@"laptop"]
remote:remote
options:laptopOptions
error:&error];
expect(laptop != nil, error.localizedDescription);
BOOL committed = [laptop transactionWithMode:PLKTransactionModeDeferred
error:&error
block:^BOOL(PLKTransaction *setup, NSError **blockError) {
// one transaction: both statements travel in one push
return [setup execute:@"CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT NOT NULL)" bindings:nil error:blockError] != nil
&& [setup execute:@"INSERT INTO note(body) VALUES(?)"
bindings:[PLKBindings positional:@[ @"buy milk" ]]
error:blockError] != nil;
}];
expect(committed, error.localizedDescription);
waitUntilPublished(laptop, laptopEvents);
// The phone synchronizes only when instructed: a manual policy, and sync
// calls that return the outcome as a value. It starts from the revision the
// laptop published.
PLKOpenOptions *phoneOptions = [[PLKOpenOptions alloc] init];
phoneOptions.syncPolicy = [PLKSyncPolicy manualPolicy];
PLKDatabase *phone = [PLKDatabase openAtPath:[scratch stringByAppendingPathComponent:@"phone"]
remote:remote
options:phoneOptions
error:&error];
expect(phone != nil, error.localizedDescription);
PLKRows *rows = [phone query:@"SELECT id, body FROM note ORDER BY id" bindings:nil error:&error];
expect(rows != nil, error.localizedDescription);
expect([rows.columns isEqualToArray:@[ @"id", @"body" ]], @"columns are reported with the rows");
expect([rows[0][@"body"] isEqualToString:@"buy milk"] && [rows[0][0] isEqualToNumber:@1],
@"a row is read by column name or position");
// Both devices modify the same row before either has seen the other's write.
expect([laptop execute:@"UPDATE note SET body = ? WHERE id = ?"
bindings:[PLKBindings positional:@[ @"buy oat milk", @1 ]]
error:&error] != nil, error.localizedDescription);
expect([phone execute:@"UPDATE note SET body = ? WHERE id = ?"
bindings:[PLKBindings positional:@[ @"buy milk and eggs", @1 ]]
error:&error] != nil, error.localizedDescription);
waitUntilPublished(laptop, laptopEvents);
// The phone's push finds a remote head it has not seen:
// a conflict, returned rather than an error.
expect([phone syncWithError:&error].outcome == PLKSyncOutcomeConflict, @"the phone's push is refused with a conflict");
PLKConflictResolution *resolution = [phone resolveConflictWithError:&error]; // adopt the laptop's revision, replay the phone's statement on it
expect(resolution != nil && resolution.quarantined.count == 0, @"no statement was quarantined");
expect([phone syncWithError:&error].outcome == PLKSyncOutcomePushed, @"the phone's push succeeds after resolution");
// The laptop pulls the resolved revision on its own.
// Both devices hold the same row.
[laptopEvents waitForOutcome:PLKSyncOutcomePulled];
for (PLKDatabase *db in @[ laptop, phone ]) {
PLKRows *current = [db query:@"SELECT body FROM note WHERE id = ?"
bindings:[PLKBindings positional:@[ @1 ]]
error:&error];
expect(current != nil && [current[0][@"body"] isEqualToString:@"buy milk and eggs"], @"both devices hold the same row");
}
// query reads, execute writes. The wrong call is refused before execution.
NSError *refused = nil;
expect([laptop execute:@"SELECT count(*) FROM note" bindings:nil error:&refused] == nil, @"execute refuses a read");
expect([refused.userInfo[PLKErrorCodeKey] isEqualToString:@"read_in_execute"], @"the refusal names the code");
puts([refused.userInfo[PLKErrorActionKey] UTF8String]); // Use query for this statement.
expect([laptop closeWithError:&error], error.localizedDescription);
expect([phone closeWithError:&error], error.localizedDescription);
expect([remote closeWithError:&error], error.localizedDescription);
[[NSFileManager defaultManager] removeItemAtPath:scratch error:NULL];
puts("quickstart: ok");
}
return 0;
}

The remote uses a directory. No server runs. remoteFromDirectory:options:error: uses a directory that stands in for a bucket in a storage service. The installation page shows the constructor for a storage service. These constructors support S3, Google Cloud Storage, and Azure Blob Storage. The rest of the program stays the same.

The laptop synchronizes automatically. Automatic synchronization is the default policy, which decides when to synchronize. The laptop never calls syncWithError:. Its onSync hook, a function that receives synchronization results, reports every attempt. The hook receives a PLKSyncResult with an outcome field. A failed result includes the error.

The hook runs on the synchronization thread. Transfer the outcome to your own thread, as shown with a mailbox and a semaphore.

ResultMeaning
PLKSyncOutcomeUpToDateThe attempt found no pending writes and no remote revision to pull.
PLKSyncOutcomePushedThe attempt published its pending writes as a remote revision.
PLKSyncOutcomePulledThe attempt adopted a remote revision. It can also report replayed and quarantined transactions.
PLKSyncOutcomeConflictThe device’s push met a remote revision that the device had not seen.
PLKSyncOutcomeFailedAn automatic attempt failed and carries the error.

An automatic result covers one attempt. A PLKSyncResult whose outcome is PLKSyncOutcomePushed confirms the writes that the attempt published. Writes made after the attempt began can still be pending. If an earlier attempt failed, an explicit syncWithError: first publishes that attempt’s writes and leaves later writes pending. A program that must know its own writes are published reads statusWithError:’s pendingWrites. waitUntilPublished does that here before the second device opens.

Rows include their column names. rows.columns lists the names. You can read a row by column name or position.

The phone receives a conflict result. syncWithError: returns a PLKSyncResult whose outcome is PLKSyncOutcomeConflict when the remote has an unseen revision. This result is not an error. resolveConflictWithError: adopts the laptop’s revision and replays the phone’s statement. The phone’s text then appears on both devices. The resolution lists any transactions that cannot replay safely as quarantined.

query:bindings:error: reads and execute:bindings:error: writes. Each accepts exactly one statement of its own kind. Each refuses the other kind before execution, with NSError whose PLKErrorActionKey names the required call.

These calls promise different things. query:bindings:error: returns rows and uses the connection’s maximumRows to bound the result. A different SQL engine can answer a read. execute:bindings:error: changes data that a revision publishes and other devices receive.

This execution-intent policy separates reads from writes. SQLite preparation can have effects, including under EXPLAIN.

Neither call accepts SQL transaction control, which starts, commits, or rolls back a transaction.

Use transactionWithMode:error:block: for transaction control, as the setup shows.

Open and construct and Database describe every call. Options and policies describes the automatic policy and its parameters. The examples provide longer programs that the test suite runs.