Quickstart for Rust
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.
| Term | Meaning in this program |
|---|---|
| Local store | The database files on one device. |
| Remote | The shared copy of a database that devices synchronize with. |
| Revision | One published database state that does not change. |
| Head | The remote’s newest published revision. |
| Push | Send local writes to the remote. |
| Pull | Receive writes from the remote. |
| Pending | Local writes that the device has not confirmed as published. |
| Replay | Run local statements again on the remote revision. |
| Quarantine | Preserve 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.
- Complete the installation.
- Save this program as
src/main.rsin a crate that depends onpotluckdb. - Run
cargo run.
The program prints two lines:
| Output | Meaning |
|---|---|
Use query for this statement. | The program passes a read statement to execute on purpose. The call fails with read_in_execute, and the program prints the error’s action. |
quickstart: ok | The 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.
use std::sync::{mpsc, Arc};use std::time::Duration;
use potluckdb::{OpenOptions, PotluckDBError, Remote, SyncPolicy, SyncResult};
/// Every sync attempt reports an outcome, UpToDate included; wait for the requested one.fn wait_for( events: &mpsc::Receiver<SyncResult>, wanted: fn(&SyncResult) -> bool,) -> Result<SyncResult, PotluckDBError> { loop { let outcome = events .recv_timeout(Duration::from_secs(10)) .expect("a sync outcome within ten seconds"); if let SyncResult::Failed { error } = outcome { return Err(error); } if wanted(&outcome) { return Ok(outcome); } }}
/// 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.fn wait_until_published( device: &potluckdb::Database, events: &mpsc::Receiver<SyncResult>,) -> Result<(), PotluckDBError> { while device.status()?.pending_writes > 0 { wait_for(events, |outcome| matches!(outcome, SyncResult::Pushed { .. }))?; } Ok(())}
fn main() -> Result<(), PotluckDBError> { let scratch = std::env::temp_dir().join(format!("potluckdb-{}", std::process::id())); std::fs::create_dir_all(&scratch).expect("a scratch directory"); let mut remote = Remote::from_directory(scratch.join("remote"))?; // a bucket replaces this in production; see the installation page
// The laptop synchronizes on its own: automatic is the default policy. // on_sync delivers every outcome to the program; // this program sends them down a channel. let (laptop_events, events) = mpsc::channel(); let laptop_options = OpenOptions { on_sync: Some(Arc::new(move |outcome| { let _ = laptop_events.send(outcome); })), ..OpenOptions::default() }; let mut laptop = potluckdb::open_with_options(scratch.join("laptop"), Some(&remote), laptop_options)?; laptop.transaction(|setup| { // one transaction: both statements travel in one push setup.execute("CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT NOT NULL)", ())?; setup.execute("INSERT INTO note(body) VALUES(?)", ("buy milk",))?; Ok(()) })?; wait_until_published(&laptop, &events)?;
// 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. let phone_options = OpenOptions { sync_policy: SyncPolicy::manual(), ..OpenOptions::default() }; let mut phone = potluckdb::open_with_options(scratch.join("phone"), Some(&remote), phone_options)?; let rows = phone.query("SELECT id, body FROM note ORDER BY id", ())?; assert_eq!(rows.columns(), ["id", "body"]); assert_eq!(rows[0].get::<String>("body")?, "buy milk"); assert_eq!(rows[0].get::<i64>(0)?, 1);
// Both devices modify the same row before either has seen the other's write. laptop.execute("UPDATE note SET body = ? WHERE id = ?", ("buy oat milk", 1))?; phone.execute("UPDATE note SET body = ? WHERE id = ?", ("buy milk and eggs", 1))?; wait_until_published(&laptop, &events)?;
// The phone's push finds a remote head it has not seen: // a Conflict, returned rather than an Err. assert!(matches!(phone.sync()?, SyncResult::Conflict { .. })); let resolution = phone.resolve_conflict()?; // adopt the laptop's revision, replay the phone's statement on it assert!(resolution.quarantined.is_empty()); assert!(matches!(phone.sync()?, SyncResult::Pushed { .. }));
// The laptop pulls the resolved revision on its own. Both devices hold the same row. wait_for(&events, |outcome| matches!(outcome, SyncResult::Pulled { .. }))?; for db in [&mut laptop, &mut phone] { let rows = db.query("SELECT body FROM note WHERE id = ?", (1,))?; assert_eq!(rows[0].get::<String>("body")?, "buy milk and eggs"); }
// query reads, execute writes. The wrong call is refused before execution. let refused = laptop.execute("SELECT count(*) FROM note", ()).unwrap_err(); assert_eq!(refused.code, "read_in_execute"); println!("{}", refused.action); // Use query for this statement.
laptop.close()?; phone.close()?; remote.close()?; std::fs::remove_dir_all(&scratch).expect("the scratch directory is removed"); println!("quickstart: ok"); Ok(())}What the program demonstrates
Section titled “What the program demonstrates”The remote uses a directory. No server runs. Remote::from_directory 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 sync(). Its on_sync hook, a function that receives synchronization results, reports every attempt. A failed result carries the error.
The hook runs on the synchronization thread. Transfer the outcome to your own thread, as shown with a channel.
| Result | Meaning |
|---|---|
SyncResult::UpToDate | The attempt found no pending writes and no remote revision to pull. |
SyncResult::Pushed | The attempt published its pending writes as a remote revision. |
SyncResult::Pulled | The attempt adopted a remote revision. It can also report replayed and quarantined transactions. |
SyncResult::Conflict | The device’s push met a remote revision that the device had not seen. |
SyncResult::Failed | An automatic attempt failed and carries the error. |
An automatic result covers one attempt. SyncResult::Pushed confirms the writes that the attempt published. Writes made after the attempt began can still be pending. If an earlier attempt failed, an explicit sync() first publishes that attempt’s writes and leaves later writes pending. A program that must know its own writes are published reads status().pending_writes. wait_until_published 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. sync() returns SyncResult::Conflict when the remote has an unseen revision. This result is not an Err. resolve_conflict() 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 reads and execute writes. Each accepts exactly one statement of its own kind. Each refuses the other kind before execution, with PotluckDBError whose action names the required call.
These calls promise different things. query returns rows and uses the connection’s maximum_rows to bound the result. A different SQL engine can answer a read. execute 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 transaction for transaction control, as the setup shows.
Reference
Section titled “Reference”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.