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}