oxide_sql_core/migrations/
snapshot.rs1use std::collections::BTreeMap;
9
10use crate::ast::DataType;
11use crate::schema::{RustTypeMapping, TableSchema};
12
13use super::column_builder::DefaultValue;
14use super::operation::strip_option;
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct ColumnSnapshot {
19 pub name: String,
21 pub data_type: DataType,
23 pub nullable: bool,
25 pub primary_key: bool,
27 pub unique: bool,
29 pub autoincrement: bool,
31 pub default: Option<DefaultValue>,
33}
34
35#[derive(Debug, Clone, PartialEq)]
37pub struct TableSnapshot {
38 pub name: String,
40 pub columns: Vec<ColumnSnapshot>,
42}
43
44impl TableSnapshot {
45 pub fn from_table_schema<T: TableSchema>(dialect: &impl RustTypeMapping) -> Self {
49 let columns = T::SCHEMA
50 .iter()
51 .map(|col| {
52 let inner = strip_option(col.rust_type);
53 let data_type = dialect.map_type(inner);
54 let default = col
55 .default_expr
56 .map(|expr| DefaultValue::Expression(expr.to_string()));
57 ColumnSnapshot {
58 name: col.name.to_string(),
59 data_type,
60 nullable: col.nullable,
61 primary_key: col.primary_key,
62 unique: col.unique,
63 autoincrement: col.autoincrement,
64 default,
65 }
66 })
67 .collect();
68 Self {
69 name: T::NAME.to_string(),
70 columns,
71 }
72 }
73
74 #[must_use]
76 pub fn column(&self, name: &str) -> Option<&ColumnSnapshot> {
77 self.columns.iter().find(|c| c.name == name)
78 }
79}
80
81#[derive(Debug, Clone, PartialEq)]
83pub struct SchemaSnapshot {
84 pub tables: BTreeMap<String, TableSnapshot>,
86}
87
88impl SchemaSnapshot {
89 #[must_use]
91 pub fn new() -> Self {
92 Self {
93 tables: BTreeMap::new(),
94 }
95 }
96
97 pub fn add_table(&mut self, table: TableSnapshot) {
99 self.tables.insert(table.name.clone(), table);
100 }
101
102 pub fn add_from_table_schema<T: TableSchema>(&mut self, dialect: &impl RustTypeMapping) {
104 let snapshot = TableSnapshot::from_table_schema::<T>(dialect);
105 self.add_table(snapshot);
106 }
107}
108
109impl Default for SchemaSnapshot {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::migrations::{DuckDbDialect, PostgresDialect, SqliteDialect};
119 use crate::schema::{ColumnSchema, Table};
120
121 struct TestTable;
123 struct TestRow;
124
125 impl Table for TestTable {
126 type Row = TestRow;
127 const NAME: &'static str = "test_items";
128 const COLUMNS: &'static [&'static str] = &["id", "name", "score", "active"];
129 const PRIMARY_KEY: Option<&'static str> = Some("id");
130 }
131
132 impl TableSchema for TestTable {
133 const SCHEMA: &'static [ColumnSchema] = &[
134 ColumnSchema {
135 name: "id",
136 rust_type: "i64",
137 nullable: false,
138 primary_key: true,
139 unique: false,
140 autoincrement: true,
141 default_expr: None,
142 },
143 ColumnSchema {
144 name: "name",
145 rust_type: "String",
146 nullable: false,
147 primary_key: false,
148 unique: true,
149 autoincrement: false,
150 default_expr: None,
151 },
152 ColumnSchema {
153 name: "score",
154 rust_type: "Option<f64>",
155 nullable: true,
156 primary_key: false,
157 unique: false,
158 autoincrement: false,
159 default_expr: None,
160 },
161 ColumnSchema {
162 name: "active",
163 rust_type: "bool",
164 nullable: false,
165 primary_key: false,
166 unique: false,
167 autoincrement: false,
168 default_expr: Some("TRUE"),
169 },
170 ];
171 }
172
173 #[test]
174 fn from_table_schema_sqlite() {
175 let dialect = SqliteDialect::new();
176 let snap = TableSnapshot::from_table_schema::<TestTable>(&dialect);
177
178 assert_eq!(snap.name, "test_items");
179 assert_eq!(snap.columns.len(), 4);
180
181 let id = snap.column("id").unwrap();
182 assert_eq!(id.data_type, DataType::Bigint);
183 assert!(id.primary_key);
184 assert!(id.autoincrement);
185 assert!(!id.nullable);
186
187 let name_col = snap.column("name").unwrap();
188 assert_eq!(name_col.data_type, DataType::Text);
189 assert!(name_col.unique);
190
191 let score = snap.column("score").unwrap();
193 assert_eq!(score.data_type, DataType::Double);
194 assert!(score.nullable);
195
196 let active = snap.column("active").unwrap();
197 assert_eq!(active.data_type, DataType::Integer);
198 assert_eq!(
199 active.default,
200 Some(DefaultValue::Expression("TRUE".into()))
201 );
202 }
203
204 #[test]
205 fn from_table_schema_postgres() {
206 let dialect = PostgresDialect::new();
207 let snap = TableSnapshot::from_table_schema::<TestTable>(&dialect);
208
209 let name_col = snap.column("name").unwrap();
210 assert_eq!(name_col.data_type, DataType::Varchar(Some(255)));
211
212 let active = snap.column("active").unwrap();
213 assert_eq!(active.data_type, DataType::Boolean);
214 }
215
216 #[test]
217 fn from_table_schema_duckdb() {
218 let dialect = DuckDbDialect::new();
219 let snap = TableSnapshot::from_table_schema::<TestTable>(&dialect);
220
221 let name_col = snap.column("name").unwrap();
222 assert_eq!(name_col.data_type, DataType::Varchar(None));
223
224 let active = snap.column("active").unwrap();
225 assert_eq!(active.data_type, DataType::Boolean);
226 }
227
228 #[test]
229 fn column_lookup_by_name() {
230 let dialect = SqliteDialect::new();
231 let snap = TableSnapshot::from_table_schema::<TestTable>(&dialect);
232
233 assert!(snap.column("id").is_some());
234 assert!(snap.column("name").is_some());
235 assert!(snap.column("nonexistent").is_none());
236 }
237
238 #[test]
239 fn schema_snapshot_add_tables() {
240 let dialect = SqliteDialect::new();
241 let mut schema = SchemaSnapshot::new();
242 schema.add_from_table_schema::<TestTable>(&dialect);
243
244 assert_eq!(schema.tables.len(), 1);
245 assert!(schema.tables.contains_key("test_items"));
246 }
247}