Skip to main content

magic_sync/
lib.rs

1//! Keeps partial copies of PostgreSQL tables on SQLite replicas, and brings
2//! back what the replicas write — offline-first, by comparing what each
3//! replica holds with what it should hold.
4//!
5//! A table is a struct that derives [`Row`] and says how it travels, and a
6//! [`Codec`](wire::Codec) says how its rows turn into bytes. On the server, a
7//! `Server` (feature `postgres`) notes every change in the writing
8//! transaction, plans per replica what it should hold against what it is
9//! known to hold, and sends the difference through your transport. On a
10//! replica, a `Replica` (feature `sqlite`) applies what arrives in one
11//! transaction, and sends up what the replica wrote.
12//!
13//! **[The book](https://rsansores.github.io/magic_sync/)** builds a small
14//! system step by step and explains the engine from the ground up;
15//! [the replica protocol](https://rsansores.github.io/magic_sync/protocol.html)
16//! is what a replica in any language must do.
17//!
18//! A build of this crate is one side: enable `postgres` for the server or
19//! `sqlite` for a replica, each in a workspace of its own
20//! ([Set up a project](https://rsansores.github.io/magic_sync/guide/setup.html)).
21//! These docs show the side they were built for; the book publishes both,
22//! [the server's](https://rsansores.github.io/magic_sync/api/server/magic_sync/)
23//! and [a replica's](https://rsansores.github.io/magic_sync/api/replica/magic_sync/).
24//!
25//! # Public dependencies
26//!
27//! What a host passes to the engine or reads back is another crate's type,
28//! and a new major version of any of them is a new major version of this
29//! crate:
30//!
31//! - [`sqlx`] 0.8: the pools, connections and transactions a host hands the
32//!   engine, and the database error ([`Error::Database`], [`sqlx::Error`]).
33//! - [`prost`] 0.14: a table that travels as protobuf is a message of it.
34//! - [`uuid`] 1: replicas, partitions and keys are [`uuid::Uuid`]s.
35//! - [`chrono`] 0.4: the times a status carries are its `DateTime<Utc>`.
36//! - [`serde`] 1: a table that travels as JSON ([`wire::Codec::json`]) is a
37//!   row that is `Serialize` and `Deserialize`, and the dashboard's
38//!   documents are serialized with it.
39
40#![warn(missing_docs, clippy::missing_errors_doc)]
41
42#[cfg(not(any(feature = "postgres", feature = "sqlite")))]
43compile_error!(
44    "magic_sync is built for one side: enable `postgres` (the server's) or `sqlite` (a replica's)"
45);
46
47#[cfg(all(feature = "postgres", feature = "sqlite"))]
48compile_error!(
49    "magic_sync is built for one side, and this build enables both `postgres` and `sqlite`: \
50     a crate that needs the server's side and one that needs a replica's were built together \
51     (one workspace, or one test crate) — build each side in a workspace of its own"
52);
53
54#[cfg(test)]
55mod conformance;
56mod dialect;
57pub mod envelope;
58mod error;
59#[cfg(feature = "postgres")]
60mod filing;
61#[cfg(test)]
62mod model;
63#[cfg(feature = "postgres")]
64pub mod plan;
65#[cfg(feature = "postgres")]
66mod relay;
67#[cfg(feature = "postgres")]
68mod replica;
69#[cfg(feature = "sqlite")]
70pub mod replica;
71pub mod row;
72pub mod run;
73#[cfg(feature = "postgres")]
74pub mod status;
75#[cfg(test)]
76mod testkit;
77pub mod wire;
78mod wish;
79
80pub use error::Error;
81pub use row::{Columns, Row};
82
83/// One struct is one table, column for column, and says how it travels:
84/// implements [`Row`](trait@Row). The same struct serves both sides.
85///
86/// ```
87/// use chrono::{DateTime, Utc};
88/// use uuid::Uuid;
89///
90/// /// Written at the head office, held by every register; a register keeps
91/// /// one a receipt of its own still points at, retired.
92/// #[derive(sqlx::FromRow, Clone, magic_sync::Row)]
93/// #[row(table = "products", schema = "shop", mirror = "catalog", sync = "broadcast", retired = "retired_at")]
94/// pub struct Product {
95///     pub id: Uuid,
96///     pub name: String,
97///     pub price_cents: i64,
98///     pub updated_at: DateTime<Utc>,
99/// }
100///
101/// /// Written by a register, filed at the head office under the register
102/// /// that sent it.
103/// #[derive(sqlx::FromRow, Clone, magic_sync::Row)]
104/// #[row(table = "receipts", schema = "shop", mirror = "record", key = "register_id, id", origin = "register_id")]
105/// pub struct Receipt {
106///     pub register_id: Uuid,
107///     pub id: Uuid,
108///     pub product_id: Uuid,
109///     pub total_cents: i64,
110///     #[row(created)]
111///     pub created_at: DateTime<Utc>,
112/// }
113/// ```
114///
115/// On the struct, `#[row(…)]` takes:
116///
117/// | key | means |
118/// |---|---|
119/// | `table` | the table's name (required) |
120/// | `schema` | its PostgreSQL schema; `public` when left out |
121/// | `mirror` | where it lives: `catalog` (written on the server, held by replicas), `record` (written on a replica, held by the server), `both` (each side its own columns) or `setting` (a value a replica holds and acts on, held by the server, which may wish its `desired` columns other values) — required |
122/// | `sync` | which replicas hold which rows, for `catalog` and `both`: `broadcast` (every replica of the row's partition), `assigned(col)` (the replica `col` names: a uuid, or an `Option` of one), `follows(col)` (wherever the row `col` points at goes: one column, the whole of a foreign key into a mirrored table) or `grouped(members = t, assigned = t)` (every replica a group holding the row is given to). Left out, a row travels only with a row that points at it |
123/// | `key` | the primary key's columns, in any order, when it is not `id`: `key = "list_id, customer_id"`. Each is a uuid or an integer ([`row::KeyValue`]), and a field of the row itself — not of a part flattened in |
124/// | `active` | for `catalog` and `both`, the relation the rows the table's rule sends are read through on the server — `view` or `schema.view` (a name has no dot), a view that leaves out the rows no replica is sent for their own sake; one it leaves out still travels while a row a replica holds points at it |
125/// | `retired` | for `catalog` and `both`, the column a replica sets on a row it lets go of but must keep, because a row of its own points at it. It is left out of the row as a rule; a row that has it as a field too gets it back as the server sends it, which is live |
126/// | `origin` | for `record`, `both` and `setting` — required for a `setting`, where it is the replica a wish of the row goes to — the uuid key column naming the replica a row belongs to: a replica deletes only its own rows |
127/// | `apply` | for a `setting`, which wish its replica carries out ([`row::Apply`]): `always` (the default) or `if_unchanged`, only while it still holds the version the wish is based on |
128/// | `unmirrored` | columns one side's table has that the row leaves out: `unmirrored = "archived_at, notes"` |
129/// | `crate` | the path to this crate, when a host reaches it through another: `crate = "platform::sync"` |
130///
131/// On a field:
132///
133/// | flag | means |
134/// |---|---|
135/// | `#[row(version)]` | the column whose value only moves forward: an older report of a replica's record never overwrites a newer one on the server. An integer or a `chrono` instant ([`row::Version`]), never NULL. Only a `record` or a `setting` has one — a setting must, a field of its own: what a wish is based on. The server files a setting as a record, the newer version winning, but one its replica re-states as a snapshot begins whatever its version: a replica restored from an older copy holds what it holds. A replica takes what the server sends whatever its version |
136/// | `#[row(desired)]` | in a `setting`, a column the server may wish other values ([`row::Setting`]); at least one, a field of the row itself, never its key or version |
137/// | `#[row(created)]` | written once, when the row is created; never by an update |
138/// | `#[row(replica_owned)]` | in a `both` table, a column only replicas write: it goes up, the rest come down |
139/// | `#[row(generated)]` | computed by the database: read, never written |
140///
141/// Fields follow `sqlx::FromRow`: `#[sqlx(rename = "…")]` names the column,
142/// `#[sqlx(try_from = "…")]` is the type the column decodes into,
143/// `#[sqlx(skip)]` is no column (and takes no `rename`), and `#[sqlx(flatten)]` brings in the columns
144/// of a part that derives [`Columns`](macro@Columns). `#[sqlx(json)]` and a
145/// struct-level `#[sqlx(rename_all)]` are refused rather than
146/// half-understood. A nullable column is a field written as `Option<…>`, spelled
147/// so (`Option<T>` or `std::option::Option<T>`): the
148/// engine refuses the table of a field typed through an alias.
149///
150/// Every name is the database's own, as written — the engine quotes each —
151/// and at most the 63 bytes PostgreSQL keeps of one. Whatever the derive
152/// cannot honour is a compile error pointing at what says it; whatever the
153/// database disagrees with — a column on one side only, a nullability, a
154/// type its field does not decode, another primary key — is refused when the
155/// server or a replica starts.
156pub use magic_sync_derive::Row;
157
158/// A part of a row, flattened into it with `#[sqlx(flatten)]`: its columns
159/// are the row's. Its fields take the same flags as a row's; on the struct,
160/// only `#[row(crate = "…")]`.
161///
162/// ```
163/// use chrono::{DateTime, Utc};
164///
165/// /// When a row was created, for every table that keeps it.
166/// #[derive(sqlx::FromRow, Clone, magic_sync::Columns)]
167/// pub struct Stamps {
168///     #[row(created)]
169///     pub created_at: DateTime<Utc>,
170/// }
171///
172/// #[derive(sqlx::FromRow, Clone, magic_sync::Row)]
173/// #[row(table = "notes", mirror = "record")]
174/// pub struct Note {
175///     pub id: uuid::Uuid,
176///     pub text: String,
177///     #[sqlx(flatten)]
178///     pub stamps: Stamps,
179/// }
180/// ```
181pub use magic_sync_derive::Columns;
182
183// The derive's generated code names this crate, and sqlx through it.
184extern crate self as magic_sync;
185
186/// A connection of this side's database: `&mut *tx` inside a transaction.
187#[cfg(feature = "postgres")]
188pub type Conn = sqlx::PgConnection;
189/// A connection of this side's database: `&mut *tx` inside a transaction.
190#[cfg(feature = "sqlite")]
191pub type Conn = sqlx::SqliteConnection;
192
193/// The version of the engine's tables on the server's side: the server
194/// refuses to run against a database that installed another. It counts the
195/// server's own steps, independent of the replicas' version.
196#[cfg(feature = "postgres")]
197pub const SERVER_SCHEMA_VERSION: i32 = 2;
198
199/// The version of the engine's tables on a replica: the replica refuses to
200/// run against a database that installed another. It counts the replica's own
201/// steps (`replica::migration`), independent of the server's version.
202#[cfg(feature = "sqlite")]
203pub const REPLICA_SCHEMA_VERSION: i32 = 2;
204
205use __private::Db;
206
207#[cfg(all(test, feature = "postgres"))]
208type DbPool = sqlx::PgPool;
209
210/// What the derive's generated code names, and nothing else may: none of it
211/// is part of the crate's API.
212#[doc(hidden)]
213pub mod __private {
214    pub use crate::row::{Column, Columns, Route, Row};
215    pub use sqlx;
216
217    /// Keeps [`Columns`] and [`Row`] to what the derives write.
218    pub trait Sealed {}
219
220    /// This side's database.
221    #[cfg(feature = "postgres")]
222    pub type Db = sqlx::Postgres;
223    /// This side's database.
224    #[cfg(feature = "sqlite")]
225    pub type Db = sqlx::Sqlite;
226
227    /// Whether `T` is an `i64`, or an `Option` of one: whatever it is spelled
228    /// as, an alias included.
229    pub fn is_int64<T: 'static>() -> bool {
230        use std::any::TypeId;
231        [TypeId::of::<i64>(), TypeId::of::<Option<i64>>()].contains(&TypeId::of::<T>())
232    }
233
234    /// How many of `found` are `Some`.
235    pub const fn count(found: &[Option<&'static str>]) -> usize {
236        let mut n = 0;
237        let mut i = 0;
238        while i < found.len() {
239            if found[i].is_some() {
240                n += 1;
241            }
242            i += 1;
243        }
244        n
245    }
246
247    /// The first of `found` that is `Some`.
248    pub const fn first(found: &[Option<&'static str>]) -> Option<&'static str> {
249        let mut i = 0;
250        while i < found.len() {
251            if found[i].is_some() {
252                return found[i];
253            }
254            i += 1;
255        }
256        None
257    }
258}