Skip to main content

ubiquisync_sql/db/
schema.rs

1use crate::dialect::SqlDialect;
2
3/// An existing table's shape as reported by backend introspection
4/// ([`Db::describe_table`](super::Db::describe_table)). Used by schema
5/// reconciliation to compare the live table against the declared schema.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct DbTableDescriptor {
8    /// The table's name.
9    pub name: String,
10    /// Primary-key columns, in declared key-position order.
11    pub pk_cols: Vec<DbColumnDescription>,
12    /// The remaining (non-primary-key) columns.
13    pub cols: Vec<DbColumnDescription>,
14}
15
16/// One column of an introspected table (see [`DbTableDescriptor`]).
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct DbColumnDescription {
19    /// The column's name.
20    pub name: String,
21    /// The column's storage class, mapped from the backend's native type
22    /// (or [`DbType::Other`] if it falls outside the engine's vocabulary).
23    pub db_type: DbType,
24    /// Whether the column permits SQL NULL.
25    pub nullable: bool,
26}
27
28/// A generic SQL storage class, independent of any data protocol.
29///
30/// This is the vocabulary the dialect names: a data domain (e.g. the tables
31/// protocol) maps its own column types down to a `DbType`, and the dialect
32/// turns that into a concrete backend type name via [`sql_type`](Self::sql_type). The
33/// `Uuid` variant is kept distinct from `Blob` so a backend may later map it
34/// to a native UUID type rather than raw bytes.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum DbType {
37    /// 64-bit signed integer (`INTEGER` / `BIGINT`).
38    Integer,
39    /// UTF-8 text (`TEXT`).
40    Text,
41    /// Raw byte string (`BLOB` / `BYTEA`).
42    Blob,
43    /// 16-byte UUID — stored as raw bytes today, but kept distinct from
44    /// [`Blob`](Self::Blob) so a backend may later use a native UUID type.
45    Uuid,
46    /// A column type the engine does not model (e.g. SQLite `REAL`/`NUMERIC`, a
47    /// Postgres enum). Produced only by backend introspection
48    /// ([`Db::describe_table`](super::Db::describe_table)) when a real table has
49    /// a column outside the engine's vocabulary; the engine never *emits* it as
50    /// DDL. Schema reconciliation treats it as a mismatch rather than silently
51    /// coercing it to a class it isn't.
52    Other,
53}
54
55impl DbType {
56    /// The concrete SQL column type name for this storage class under
57    /// `dialect`. SQLite uses type affinity (`INTEGER`/`TEXT`/`BLOB`) and has no
58    /// native UUID type, so a `Uuid` is stored as a raw `BLOB`. Postgres needs
59    /// `BIGINT` (its `INTEGER` is 32-bit and would overflow an i64) and `BYTEA`
60    /// for raw bytes, and maps `Uuid` to its native `UUID` type.
61    pub fn sql_type(self, dialect: SqlDialect) -> &'static str {
62        match (dialect, self) {
63            (SqlDialect::Sqlite, DbType::Integer) => "INTEGER",
64            (SqlDialect::Sqlite, DbType::Text) => "TEXT",
65            (SqlDialect::Sqlite, DbType::Blob | DbType::Uuid) => "BLOB",
66            (SqlDialect::Postgres, DbType::Integer) => "BIGINT",
67            (SqlDialect::Postgres, DbType::Text) => "TEXT",
68            (SqlDialect::Postgres, DbType::Blob) => "BYTEA",
69            (SqlDialect::Postgres, DbType::Uuid) => "UUID",
70            // `Other` is introspection-only — it names a column type the engine
71            // doesn't model, so it has no DDL spelling and is never emitted.
72            (_, DbType::Other) => {
73                unreachable!("DbType::Other is introspection-only and never rendered as DDL")
74            }
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn sql_type_maps_to_sqlite_names() {
85        assert_eq!(DbType::Integer.sql_type(SqlDialect::Sqlite), "INTEGER");
86        assert_eq!(DbType::Text.sql_type(SqlDialect::Sqlite), "TEXT");
87        assert_eq!(DbType::Blob.sql_type(SqlDialect::Sqlite), "BLOB");
88        assert_eq!(DbType::Uuid.sql_type(SqlDialect::Sqlite), "BLOB");
89    }
90
91    #[test]
92    fn sql_type_maps_to_postgres_names() {
93        // i64 needs BIGINT (Postgres INTEGER is 32-bit); raw bytes are BYTEA;
94        // UUIDs use the native UUID type.
95        assert_eq!(DbType::Integer.sql_type(SqlDialect::Postgres), "BIGINT");
96        assert_eq!(DbType::Text.sql_type(SqlDialect::Postgres), "TEXT");
97        assert_eq!(DbType::Blob.sql_type(SqlDialect::Postgres), "BYTEA");
98        assert_eq!(DbType::Uuid.sql_type(SqlDialect::Postgres), "UUID");
99    }
100}