Skip to main content

baukit_sync/
lib.rs

1//! Revision allocation, tombstone purge horizons, and hybrid logical clocks
2//! for incremental sync.
3//!
4//! A syncable row carries a `revision` drawn from a counter that is private to
5//! its owner. A client pulls by asking for everything above the revision it
6//! last saw, so the counter must be monotonic per owner and must move in the
7//! same transaction as the row write it stamps. [`next_revision`] does exactly
8//! that. The [`purge`] module deletes old tombstones and records the per-owner
9//! purge horizon, and [`horizon`] holds the pull-cursor rule that turns a
10//! cursor below the horizon into a full-resync signal. The [`hlc`] module
11//! supplies a cross-runtime logical clock for ordering writes when physical
12//! clocks stall or move backward.
13//!
14//! # Features
15//!
16//! Without features the crate contains only [`hlc`], [`horizon`], and the
17//! reference SQL constants, and depends on no database driver. The
18//! `sqlx-postgres` feature adds the PostgreSQL revision allocator and the
19//! [`purge`] module.
20//!
21//! # What this crate is not
22//!
23//! Baukit does not standardize a sync protocol. Wire payloads, conflict
24//! resolution, batching, and the pull endpoint stay product-owned, as
25//! `docs/platform/offline-readiness-contract.md` says. This crate owns
26//! revision allocation, tombstone purge horizons, and timestamp generation.
27//! Merge rules, retention periods, table lists, and scheduling stay
28//! product-owned.
29//!
30//! # Schema
31//!
32//! Products copy [`POSTGRES_MIGRATION_SQL`] and
33//! [`POSTGRES_PURGE_HORIZONS_MIGRATION_SQL`] into their own ordered
34//! migrations; the crate never runs migrations at process startup. Products
35//! whose existing counter uses `user_id` can instead copy
36//! [`POSTGRES_RENAME_USER_ID_TO_OWNER_ID_SQL`]. The reference migration also
37//! documents the column convention every syncable table follows: `id`,
38//! `owner_id`, `updated_at`, `deleted_at`, `revision`, plus an `(owner_id,
39//! revision)` index.
40//!
41//! # Usage
42//!
43//! Call [`next_revision`] inside the transaction that writes the row, and stamp
44//! the returned value onto that row. If the transaction rolls back, the
45//! allocation rolls back with it and the revision is never handed out.
46//!
47//! ```no_run
48//! # #[cfg(feature = "sqlx-postgres")]
49//! # async fn example(pool: &sqlx::PgPool, owner_id: uuid::Uuid) -> Result<(), sqlx::Error> {
50//! let mut transaction = pool.begin().await?;
51//! let revision = baukit_sync::next_revision(&mut transaction, owner_id).await?;
52//! sqlx::query("UPDATE product_records SET revision = $1 WHERE owner_id = $2")
53//!     .bind(revision)
54//!     .bind(owner_id)
55//!     .execute(&mut *transaction)
56//!     .await?;
57//! transaction.commit().await?;
58//! # Ok(())
59//! # }
60//! ```
61
62#![deny(missing_docs)]
63
64pub mod hlc;
65pub mod horizon;
66#[cfg(feature = "sqlx-postgres")]
67pub mod purge;
68#[cfg(feature = "sqlx-postgres")]
69mod revision;
70
71#[cfg(feature = "sqlx-postgres")]
72pub use revision::{current_revision, current_revision_for_update, ensure_owner, next_revision};
73
74/// Reference PostgreSQL schema and column convention for product migrations.
75///
76/// Copy this SQL into a product migration; do not execute it dynamically during
77/// application startup.
78pub const POSTGRES_MIGRATION_SQL: &str = include_str!("../migrations/0001_baukit_sync.sql");
79
80/// Reference PostgreSQL schema for per-owner tombstone purge horizons.
81///
82/// Copy this SQL into a product migration after [`POSTGRES_MIGRATION_SQL`].
83/// The [`purge`] module reads and writes the `sync_purge_horizons` table it
84/// creates. The table name is fixed, like `sync_revisions`.
85pub const POSTGRES_PURGE_HORIZONS_MIGRATION_SQL: &str =
86    include_str!("../migrations/0002_baukit_sync_purge_horizons.sql");
87
88/// One-shot PostgreSQL migration from `sync_revisions.user_id` to `owner_id`.
89///
90/// Copy this SQL into a product migration only when its existing table has the
91/// old `user_id` shape and the foreign key has the conventional
92/// `sync_revisions_user_id_fkey` name. PostgreSQL keeps the foreign key and its
93/// delete action when the column is renamed. The SQL renames that constraint
94/// for clarity and adds the canonical non-negative revision check.
95///
96/// This migration is intentionally one-shot. It fails if `owner_id` already
97/// exists, which prevents a partially applicable migration from being hidden.
98pub const POSTGRES_RENAME_USER_ID_TO_OWNER_ID_SQL: &str =
99    include_str!("../postgres_rename_user_id_to_owner_id.sql");
100
101// Compiles the README's examples so they cannot drift from the API.
102#[doc = include_str!("../README.md")]
103#[cfg(all(doctest, feature = "sqlx-postgres"))]
104struct ReadmeDoctests;