Skip to content
potluckdb

Examples

Rust: also in Python · Node · Go · Swift · Dart · 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 is a `SyncResult::Conflict` value, not an `Err`. Errors
//! are for things that went wrong; losing a race is an ordinary outcome
//! with a defined next step.
use potluckdb::{OpenOptions, Remote, SqlValue, SyncPolicy, SyncResult};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let scratch = scratch_dir()?;
let remote = Remote::from_directory(scratch.join("remote"))?;
let manual = OpenOptions {
sync_policy: SyncPolicy::manual(),
..Default::default()
};
let mut writer =
potluckdb::open_with_options(scratch.join("writer"), Some(&remote), manual.clone())?;
writer.execute(
"CREATE TABLE item(id INTEGER PRIMARY KEY, value TEXT)",
(),
)?;
writer.sync()?;
// 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.
let mut rival = potluckdb::open_with_options(scratch.join("rival"), Some(&remote), manual)?;
rival.execute(
"INSERT INTO item(value) VALUES('from rival')",
(),
)?;
match rival.sync()? {
SyncResult::Pushed { .. } => {}
other => panic!("expected the rival to publish, got {other:?}"),
}
// The writer races the same head with its own local work.
writer.execute(
"INSERT INTO item(value) VALUES('from writer')",
(),
)?;
match writer.sync()? {
SyncResult::Conflict { .. } => {}
other => panic!("expected the writer to be told it lost, got {other:?}"),
}
let resolution = writer.resolve_conflict()?;
// `revision` is a plain [u8; 32] rather than an Option: a resolution
// always adopts one, and the type says so, where Go and Node hand back
// something nullable that a caller has to check.
assert_eq!(resolution.revision.len(), 32);
// Pushed, not "Pushed or UpToDate": resolving stages the replayed
// statement, so there is always something left to publish.
match writer.sync()? {
SyncResult::Pushed { .. } => {}
other => panic!("expected the writer to publish what it resolved, got {other:?}"),
}
// Both sides' rows survive: the winner was adopted and the safe local
// statement replayed on top of it.
let rows = writer.query(
"SELECT id, value FROM item ORDER BY id",
(),
)?;
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][1], SqlValue::Text("from rival".into()));
assert_eq!(rows[1][1], SqlValue::Text("from writer".into()));
writer.close()?;
rival.close()?;
drop(remote);
std::fs::remove_dir_all(&scratch).ok();
println!("conflict_resolution: ok");
Ok(())
}
fn scratch_dir() -> std::io::Result<std::path::PathBuf> {
let mut path = std::env::temp_dir();
path.push(format!("potluckdb-conflict-{}", std::process::id()));
std::fs::create_dir_all(&path)?;
Ok(path)
}
//! Local use needs no configuration: open, write, read, close.
//!
//! Values are a `SqlValue` enum in both directions. That is more typing
//! than a dynamically typed binding, and in exchange a column that is not
//! the type you expected is a compile-time `match` arm rather than a
//! surprise at run time.
use potluckdb::SqlValue;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let scratch = tempdir()?;
let mut database = potluckdb::open(scratch.join("app.potluckdb"))?;
database.execute(
"CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT NOT NULL)",
(),
)?;
let inserted = database.execute(
"INSERT INTO note(body) VALUES(?)",
("hello",),
)?;
assert_eq!(inserted.rows_affected, 1);
// `last_insert_id` is an Option: a statement that inserted nothing has
// no identifier to report, and None says so without inventing a zero.
assert_eq!(inserted.last_insert_id, Some(1));
let rows = database.query(
"SELECT id, body FROM note ORDER BY id",
(),
)?;
assert_eq!(rows.columns(), ["id", "body"]);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get::<i64>(0)?, 1);
assert_eq!(rows[0].get::<String>("body")?, "hello");
assert_eq!(rows[0].get::<Option<String>>("body")?.as_deref(), Some("hello"));
assert_eq!(rows[0][0], SqlValue::Integer(1));
assert_eq!(rows[0]["body"], SqlValue::Text("hello".into()));
database.close()?;
database.close()?; // close is idempotent
std::fs::remove_dir_all(&scratch).ok();
println!("quickstart_local: ok");
Ok(())
}
fn tempdir() -> std::io::Result<std::path::PathBuf> {
let mut path = std::env::temp_dir();
path.push(format!("potluckdb-{}", std::process::id()));
std::fs::create_dir_all(&path)?;
Ok(path)
}
//! Two devices share one remote: write on one, sync, read on the other.
//!
//! `SyncResult` is an enum, so every outcome is a `match` arm and the
//! compiler objects when a new one is added and left unhandled.
use potluckdb::{OpenOptions, Remote, SqlValue, SyncPolicy, SyncResult};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let scratch = scratch_dir()?;
let remote = Remote::from_directory(scratch.join("remote"))?;
let manual = OpenOptions {
sync_policy: SyncPolicy::manual(),
..Default::default()
};
let mut laptop =
potluckdb::open_with_options(scratch.join("laptop"), Some(&remote), manual.clone())?;
laptop.execute(
"CREATE TABLE task(id INTEGER PRIMARY KEY, title TEXT)",
(),
)?;
laptop.execute(
"INSERT INTO task(title) VALUES(?)",
("ship the release",),
)?;
match laptop.sync()? {
SyncResult::Pushed { .. } => {}
other => panic!("expected the first sync to publish, got {other:?}"),
}
let mut phone = potluckdb::open_with_options(scratch.join("phone"), Some(&remote), manual)?;
let rows = phone.query("SELECT title FROM task", ())?;
assert_eq!(rows[0][0], SqlValue::Text("ship the release".into()));
// An unchanged head is reported as exactly that.
match phone.sync()? {
SyncResult::UpToDate { .. } => {}
other => panic!("expected an unchanged head, got {other:?}"),
}
laptop.close()?;
phone.close()?;
drop(remote);
std::fs::remove_dir_all(&scratch).ok();
println!("remote_sync: ok");
Ok(())
}
fn scratch_dir() -> std::io::Result<std::path::PathBuf> {
let mut path = std::env::temp_dir();
path.push(format!("potluckdb-sync-{}", std::process::id()));
std::fs::create_dir_all(&path)?;
Ok(path)
}
//! Adding a second device, including one that opens before anything is published.
use potluckdb::{OpenOptions, Remote, SqlValue, SyncPolicy, SyncResult};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let scratch = scratch_dir()?;
let remote = Remote::from_directory(scratch.join("remote"))?;
let manual = OpenOptions {
sync_policy: SyncPolicy::manual(),
..Default::default()
};
// The first device publishes before any other device opens.
let mut laptop =
potluckdb::open_with_options(scratch.join("laptop"), Some(&remote), manual.clone())?;
laptop.execute(
"CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)",
(),
)?;
laptop.execute(
"INSERT INTO note VALUES(1, 'from the laptop')",
(),
)?;
match laptop.sync()? {
SyncResult::Pushed { .. } => {}
other => panic!("expected the first sync to publish, got {other:?}"),
}
// Opening now bootstraps from that published head, so the phone
// already has the table.
let mut phone =
potluckdb::open_with_options(scratch.join("phone"), Some(&remote), manual.clone())?;
let rows = phone.query("SELECT body FROM note", ())?;
assert_eq!(rows[0][0], SqlValue::Text("from the laptop".into()));
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.
let early_remote = Remote::from_directory(scratch.join("early-remote"))?;
let mut first =
potluckdb::open_with_options(scratch.join("first"), Some(&early_remote), manual.clone())?;
let mut tablet =
potluckdb::open_with_options(scratch.join("tablet"), Some(&early_remote), manual)?;
assert_eq!(
tablet.status()?.pending_writes,
0,
"a device that has written nothing has 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')",
(),
)?;
match first.sync()? {
SyncResult::Pushed { .. } => {}
other => panic!("expected the first device to publish, got {other:?}"),
}
match tablet.sync()? {
SyncResult::Pulled { .. } => {}
other => panic!("expected a pristine device to pull, got {other:?}"),
}
let visible = tablet.query("SELECT body FROM note", ())?;
assert_eq!(visible[0][0], SqlValue::Text("from the first device".into()));
first.close()?;
tablet.close()?;
drop(early_remote);
laptop.close()?;
drop(remote);
std::fs::remove_dir_all(&scratch).ok();
println!("second_device: ok");
Ok(())
}
fn scratch_dir() -> std::io::Result<std::path::PathBuf> {
let mut path = std::env::temp_dir();
path.push(format!("potluckdb-second-device-{}", std::process::id()));
std::fs::create_dir_all(&path)?;
Ok(path)
}
//! Managed transactions: commit on success, roll back on failure.
//!
//! The scope is a closure returning `Result`. Returning `Ok` commits;
//! returning `Err` rolls back and the error reaches the caller unchanged,
//! so a failed batch cannot be mistaken for a successful one.
use potluckdb::{PotluckDBError, SqlValue};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let scratch = scratch_dir()?;
let mut database = potluckdb::open(scratch.join("app.potluckdb"))?;
database.execute(
"CREATE TABLE ledger(id INTEGER PRIMARY KEY, amount INTEGER)",
(),
)?;
database.transaction(|transaction| {
transaction.execute(
"INSERT INTO ledger(amount) VALUES(?)",
(100,),
)?;
transaction.execute(
"INSERT INTO ledger(amount) VALUES(?)",
(-40,),
)?;
let rows = transaction.query("SELECT SUM(amount) FROM ledger", ())?;
assert_eq!(rows[0][0], SqlValue::Integer(60));
Ok(())
})?;
let committed = database.query("SELECT COUNT(*) FROM ledger", ())?;
assert_eq!(committed[0][0], SqlValue::Integer(2));
// A scope returning Err rolls back. The error type is the SDK's, so a
// caller-defined failure travels as a message rather than a new type.
let aborted: Result<(), PotluckDBError> = database.transaction(|transaction| {
transaction.execute(
"INSERT INTO ledger(amount) VALUES(?)",
(999,),
)?;
transaction.execute("THIS IS NOT SQL", ())?;
Ok(())
});
assert!(aborted.is_err(), "the failing scope must report failure");
let after = database.query("SELECT COUNT(*) FROM ledger", ())?;
assert_eq!(
after[0][0],
SqlValue::Integer(2),
"the failed batch left nothing"
);
database.close()?;
std::fs::remove_dir_all(&scratch).ok();
println!("transactions: ok");
Ok(())
}
fn scratch_dir() -> std::io::Result<std::path::PathBuf> {
let mut path = std::env::temp_dir();
path.push(format!("potluckdb-tx-{}", std::process::id()));
std::fs::create_dir_all(&path)?;
Ok(path)
}
//! 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 and
//! says so.
use potluckdb::{OpenOptions, Remote, SqlValue, SyncPolicy, SyncResult};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let scratch = scratch_dir()?;
let remote = Remote::from_directory(scratch.join("remote"))?;
let manual = OpenOptions {
sync_policy: SyncPolicy::manual(),
..Default::default()
};
let mut laptop =
potluckdb::open_with_options(scratch.join("laptop"), Some(&remote), manual.clone())?;
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.
let mut phone = potluckdb::open_with_options(scratch.join("phone"), Some(&remote), 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')",
(),
)?;
match laptop.sync()? {
SyncResult::Pushed { .. } => {}
other => panic!("expected the laptop to publish, got {other:?}"),
}
// The phone published second, so it finds the head has moved. This is
// the expected result of concurrent writing, not a failure.
match phone.sync()? {
SyncResult::Conflict { .. } => {}
other => panic!("expected the phone to be told the head moved, got {other:?}"),
}
let resolution = phone.resolve_conflict()?;
assert!(
resolution.quarantined.is_empty(),
"nothing was given up to resolve this"
);
match phone.sync()? {
SyncResult::Pushed { .. } => {}
other => panic!("expected the phone to publish what it resolved, got {other:?}"),
}
// The laptop pulls what the phone resolved and published.
match laptop.sync()? {
SyncResult::Pulled { .. } => {}
other => panic!("expected the laptop to pull, got {other:?}"),
}
for device in [&mut laptop, &mut phone] {
let rows = device.query("SELECT body FROM note ORDER BY body", ())?;
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][0], SqlValue::Text("buy milk".into()));
assert_eq!(rows[1][0], SqlValue::Text("call the dentist".into()));
}
laptop.close()?;
phone.close()?;
drop(remote);
std::fs::remove_dir_all(&scratch).ok();
println!("two_writers: ok");
Ok(())
}
fn scratch_dir() -> std::io::Result<std::path::PathBuf> {
let mut path = std::env::temp_dir();
path.push(format!("potluckdb-two-writers-{}", std::process::id()));
// Cleared first: a process id comes round again, and opening a
// database on top of one a crashed run left behind is not the thing
// this example is demonstrating.
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path)?;
Ok(path)
}