oxide_sql_core/migrations/
introspect.rs1use super::snapshot::SchemaSnapshot;
8
9pub trait Introspect {
14 type Error: std::error::Error;
16
17 fn introspect_schema(&self) -> Result<SchemaSnapshot, Self::Error>;
19}
20
21pub mod sqlite_helpers {
25 use crate::ast::DataType;
26 use crate::migrations::column_builder::DefaultValue;
27 use crate::migrations::snapshot::ColumnSnapshot;
28
29 pub const LIST_TABLES: &str = "SELECT name FROM sqlite_master WHERE type='table' \
32 AND name NOT LIKE 'sqlite_%' ORDER BY name";
33
34 pub const TABLE_INFO: &str = "PRAGMA table_info({table})";
37
38 pub const INDEX_LIST: &str = "PRAGMA index_list({table})";
41
42 pub const INDEX_INFO: &str = "PRAGMA index_info({index})";
45
46 pub const FOREIGN_KEY_LIST: &str = "PRAGMA foreign_key_list({table})";
49
50 #[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 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 #[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}