1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! Per-owner revision allocation for incremental sync.
//!
//! A syncable row carries a `revision` drawn from a counter that is private to
//! its owner. A client pulls by asking for everything above the revision it
//! last saw, so the counter must be monotonic per owner and must move in the
//! same transaction as the row write it stamps. [`next_revision`] does exactly
//! that and nothing else.
//!
//! # What this crate is not
//!
//! Baukit does not standardize a sync protocol. Wire payloads, conflict
//! resolution, batching, and the pull endpoint stay product-owned, as
//! `docs/platform/offline-readiness-contract.md` says. This crate owns one
//! mechanism: allocating the next revision safely.
//!
//! # Schema
//!
//! Products copy [`POSTGRES_MIGRATION_SQL`] into their own ordered migrations;
//! the crate never runs migrations at process startup. Products whose existing
//! counter uses `user_id` can instead copy
//! [`POSTGRES_RENAME_USER_ID_TO_OWNER_ID_SQL`]. The reference migration also
//! documents the column convention every syncable table follows: `id`,
//! `owner_id`, `updated_at`, `deleted_at`, `revision`, plus an `(owner_id,
//! revision)` index.
//!
//! # Usage
//!
//! Call [`next_revision`] inside the transaction that writes the row, and stamp
//! the returned value onto that row. If the transaction rolls back, the
//! allocation rolls back with it and the revision is never handed out.
//!
//! ```no_run
//! # async fn example(pool: &sqlx::PgPool, owner_id: uuid::Uuid) -> Result<(), sqlx::Error> {
//! let mut transaction = pool.begin().await?;
//! let revision = baukit_sync::next_revision(&mut transaction, owner_id).await?;
//! sqlx::query("UPDATE product_records SET revision = $1 WHERE owner_id = $2")
//! .bind(revision)
//! .bind(owner_id)
//! .execute(&mut *transaction)
//! .await?;
//! transaction.commit().await?;
//! # Ok(())
//! # }
//! ```
use ;
use Uuid;
/// Reference PostgreSQL schema and column convention for product migrations.
///
/// Copy this SQL into a product migration; do not execute it dynamically during
/// application startup.
pub const POSTGRES_MIGRATION_SQL: &str = include_str!;
/// One-shot PostgreSQL migration from `sync_revisions.user_id` to `owner_id`.
///
/// Copy this SQL into a product migration only when its existing table has the
/// old `user_id` shape and the foreign key has the conventional
/// `sync_revisions_user_id_fkey` name. PostgreSQL keeps the foreign key and its
/// delete action when the column is renamed. The SQL renames that constraint
/// for clarity and adds the canonical non-negative revision check.
///
/// This migration is intentionally one-shot. It fails if `owner_id` already
/// exists, which prevents a partially applicable migration from being hidden.
pub const POSTGRES_RENAME_USER_ID_TO_OWNER_ID_SQL: &str =
include_str!;
/// Allocates the owner's next revision inside the caller's transaction.
///
/// The `UPDATE ... RETURNING` takes a row lock for the duration of the
/// transaction, so concurrent writers for one owner serialize and each sees a
/// distinct, increasing value. Different owners touch different rows and do not
/// block each other. A rollback discards the allocation.
///
/// The owner's counter row must exist; call [`ensure_owner`] once when the
/// owner is created.
///
/// # Errors
///
/// Returns [`sqlx::Error::RowNotFound`] when the owner has no counter row, and
/// any other database error unchanged.
pub async
/// Creates the owner's revision counter if it does not exist yet.
///
/// Call this when the owner is created, in the same transaction. Calling it
/// again is harmless and never resets an existing counter.
///
/// # Errors
///
/// Returns any database error unchanged.
pub async
/// Reads the owner's current revision without allocating a new one.
///
/// Use this to answer "what is the newest revision a pull could return". It
/// takes no lock, so a concurrent writer may advance the counter immediately
/// after the read.
///
/// # Errors
///
/// Returns any database error unchanged. An owner without a counter row reads
/// as `None`.
pub async
/// Reads and locks the owner's current revision without allocating a new one.
///
/// The row lock lasts until the caller's transaction commits or rolls back.
/// Use this before a read-dependent write that must not race another revision
/// allocation. [`current_revision`] remains the non-locking read for pull
/// boundaries and status queries.
///
/// # Errors
///
/// Returns any database error unchanged. An owner without a counter row reads
/// as `None` and no row is locked.
pub async