Rindle docs and package mapSkip to main content

rindle_replica/
schema.rs

1//! Table schema discovery via SQLite pragmas → the engine's `ColumnDef`/PK shape,
2//! plus ensuring the PK has the UNIQUE index `TableSource` requires.
3
4use rindle::value::ColId;
5use rindle_sqlite::ColumnDef;
6use rusqlite::Connection;
7
8use crate::schema_envelope::introspect_replicated_table;
9use crate::table_shape::ReplicatedTableSchema;
10
11// The schema-**envelope** half of this module (introspection, validation, DDL application and
12// its audit trail) moved to `rindle_writeplane::schema_envelope`: only the `TableSchema` /
13// `discover` pair below needs `rindle_sqlite::ColumnDef`, and the write-master validates and
14// applies schema without an engine. Re-exported so in-crate `schema::…` paths are unchanged.
15pub use crate::schema_envelope::{
16    ensure_embedded_ddl_batch, transactionally_apply_schema, DdlApplyReport, DdlStep,
17};
18use crate::ReplicaError;
19
20/// The discovered shape of a registered table: columns in `cid` (== [`ColId`]) order
21/// plus the primary-key column ids. Feeds `TableSource::try_new`. `Clone` so the
22/// coordinator can fan one discovered schema out to every worker (it is `Send`:
23/// `ColumnDef` is `Box<str>` + scalars). Public via the crate's embedded-engine seam
24/// (design 308): a host that drives [`crate::Engine`] directly discovers with
25/// [`discover`] and registers the result.
26#[derive(Clone)]
27pub struct TableSchema {
28    pub columns: Vec<ColumnDef>,
29    pub primary_key: Vec<ColId>,
30    /// `true` when the primary key is a single `INTEGER PRIMARY KEY` column — a rowid alias.
31    /// Such a PK is already unique and point-lookupable via the rowid, so it needs **no**
32    /// separate UNIQUE index ([`ensure_unique_pk_index`] skips it, and `TableSource` recognizes
33    /// it). Detected from the declared column type during [`discover`].
34    pub pk_is_rowid_alias: bool,
35    /// Whether SQLite stores the table `WITHOUT ROWID`. Such a table's `INTEGER`
36    /// primary key is an ordinary primary-key column, never a rowid alias.
37    pub without_rowid: bool,
38}
39
40/// Discover a table's columns + primary key via `pragma_table_info`.
41pub fn discover(conn: &Connection, table: &str) -> Result<TableSchema, ReplicaError> {
42    Ok(lift_replicated(introspect_replicated_table(conn, table)?))
43}
44
45/// Lift an engine-free [`ReplicatedTableSchema`] (the apply plane's / write master's
46/// introspection shape) into the engine's `ColumnDef` shape. Pure — the one place the
47/// `rindle_sqlite` lift happens, so the derivation hosts build their sources from the
48/// SAME introspection the capture half ran (design 309 §3).
49pub(crate) fn lift_replicated(table: ReplicatedTableSchema) -> TableSchema {
50    let columns = table
51        .columns
52        .into_iter()
53        .map(|column| ColumnDef {
54            name: column.name.into_boxed_str(),
55            ty: column.value_type,
56            optional: column.nullable,
57        })
58        .collect();
59    TableSchema {
60        columns,
61        primary_key: table.primary_key,
62        pk_is_rowid_alias: table.pk_is_rowid_alias,
63        without_rowid: table.without_rowid,
64    }
65}
66
67/// Ensure the table has a UNIQUE index covering exactly the PK columns, which `TableSource`
68/// requires for row-identity point lookups. Idempotent.
69///
70/// A rowid-alias `INTEGER PRIMARY KEY` is **skipped**: the rowid already enforces uniqueness and
71/// SQLite point-looks-it-up natively (`SEARCH … USING INTEGER PRIMARY KEY (rowid=?)`), so a
72/// synthetic index would be pure write amplification with no plan benefit — and `TableSource`
73/// recognizes the rowid alias as a unique key without one. (Databases created by older versions
74/// may still carry a now-unused `rindle_replica_pk_*` index; it is harmless and left in place.)
75///
76/// One implementation, shared with the apply plane
77/// ([`crate::apply::ensure_unique_pk_index_for`], design 309) — this is the
78/// `ColumnDef`-shaped adapter for hosts on the embedded-engine seam.
79pub fn ensure_unique_pk_index(
80    conn: &Connection,
81    table: &str,
82    ts: &TableSchema,
83) -> Result<(), ReplicaError> {
84    crate::apply::ensure_unique_pk_index_for(
85        conn,
86        table,
87        &ts.columns
88            .iter()
89            .map(|column| &*column.name)
90            .collect::<Vec<_>>(),
91        &ts.primary_key,
92        ts.pk_is_rowid_alias,
93        ts.without_rowid,
94    )
95}