Skip to main content

oxide_sql_core/migrations/
introspect.rs

1//! Schema introspection trait.
2//!
3//! Driver crates (oxide-sql-sqlite, etc.) implement [`Introspect`]
4//! to read the current database schema at runtime. The core crate
5//! defines only the trait so it stays driver-agnostic.
6
7use super::snapshot::SchemaSnapshot;
8
9/// Introspects a live database connection to produce a
10/// [`SchemaSnapshot`] of the current schema.
11///
12/// Implementations live in driver crates (e.g. oxide-sql-sqlite).
13pub trait Introspect {
14    /// Error type for introspection failures.
15    type Error: std::error::Error;
16
17    /// Reads the current database schema and returns a snapshot.
18    fn introspect_schema(&self) -> Result<SchemaSnapshot, Self::Error>;
19}
20
21/// Helper constants and functions for implementing [`Introspect`]
22/// on SQLite connections. No driver dependency — just SQL strings
23/// and type-mapping logic that any SQLite driver crate can use.
24pub mod sqlite_helpers {
25    use crate::ast::DataType;
26    use crate::migrations::column_builder::DefaultValue;
27    use crate::migrations::snapshot::ColumnSnapshot;
28
29    /// SQL to list all user tables (excludes internal SQLite
30    /// tables).
31    pub const LIST_TABLES: &str = "SELECT name FROM sqlite_master WHERE type='table' \
32         AND name NOT LIKE 'sqlite_%' ORDER BY name";
33
34    /// PRAGMA to get column info for a table.
35    /// Replace `{table}` with the actual table name.
36    pub const TABLE_INFO: &str = "PRAGMA table_info({table})";
37
38    /// PRAGMA to get the index list for a table.
39    /// Replace `{table}` with the actual table name.
40    pub const INDEX_LIST: &str = "PRAGMA index_list({table})";
41
42    /// PRAGMA to get the columns of an index.
43    /// Replace `{index}` with the actual index name.
44    pub const INDEX_INFO: &str = "PRAGMA index_info({index})";
45
46    /// PRAGMA to get foreign key list for a table.
47    /// Replace `{table}` with the actual table name.
48    pub const FOREIGN_KEY_LIST: &str = "PRAGMA foreign_key_list({table})";
49
50    /// Maps a SQLite type affinity string to a [`DataType`].
51    ///
52    /// SQLite is flexible about type names; this function handles
53    /// the most common forms returned by `PRAGMA table_info`.
54    #[must_use]
55    pub fn parse_sqlite_type(type_str: &str) -> DataType {
56        let upper = type_str.to_uppercase();
57        let upper = upper.trim();
58        match upper.as_ref() {
59            "INTEGER" | "INT" => DataType::Integer,
60            "BIGINT" => DataType::Bigint,
61            "SMALLINT" | "TINYINT" => DataType::Smallint,
62            "REAL" | "FLOAT" => DataType::Real,
63            "DOUBLE" | "DOUBLE PRECISION" => DataType::Double,
64            "TEXT" | "CLOB" => DataType::Text,
65            "BLOB" => DataType::Blob,
66            "BOOLEAN" | "BOOL" => DataType::Integer,
67            "DATE" => DataType::Date,
68            "DATETIME" | "TIMESTAMP" => DataType::Datetime,
69            s if s.starts_with("VARCHAR") => {
70                let len = extract_length(s);
71                DataType::Varchar(len)
72            }
73            s if s.starts_with("CHAR") => {
74                let len = extract_length(s);
75                DataType::Char(len)
76            }
77            s if s.starts_with("NUMERIC") || s.starts_with("DECIMAL") => DataType::Real,
78            _ => DataType::Text,
79        }
80    }
81
82    /// Extracts the length from a type like "VARCHAR(255)".
83    fn extract_length(s: &str) -> Option<u32> {
84        s.find('(')
85            .and_then(|start| s.find(')').map(|end| (start, end)))
86            .and_then(|(start, end)| s[start + 1..end].trim().parse::<u32>().ok())
87    }
88
89    /// Builds a [`ColumnSnapshot`] from raw `PRAGMA table_info`
90    /// row data.
91    ///
92    /// # Arguments
93    ///
94    /// * `name` — Column name.
95    /// * `type_str` — Type string from SQLite (e.g. "INTEGER",
96    ///   "VARCHAR(255)").
97    /// * `notnull` — Whether NOT NULL is set.
98    /// * `default_value` — Default value expression, if any.
99    /// * `pk` — Whether this column is part of the primary key.
100    #[must_use]
101    pub fn column_from_pragma(
102        name: &str,
103        type_str: &str,
104        notnull: bool,
105        default_value: Option<&str>,
106        pk: bool,
107    ) -> ColumnSnapshot {
108        let data_type = parse_sqlite_type(type_str);
109        let default = default_value.map(|v| {
110            if v == "NULL" {
111                DefaultValue::Null
112            } else if v == "TRUE" || v == "FALSE" {
113                DefaultValue::Expression(v.to_string())
114            } else if let Ok(i) = v.parse::<i64>() {
115                DefaultValue::Integer(i)
116            } else if let Ok(f) = v.parse::<f64>() {
117                DefaultValue::Float(f)
118            } else if v.starts_with('\'') && v.ends_with('\'') {
119                DefaultValue::String(v[1..v.len() - 1].replace("''", "'"))
120            } else {
121                DefaultValue::Expression(v.to_string())
122            }
123        });
124        ColumnSnapshot {
125            name: name.to_string(),
126            data_type,
127            nullable: !notnull,
128            primary_key: pk,
129            unique: false,
130            autoincrement: false,
131            default,
132        }
133    }
134
135    #[cfg(test)]
136    mod tests {
137        use super::*;
138
139        #[test]
140        fn parse_common_types() {
141            assert_eq!(parse_sqlite_type("INTEGER"), DataType::Integer);
142            assert_eq!(parse_sqlite_type("BIGINT"), DataType::Bigint);
143            assert_eq!(parse_sqlite_type("TEXT"), DataType::Text);
144            assert_eq!(parse_sqlite_type("BLOB"), DataType::Blob);
145            assert_eq!(parse_sqlite_type("REAL"), DataType::Real);
146            assert_eq!(
147                parse_sqlite_type("VARCHAR(255)"),
148                DataType::Varchar(Some(255))
149            );
150            assert_eq!(parse_sqlite_type("CHAR(10)"), DataType::Char(Some(10)));
151        }
152
153        #[test]
154        fn column_from_pragma_basic() {
155            let col = column_from_pragma("id", "INTEGER", true, None, true);
156            assert_eq!(col.name, "id");
157            assert_eq!(col.data_type, DataType::Integer);
158            assert!(!col.nullable);
159            assert!(col.primary_key);
160            assert!(col.default.is_none());
161        }
162
163        #[test]
164        fn column_from_pragma_with_default() {
165            let col = column_from_pragma("active", "INTEGER", true, Some("TRUE"), false);
166            assert_eq!(col.default, Some(DefaultValue::Expression("TRUE".into())));
167
168            let col = column_from_pragma("count", "INTEGER", false, Some("42"), false);
169            assert_eq!(col.default, Some(DefaultValue::Integer(42)));
170        }
171    }
172}