1use std::collections::HashMap;
18use super::types::ColumnType;
19
20#[derive(Debug, Clone, Default)]
22pub struct Schema {
23 pub tables: HashMap<String, Table>,
24 pub indexes: Vec<Index>,
25 pub migrations: Vec<MigrationHint>,
26}
27
28#[derive(Debug, Clone)]
30pub struct Table {
31 pub name: String,
32 pub columns: Vec<Column>,
33}
34
35#[derive(Debug, Clone)]
37pub struct Column {
38 pub name: String,
39 pub data_type: ColumnType,
40 pub nullable: bool,
41 pub primary_key: bool,
42 pub unique: bool,
43 pub default: Option<String>,
44 pub foreign_key: Option<ForeignKey>,
45}
46
47#[derive(Debug, Clone)]
49pub struct ForeignKey {
50 pub table: String,
51 pub column: String,
52 pub on_delete: FkAction,
53 pub on_update: FkAction,
54}
55
56#[derive(Debug, Clone, Default, PartialEq)]
58pub enum FkAction {
59 #[default]
60 NoAction,
61 Cascade,
62 SetNull,
63 SetDefault,
64 Restrict,
65}
66
67#[derive(Debug, Clone)]
69pub struct Index {
70 pub name: String,
71 pub table: String,
72 pub columns: Vec<String>,
73 pub unique: bool,
74}
75
76#[derive(Debug, Clone)]
78pub enum MigrationHint {
79 Rename { from: String, to: String },
81 Transform { expression: String, target: String },
83 Drop { target: String, confirmed: bool },
85}
86
87impl Schema {
88 pub fn new() -> Self {
89 Self::default()
90 }
91
92 pub fn add_table(&mut self, table: Table) {
93 self.tables.insert(table.name.clone(), table);
94 }
95
96 pub fn add_index(&mut self, index: Index) {
97 self.indexes.push(index);
98 }
99
100 pub fn add_hint(&mut self, hint: MigrationHint) {
101 self.migrations.push(hint);
102 }
103
104 pub fn validate(&self) -> Result<(), Vec<String>> {
108 let mut errors = Vec::new();
109
110 for table in self.tables.values() {
111 for col in &table.columns {
112 if let Some(ref fk) = col.foreign_key {
113 if !self.tables.contains_key(&fk.table) {
115 errors.push(format!(
116 "FK error: {}.{} references non-existent table '{}'",
117 table.name, col.name, fk.table
118 ));
119 } else {
120 let ref_table = &self.tables[&fk.table];
122 if !ref_table.columns.iter().any(|c| c.name == fk.column) {
123 errors.push(format!(
124 "FK error: {}.{} references non-existent column '{}.{}'",
125 table.name, col.name, fk.table, fk.column
126 ));
127 }
128 }
129 }
130 }
131 }
132
133 if errors.is_empty() { Ok(()) } else { Err(errors) }
134 }
135}
136
137impl Table {
138 pub fn new(name: impl Into<String>) -> Self {
139 Self {
140 name: name.into(),
141 columns: Vec::new(),
142 }
143 }
144
145 pub fn column(mut self, col: Column) -> Self {
146 self.columns.push(col);
147 self
148 }
149}
150
151impl Column {
152 pub fn new(name: impl Into<String>, data_type: ColumnType) -> Self {
154 Self {
155 name: name.into(),
156 data_type,
157 nullable: true,
158 primary_key: false,
159 unique: false,
160 default: None,
161 foreign_key: None,
162 }
163 }
164
165 pub fn not_null(mut self) -> Self {
166 self.nullable = false;
167 self
168 }
169
170 pub fn primary_key(mut self) -> Self {
175 if !self.data_type.can_be_primary_key() {
176 panic!(
177 "Column '{}' of type {} cannot be a primary key. \
178 Valid PK types: UUID, SERIAL, BIGSERIAL, INT, BIGINT",
179 self.name,
180 self.data_type.name()
181 );
182 }
183 self.primary_key = true;
184 self.nullable = false;
185 self
186 }
187
188 pub fn unique(mut self) -> Self {
192 if !self.data_type.supports_indexing() {
193 panic!(
194 "Column '{}' of type {} cannot have UNIQUE constraint. \
195 JSONB and BYTEA types do not support standard indexing.",
196 self.name,
197 self.data_type.name()
198 );
199 }
200 self.unique = true;
201 self
202 }
203
204 pub fn default(mut self, val: impl Into<String>) -> Self {
205 self.default = Some(val.into());
206 self
207 }
208
209 pub fn references(mut self, table: &str, column: &str) -> Self {
218 self.foreign_key = Some(ForeignKey {
219 table: table.to_string(),
220 column: column.to_string(),
221 on_delete: FkAction::default(),
222 on_update: FkAction::default(),
223 });
224 self
225 }
226
227 pub fn on_delete(mut self, action: FkAction) -> Self {
229 if let Some(ref mut fk) = self.foreign_key {
230 fk.on_delete = action;
231 }
232 self
233 }
234
235 pub fn on_update(mut self, action: FkAction) -> Self {
237 if let Some(ref mut fk) = self.foreign_key {
238 fk.on_update = action;
239 }
240 self
241 }
242}
243
244impl Index {
245 pub fn new(name: impl Into<String>, table: impl Into<String>, columns: Vec<String>) -> Self {
246 Self {
247 name: name.into(),
248 table: table.into(),
249 columns,
250 unique: false,
251 }
252 }
253
254 pub fn unique(mut self) -> Self {
255 self.unique = true;
256 self
257 }
258}
259
260pub fn to_qail_string(schema: &Schema) -> String {
262 let mut output = String::new();
263 output.push_str("# QAIL Schema\n\n");
264
265 for table in schema.tables.values() {
266 output.push_str(&format!("table {} {{\n", table.name));
267 for col in &table.columns {
268 let mut constraints: Vec<String> = Vec::new();
269 if col.primary_key {
270 constraints.push("primary_key".to_string());
271 }
272 if !col.nullable && !col.primary_key {
273 constraints.push("not_null".to_string());
274 }
275 if col.unique {
276 constraints.push("unique".to_string());
277 }
278 if let Some(def) = &col.default {
279 constraints.push(format!("default {}", def));
280 }
281
282 let constraint_str = if constraints.is_empty() {
283 String::new()
284 } else {
285 format!(" {}", constraints.join(" "))
286 };
287
288 output.push_str(&format!(" {} {}{}\n", col.name, col.data_type.to_pg_type(), constraint_str));
289 }
290 output.push_str("}\n\n");
291 }
292
293 for idx in &schema.indexes {
294 let unique = if idx.unique { "unique " } else { "" };
295 output.push_str(&format!(
296 "{}index {} on {} ({})\n",
297 unique,
298 idx.name,
299 idx.table,
300 idx.columns.join(", ")
301 ));
302 }
303
304 for hint in &schema.migrations {
305 match hint {
306 MigrationHint::Rename { from, to } => {
307 output.push_str(&format!("rename {} -> {}\n", from, to));
308 }
309 MigrationHint::Transform { expression, target } => {
310 output.push_str(&format!("transform {} -> {}\n", expression, target));
311 }
312 MigrationHint::Drop { target, confirmed } => {
313 let confirm = if *confirmed { " confirm" } else { "" };
314 output.push_str(&format!("drop {}{}\n", target, confirm));
315 }
316 }
317 }
318
319 output
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_schema_builder() {
328 let mut schema = Schema::new();
329
330 let users = Table::new("users")
331 .column(Column::new("id", ColumnType::Serial).primary_key())
332 .column(Column::new("name", ColumnType::Text).not_null())
333 .column(Column::new("email", ColumnType::Text).unique());
334
335 schema.add_table(users);
336 schema.add_index(Index::new("idx_users_email", "users", vec!["email".into()]).unique());
337
338 let output = to_qail_string(&schema);
339 assert!(output.contains("table users"));
340 assert!(output.contains("id SERIAL primary_key"));
341 assert!(output.contains("unique index idx_users_email"));
342 }
343
344 #[test]
345 fn test_migration_hints() {
346 let mut schema = Schema::new();
347 schema.add_hint(MigrationHint::Rename {
348 from: "users.username".into(),
349 to: "users.name".into(),
350 });
351
352 let output = to_qail_string(&schema);
353 assert!(output.contains("rename users.username -> users.name"));
354 }
355
356 #[test]
357 #[should_panic(expected = "cannot be a primary key")]
358 fn test_invalid_primary_key_type() {
359 Column::new("data", ColumnType::Text).primary_key();
361 }
362
363 #[test]
364 #[should_panic(expected = "cannot have UNIQUE")]
365 fn test_invalid_unique_type() {
366 Column::new("data", ColumnType::Jsonb).unique();
368 }
369
370 #[test]
371 fn test_foreign_key_valid() {
372 let mut schema = Schema::new();
373
374 schema.add_table(Table::new("users")
376 .column(Column::new("id", ColumnType::Uuid).primary_key()));
377
378 schema.add_table(Table::new("posts")
380 .column(Column::new("id", ColumnType::Uuid).primary_key())
381 .column(Column::new("user_id", ColumnType::Uuid)
382 .references("users", "id")
383 .on_delete(FkAction::Cascade)));
384
385 assert!(schema.validate().is_ok());
387 }
388
389 #[test]
390 fn test_foreign_key_invalid_table() {
391 let mut schema = Schema::new();
392
393 schema.add_table(Table::new("posts")
395 .column(Column::new("id", ColumnType::Uuid).primary_key())
396 .column(Column::new("user_id", ColumnType::Uuid)
397 .references("nonexistent", "id")));
398
399 let result = schema.validate();
401 assert!(result.is_err());
402 assert!(result.unwrap_err()[0].contains("non-existent table"));
403 }
404
405 #[test]
406 fn test_foreign_key_invalid_column() {
407 let mut schema = Schema::new();
408
409 schema.add_table(Table::new("users")
411 .column(Column::new("id", ColumnType::Uuid).primary_key()));
412
413 schema.add_table(Table::new("posts")
415 .column(Column::new("id", ColumnType::Uuid).primary_key())
416 .column(Column::new("user_id", ColumnType::Uuid)
417 .references("users", "wrong_column")));
418
419 let result = schema.validate();
421 assert!(result.is_err());
422 assert!(result.unwrap_err()[0].contains("non-existent column"));
423 }
424}