1use super::types::ColumnType;
18use std::collections::HashMap;
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 pub check: Option<CheckConstraint>,
47 pub generated: Option<Generated>,
49}
50
51#[derive(Debug, Clone)]
53pub struct ForeignKey {
54 pub table: String,
55 pub column: String,
56 pub on_delete: FkAction,
57 pub on_update: FkAction,
58 pub deferrable: Deferrable,
60}
61
62#[derive(Debug, Clone, Default, PartialEq)]
64pub enum FkAction {
65 #[default]
66 NoAction,
67 Cascade,
68 SetNull,
69 SetDefault,
70 Restrict,
71}
72
73#[derive(Debug, Clone)]
75pub struct Index {
76 pub name: String,
77 pub table: String,
78 pub columns: Vec<String>,
79 pub unique: bool,
80 pub method: IndexMethod,
82 pub where_clause: Option<CheckExpr>,
84 pub include: Vec<String>,
86 pub concurrently: bool,
88}
89
90#[derive(Debug, Clone)]
92pub enum MigrationHint {
93 Rename { from: String, to: String },
95 Transform { expression: String, target: String },
97 Drop { target: String, confirmed: bool },
99}
100
101#[derive(Debug, Clone)]
107pub enum CheckExpr {
108 GreaterThan { column: String, value: i64 },
110 GreaterOrEqual { column: String, value: i64 },
112 LessThan { column: String, value: i64 },
114 LessOrEqual { column: String, value: i64 },
116 Between { column: String, low: i64, high: i64 },
118 In { column: String, values: Vec<String> },
120 Regex { column: String, pattern: String },
122 MaxLength { column: String, max: usize },
124 MinLength { column: String, min: usize },
126 NotNull { column: String },
128 And(Box<CheckExpr>, Box<CheckExpr>),
130 Or(Box<CheckExpr>, Box<CheckExpr>),
132 Not(Box<CheckExpr>),
134}
135
136#[derive(Debug, Clone)]
138pub struct CheckConstraint {
139 pub expr: CheckExpr,
140 pub name: Option<String>,
141}
142
143#[derive(Debug, Clone, Default, PartialEq)]
149pub enum Deferrable {
150 #[default]
151 NotDeferrable,
152 Deferrable,
153 InitiallyDeferred,
154 InitiallyImmediate,
155}
156
157#[derive(Debug, Clone)]
163pub enum Generated {
164 AlwaysStored(String),
166 AlwaysIdentity,
168 ByDefaultIdentity,
170}
171
172#[derive(Debug, Clone, Default, PartialEq)]
178pub enum IndexMethod {
179 #[default]
180 BTree,
181 Hash,
182 Gin,
183 Gist,
184 Brin,
185 SpGist,
186}
187
188impl Schema {
189 pub fn new() -> Self {
190 Self::default()
191 }
192
193 pub fn add_table(&mut self, table: Table) {
194 self.tables.insert(table.name.clone(), table);
195 }
196
197 pub fn add_index(&mut self, index: Index) {
198 self.indexes.push(index);
199 }
200
201 pub fn add_hint(&mut self, hint: MigrationHint) {
202 self.migrations.push(hint);
203 }
204
205 pub fn validate(&self) -> Result<(), Vec<String>> {
209 let mut errors = Vec::new();
210
211 for table in self.tables.values() {
212 for col in &table.columns {
213 if let Some(ref fk) = col.foreign_key {
214 if !self.tables.contains_key(&fk.table) {
216 errors.push(format!(
217 "FK error: {}.{} references non-existent table '{}'",
218 table.name, col.name, fk.table
219 ));
220 } else {
221 let ref_table = &self.tables[&fk.table];
223 if !ref_table.columns.iter().any(|c| c.name == fk.column) {
224 errors.push(format!(
225 "FK error: {}.{} references non-existent column '{}.{}'",
226 table.name, col.name, fk.table, fk.column
227 ));
228 }
229 }
230 }
231 }
232 }
233
234 if errors.is_empty() {
235 Ok(())
236 } else {
237 Err(errors)
238 }
239 }
240}
241
242impl Table {
243 pub fn new(name: impl Into<String>) -> Self {
244 Self {
245 name: name.into(),
246 columns: Vec::new(),
247 }
248 }
249
250 pub fn column(mut self, col: Column) -> Self {
251 self.columns.push(col);
252 self
253 }
254}
255
256impl Column {
257 pub fn new(name: impl Into<String>, data_type: ColumnType) -> Self {
259 Self {
260 name: name.into(),
261 data_type,
262 nullable: true,
263 primary_key: false,
264 unique: false,
265 default: None,
266 foreign_key: None,
267 check: None,
268 generated: None,
269 }
270 }
271
272 pub fn not_null(mut self) -> Self {
273 self.nullable = false;
274 self
275 }
276
277 pub fn primary_key(mut self) -> Self {
282 if !self.data_type.can_be_primary_key() {
283 panic!(
284 "Column '{}' of type {} cannot be a primary key. \
285 Valid PK types: UUID, SERIAL, BIGSERIAL, INT, BIGINT",
286 self.name,
287 self.data_type.name()
288 );
289 }
290 self.primary_key = true;
291 self.nullable = false;
292 self
293 }
294
295 pub fn unique(mut self) -> Self {
299 if !self.data_type.supports_indexing() {
300 panic!(
301 "Column '{}' of type {} cannot have UNIQUE constraint. \
302 JSONB and BYTEA types do not support standard indexing.",
303 self.name,
304 self.data_type.name()
305 );
306 }
307 self.unique = true;
308 self
309 }
310
311 pub fn default(mut self, val: impl Into<String>) -> Self {
312 self.default = Some(val.into());
313 self
314 }
315
316 pub fn references(mut self, table: &str, column: &str) -> Self {
325 self.foreign_key = Some(ForeignKey {
326 table: table.to_string(),
327 column: column.to_string(),
328 on_delete: FkAction::default(),
329 on_update: FkAction::default(),
330 deferrable: Deferrable::default(),
331 });
332 self
333 }
334
335 pub fn on_delete(mut self, action: FkAction) -> Self {
337 if let Some(ref mut fk) = self.foreign_key {
338 fk.on_delete = action;
339 }
340 self
341 }
342
343 pub fn on_update(mut self, action: FkAction) -> Self {
345 if let Some(ref mut fk) = self.foreign_key {
346 fk.on_update = action;
347 }
348 self
349 }
350
351 pub fn check(mut self, expr: CheckExpr) -> Self {
355 self.check = Some(CheckConstraint { expr, name: None });
356 self
357 }
358
359 pub fn check_named(mut self, name: impl Into<String>, expr: CheckExpr) -> Self {
361 self.check = Some(CheckConstraint {
362 expr,
363 name: Some(name.into()),
364 });
365 self
366 }
367
368 pub fn deferrable(mut self) -> Self {
372 if let Some(ref mut fk) = self.foreign_key {
373 fk.deferrable = Deferrable::Deferrable;
374 }
375 self
376 }
377
378 pub fn initially_deferred(mut self) -> Self {
380 if let Some(ref mut fk) = self.foreign_key {
381 fk.deferrable = Deferrable::InitiallyDeferred;
382 }
383 self
384 }
385
386 pub fn initially_immediate(mut self) -> Self {
388 if let Some(ref mut fk) = self.foreign_key {
389 fk.deferrable = Deferrable::InitiallyImmediate;
390 }
391 self
392 }
393
394 pub fn generated_stored(mut self, expr: impl Into<String>) -> Self {
398 self.generated = Some(Generated::AlwaysStored(expr.into()));
399 self
400 }
401
402 pub fn generated_identity(mut self) -> Self {
404 self.generated = Some(Generated::AlwaysIdentity);
405 self
406 }
407
408 pub fn generated_by_default(mut self) -> Self {
410 self.generated = Some(Generated::ByDefaultIdentity);
411 self
412 }
413}
414
415impl Index {
416 pub fn new(name: impl Into<String>, table: impl Into<String>, columns: Vec<String>) -> Self {
417 Self {
418 name: name.into(),
419 table: table.into(),
420 columns,
421 unique: false,
422 method: IndexMethod::default(),
423 where_clause: None,
424 include: Vec::new(),
425 concurrently: false,
426 }
427 }
428
429 pub fn unique(mut self) -> Self {
430 self.unique = true;
431 self
432 }
433
434 pub fn using(mut self, method: IndexMethod) -> Self {
438 self.method = method;
439 self
440 }
441
442 pub fn partial(mut self, expr: CheckExpr) -> Self {
444 self.where_clause = Some(expr);
445 self
446 }
447
448 pub fn include(mut self, cols: Vec<String>) -> Self {
450 self.include = cols;
451 self
452 }
453
454 pub fn concurrently(mut self) -> Self {
456 self.concurrently = true;
457 self
458 }
459}
460
461pub fn to_qail_string(schema: &Schema) -> String {
463 let mut output = String::new();
464 output.push_str("# QAIL Schema\n\n");
465
466 for table in schema.tables.values() {
467 output.push_str(&format!("table {} {{\n", table.name));
468 for col in &table.columns {
469 let mut constraints: Vec<String> = Vec::new();
470 if col.primary_key {
471 constraints.push("primary_key".to_string());
472 }
473 if !col.nullable && !col.primary_key {
474 constraints.push("not_null".to_string());
475 }
476 if col.unique {
477 constraints.push("unique".to_string());
478 }
479 if let Some(def) = &col.default {
480 constraints.push(format!("default {}", def));
481 }
482 if let Some(ref fk) = col.foreign_key {
483 constraints.push(format!("references {}({})", fk.table, fk.column));
484 }
485
486 let constraint_str = if constraints.is_empty() {
487 String::new()
488 } else {
489 format!(" {}", constraints.join(" "))
490 };
491
492 output.push_str(&format!(
493 " {} {}{}\n",
494 col.name,
495 col.data_type.to_pg_type(),
496 constraint_str
497 ));
498 }
499 output.push_str("}\n\n");
500 }
501
502 for idx in &schema.indexes {
503 let unique = if idx.unique { "unique " } else { "" };
504 output.push_str(&format!(
505 "{}index {} on {} ({})\n",
506 unique,
507 idx.name,
508 idx.table,
509 idx.columns.join(", ")
510 ));
511 }
512
513 for hint in &schema.migrations {
514 match hint {
515 MigrationHint::Rename { from, to } => {
516 output.push_str(&format!("rename {} -> {}\n", from, to));
517 }
518 MigrationHint::Transform { expression, target } => {
519 output.push_str(&format!("transform {} -> {}\n", expression, target));
520 }
521 MigrationHint::Drop { target, confirmed } => {
522 let confirm = if *confirmed { " confirm" } else { "" };
523 output.push_str(&format!("drop {}{}\n", target, confirm));
524 }
525 }
526 }
527
528 output
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn test_schema_builder() {
537 let mut schema = Schema::new();
538
539 let users = Table::new("users")
540 .column(Column::new("id", ColumnType::Serial).primary_key())
541 .column(Column::new("name", ColumnType::Text).not_null())
542 .column(Column::new("email", ColumnType::Text).unique());
543
544 schema.add_table(users);
545 schema.add_index(Index::new("idx_users_email", "users", vec!["email".into()]).unique());
546
547 let output = to_qail_string(&schema);
548 assert!(output.contains("table users"));
549 assert!(output.contains("id SERIAL primary_key"));
550 assert!(output.contains("unique index idx_users_email"));
551 }
552
553 #[test]
554 fn test_migration_hints() {
555 let mut schema = Schema::new();
556 schema.add_hint(MigrationHint::Rename {
557 from: "users.username".into(),
558 to: "users.name".into(),
559 });
560
561 let output = to_qail_string(&schema);
562 assert!(output.contains("rename users.username -> users.name"));
563 }
564
565 #[test]
566 #[should_panic(expected = "cannot be a primary key")]
567 fn test_invalid_primary_key_type() {
568 Column::new("data", ColumnType::Text).primary_key();
570 }
571
572 #[test]
573 #[should_panic(expected = "cannot have UNIQUE")]
574 fn test_invalid_unique_type() {
575 Column::new("data", ColumnType::Jsonb).unique();
577 }
578
579 #[test]
580 fn test_foreign_key_valid() {
581 let mut schema = Schema::new();
582
583 schema.add_table(
585 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
586 );
587
588 schema.add_table(
590 Table::new("posts")
591 .column(Column::new("id", ColumnType::Uuid).primary_key())
592 .column(
593 Column::new("user_id", ColumnType::Uuid)
594 .references("users", "id")
595 .on_delete(FkAction::Cascade),
596 ),
597 );
598
599 assert!(schema.validate().is_ok());
601 }
602
603 #[test]
604 fn test_foreign_key_invalid_table() {
605 let mut schema = Schema::new();
606
607 schema.add_table(
609 Table::new("posts")
610 .column(Column::new("id", ColumnType::Uuid).primary_key())
611 .column(Column::new("user_id", ColumnType::Uuid).references("nonexistent", "id")),
612 );
613
614 let result = schema.validate();
616 assert!(result.is_err());
617 assert!(result.unwrap_err()[0].contains("non-existent table"));
618 }
619
620 #[test]
621 fn test_foreign_key_invalid_column() {
622 let mut schema = Schema::new();
623
624 schema.add_table(
626 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
627 );
628
629 schema.add_table(
631 Table::new("posts")
632 .column(Column::new("id", ColumnType::Uuid).primary_key())
633 .column(
634 Column::new("user_id", ColumnType::Uuid).references("users", "wrong_column"),
635 ),
636 );
637
638 let result = schema.validate();
640 assert!(result.is_err());
641 assert!(result.unwrap_err()[0].contains("non-existent column"));
642 }
643}