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 pub extensions: Vec<Extension>,
28 pub comments: Vec<Comment>,
30 pub sequences: Vec<Sequence>,
32 pub enums: Vec<EnumType>,
34 pub views: Vec<ViewDef>,
36 pub functions: Vec<SchemaFunctionDef>,
38 pub triggers: Vec<SchemaTriggerDef>,
40 pub grants: Vec<Grant>,
42}
43
44#[derive(Debug, Clone)]
45pub struct Table {
46 pub name: String,
47 pub columns: Vec<Column>,
48 pub multi_column_fks: Vec<MultiColumnForeignKey>,
50}
51
52#[derive(Debug, Clone)]
54pub struct Column {
55 pub name: String,
56 pub data_type: ColumnType,
57 pub nullable: bool,
58 pub primary_key: bool,
59 pub unique: bool,
60 pub default: Option<String>,
61 pub foreign_key: Option<ForeignKey>,
62 pub check: Option<CheckConstraint>,
64 pub generated: Option<Generated>,
66}
67
68#[derive(Debug, Clone)]
70pub struct ForeignKey {
71 pub table: String,
72 pub column: String,
73 pub on_delete: FkAction,
74 pub on_update: FkAction,
75 pub deferrable: Deferrable,
77}
78
79#[derive(Debug, Clone, Default, PartialEq)]
81pub enum FkAction {
82 #[default]
83 NoAction,
84 Cascade,
85 SetNull,
86 SetDefault,
87 Restrict,
88}
89
90#[derive(Debug, Clone)]
91pub struct Index {
92 pub name: String,
93 pub table: String,
94 pub columns: Vec<String>,
95 pub unique: bool,
96 pub method: IndexMethod,
98 pub where_clause: Option<CheckExpr>,
100 pub include: Vec<String>,
102 pub concurrently: bool,
104 pub expressions: Vec<String>,
106}
107
108#[derive(Debug, Clone)]
109pub enum MigrationHint {
110 Rename { from: String, to: String },
112 Transform { expression: String, target: String },
114 Drop { target: String, confirmed: bool },
116}
117
118#[derive(Debug, Clone)]
124pub enum CheckExpr {
125 GreaterThan { column: String, value: i64 },
127 GreaterOrEqual { column: String, value: i64 },
129 LessThan { column: String, value: i64 },
131 LessOrEqual { column: String, value: i64 },
133 Between { column: String, low: i64, high: i64 },
134 In { column: String, values: Vec<String> },
135 Regex { column: String, pattern: String },
137 MaxLength { column: String, max: usize },
139 MinLength { column: String, min: usize },
141 NotNull { column: String },
142 And(Box<CheckExpr>, Box<CheckExpr>),
143 Or(Box<CheckExpr>, Box<CheckExpr>),
144 Not(Box<CheckExpr>),
145}
146
147#[derive(Debug, Clone)]
149pub struct CheckConstraint {
150 pub expr: CheckExpr,
151 pub name: Option<String>,
152}
153
154#[derive(Debug, Clone, Default, PartialEq)]
160pub enum Deferrable {
161 #[default]
162 NotDeferrable,
163 Deferrable,
164 InitiallyDeferred,
165 InitiallyImmediate,
166}
167
168#[derive(Debug, Clone)]
174pub enum Generated {
175 AlwaysStored(String),
177 AlwaysIdentity,
179 ByDefaultIdentity,
181}
182
183#[derive(Debug, Clone, Default, PartialEq)]
189pub enum IndexMethod {
190 #[default]
191 BTree,
192 Hash,
193 Gin,
194 Gist,
195 Brin,
196 SpGist,
197}
198
199#[derive(Debug, Clone, PartialEq)]
205pub struct Extension {
206 pub name: String,
207 pub schema: Option<String>,
208 pub version: Option<String>,
209}
210
211impl Extension {
212 pub fn new(name: impl Into<String>) -> Self {
213 Self {
214 name: name.into(),
215 schema: None,
216 version: None,
217 }
218 }
219
220 pub fn schema(mut self, schema: impl Into<String>) -> Self {
221 self.schema = Some(schema.into());
222 self
223 }
224
225 pub fn version(mut self, version: impl Into<String>) -> Self {
226 self.version = Some(version.into());
227 self
228 }
229}
230
231#[derive(Debug, Clone, PartialEq)]
233pub struct Comment {
234 pub target: CommentTarget,
235 pub text: String,
236}
237
238#[derive(Debug, Clone, PartialEq)]
239pub enum CommentTarget {
240 Table(String),
241 Column { table: String, column: String },
242}
243
244impl Comment {
245 pub fn on_table(table: impl Into<String>, text: impl Into<String>) -> Self {
246 Self {
247 target: CommentTarget::Table(table.into()),
248 text: text.into(),
249 }
250 }
251
252 pub fn on_column(
253 table: impl Into<String>,
254 column: impl Into<String>,
255 text: impl Into<String>,
256 ) -> Self {
257 Self {
258 target: CommentTarget::Column {
259 table: table.into(),
260 column: column.into(),
261 },
262 text: text.into(),
263 }
264 }
265}
266
267#[derive(Debug, Clone, PartialEq)]
269pub struct Sequence {
270 pub name: String,
271 pub data_type: Option<String>,
272 pub start: Option<i64>,
273 pub increment: Option<i64>,
274 pub min_value: Option<i64>,
275 pub max_value: Option<i64>,
276 pub cache: Option<i64>,
277 pub cycle: bool,
278 pub owned_by: Option<String>,
279}
280
281impl Sequence {
282 pub fn new(name: impl Into<String>) -> Self {
283 Self {
284 name: name.into(),
285 data_type: None,
286 start: None,
287 increment: None,
288 min_value: None,
289 max_value: None,
290 cache: None,
291 cycle: false,
292 owned_by: None,
293 }
294 }
295
296 pub fn start(mut self, v: i64) -> Self {
297 self.start = Some(v);
298 self
299 }
300
301 pub fn increment(mut self, v: i64) -> Self {
302 self.increment = Some(v);
303 self
304 }
305
306 pub fn min_value(mut self, v: i64) -> Self {
307 self.min_value = Some(v);
308 self
309 }
310
311 pub fn max_value(mut self, v: i64) -> Self {
312 self.max_value = Some(v);
313 self
314 }
315
316 pub fn cache(mut self, v: i64) -> Self {
317 self.cache = Some(v);
318 self
319 }
320
321 pub fn cycle(mut self) -> Self {
322 self.cycle = true;
323 self
324 }
325
326 pub fn owned_by(mut self, col: impl Into<String>) -> Self {
327 self.owned_by = Some(col.into());
328 self
329 }
330}
331
332#[derive(Debug, Clone, PartialEq)]
338pub struct EnumType {
339 pub name: String,
340 pub values: Vec<String>,
341}
342
343impl EnumType {
344 pub fn new(name: impl Into<String>, values: Vec<String>) -> Self {
345 Self {
346 name: name.into(),
347 values,
348 }
349 }
350
351 pub fn add_value(mut self, value: impl Into<String>) -> Self {
353 self.values.push(value.into());
354 self
355 }
356}
357
358#[derive(Debug, Clone, PartialEq)]
360pub struct MultiColumnForeignKey {
361 pub columns: Vec<String>,
362 pub ref_table: String,
363 pub ref_columns: Vec<String>,
364 pub on_delete: FkAction,
365 pub on_update: FkAction,
366 pub deferrable: Deferrable,
367 pub name: Option<String>,
368}
369
370impl MultiColumnForeignKey {
371 pub fn new(
372 columns: Vec<String>,
373 ref_table: impl Into<String>,
374 ref_columns: Vec<String>,
375 ) -> Self {
376 Self {
377 columns,
378 ref_table: ref_table.into(),
379 ref_columns,
380 on_delete: FkAction::default(),
381 on_update: FkAction::default(),
382 deferrable: Deferrable::default(),
383 name: None,
384 }
385 }
386
387 pub fn on_delete(mut self, action: FkAction) -> Self {
388 self.on_delete = action;
389 self
390 }
391
392 pub fn on_update(mut self, action: FkAction) -> Self {
393 self.on_update = action;
394 self
395 }
396
397 pub fn named(mut self, name: impl Into<String>) -> Self {
398 self.name = Some(name.into());
399 self
400 }
401}
402
403#[derive(Debug, Clone, PartialEq)]
409pub struct ViewDef {
410 pub name: String,
411 pub query: String,
412 pub materialized: bool,
413}
414
415impl ViewDef {
416 pub fn new(name: impl Into<String>, query: impl Into<String>) -> Self {
417 Self {
418 name: name.into(),
419 query: query.into(),
420 materialized: false,
421 }
422 }
423
424 pub fn materialized(mut self) -> Self {
425 self.materialized = true;
426 self
427 }
428}
429
430#[derive(Debug, Clone, PartialEq)]
432pub struct SchemaFunctionDef {
433 pub name: String,
434 pub args: Vec<String>,
435 pub returns: String,
436 pub body: String,
437 pub language: String,
438 pub volatility: Option<String>,
439}
440
441impl SchemaFunctionDef {
442 pub fn new(
443 name: impl Into<String>,
444 returns: impl Into<String>,
445 body: impl Into<String>,
446 ) -> Self {
447 Self {
448 name: name.into(),
449 args: Vec::new(),
450 returns: returns.into(),
451 body: body.into(),
452 language: "plpgsql".to_string(),
453 volatility: None,
454 }
455 }
456
457 pub fn language(mut self, lang: impl Into<String>) -> Self {
458 self.language = lang.into();
459 self
460 }
461
462 pub fn arg(mut self, arg: impl Into<String>) -> Self {
463 self.args.push(arg.into());
464 self
465 }
466
467 pub fn volatility(mut self, v: impl Into<String>) -> Self {
468 self.volatility = Some(v.into());
469 self
470 }
471}
472
473#[derive(Debug, Clone, PartialEq)]
475pub struct SchemaTriggerDef {
476 pub name: String,
477 pub table: String,
478 pub timing: String,
479 pub events: Vec<String>,
480 pub for_each_row: bool,
481 pub execute_function: String,
482 pub condition: Option<String>,
483}
484
485impl SchemaTriggerDef {
486 pub fn new(
487 name: impl Into<String>,
488 table: impl Into<String>,
489 execute_function: impl Into<String>,
490 ) -> Self {
491 Self {
492 name: name.into(),
493 table: table.into(),
494 timing: "BEFORE".to_string(),
495 events: vec!["INSERT".to_string()],
496 for_each_row: true,
497 execute_function: execute_function.into(),
498 condition: None,
499 }
500 }
501
502 pub fn timing(mut self, t: impl Into<String>) -> Self {
503 self.timing = t.into();
504 self
505 }
506
507 pub fn events(mut self, evts: Vec<String>) -> Self {
508 self.events = evts;
509 self
510 }
511
512 pub fn for_each_statement(mut self) -> Self {
513 self.for_each_row = false;
514 self
515 }
516
517 pub fn condition(mut self, cond: impl Into<String>) -> Self {
518 self.condition = Some(cond.into());
519 self
520 }
521}
522
523#[derive(Debug, Clone, PartialEq)]
525pub struct Grant {
526 pub action: GrantAction,
527 pub privileges: Vec<Privilege>,
528 pub on_object: String,
529 pub to_role: String,
530}
531
532#[derive(Debug, Clone, PartialEq, Default)]
533pub enum GrantAction {
534 #[default]
535 Grant,
536 Revoke,
537}
538
539#[derive(Debug, Clone, PartialEq)]
540pub enum Privilege {
541 All,
542 Select,
543 Insert,
544 Update,
545 Delete,
546 Usage,
547 Execute,
548}
549
550impl std::fmt::Display for Privilege {
551 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552 match self {
553 Privilege::All => write!(f, "ALL"),
554 Privilege::Select => write!(f, "SELECT"),
555 Privilege::Insert => write!(f, "INSERT"),
556 Privilege::Update => write!(f, "UPDATE"),
557 Privilege::Delete => write!(f, "DELETE"),
558 Privilege::Usage => write!(f, "USAGE"),
559 Privilege::Execute => write!(f, "EXECUTE"),
560 }
561 }
562}
563
564impl Grant {
565 pub fn new(
566 privileges: Vec<Privilege>,
567 on_object: impl Into<String>,
568 to_role: impl Into<String>,
569 ) -> Self {
570 Self {
571 action: GrantAction::Grant,
572 privileges,
573 on_object: on_object.into(),
574 to_role: to_role.into(),
575 }
576 }
577
578 pub fn revoke(
579 privileges: Vec<Privilege>,
580 on_object: impl Into<String>,
581 from_role: impl Into<String>,
582 ) -> Self {
583 Self {
584 action: GrantAction::Revoke,
585 privileges,
586 on_object: on_object.into(),
587 to_role: from_role.into(),
588 }
589 }
590}
591
592impl Schema {
593 pub fn new() -> Self {
594 Self::default()
595 }
596
597 pub fn add_table(&mut self, table: Table) {
598 self.tables.insert(table.name.clone(), table);
599 }
600
601 pub fn add_index(&mut self, index: Index) {
602 self.indexes.push(index);
603 }
604
605 pub fn add_hint(&mut self, hint: MigrationHint) {
606 self.migrations.push(hint);
607 }
608
609 pub fn add_extension(&mut self, ext: Extension) {
610 self.extensions.push(ext);
611 }
612
613 pub fn add_comment(&mut self, comment: Comment) {
614 self.comments.push(comment);
615 }
616
617 pub fn add_sequence(&mut self, seq: Sequence) {
618 self.sequences.push(seq);
619 }
620
621 pub fn add_enum(&mut self, enum_type: EnumType) {
622 self.enums.push(enum_type);
623 }
624
625 pub fn add_view(&mut self, view: ViewDef) {
626 self.views.push(view);
627 }
628
629 pub fn add_function(&mut self, func: SchemaFunctionDef) {
630 self.functions.push(func);
631 }
632
633 pub fn add_trigger(&mut self, trigger: SchemaTriggerDef) {
634 self.triggers.push(trigger);
635 }
636
637 pub fn add_grant(&mut self, grant: Grant) {
638 self.grants.push(grant);
639 }
640
641 pub fn validate(&self) -> Result<(), Vec<String>> {
643 let mut errors = Vec::new();
644
645 for table in self.tables.values() {
646 for col in &table.columns {
647 if let Some(ref fk) = col.foreign_key {
648 if !self.tables.contains_key(&fk.table) {
649 errors.push(format!(
650 "FK error: {}.{} references non-existent table '{}'",
651 table.name, col.name, fk.table
652 ));
653 } else {
654 let ref_table = &self.tables[&fk.table];
655 if !ref_table.columns.iter().any(|c| c.name == fk.column) {
656 errors.push(format!(
657 "FK error: {}.{} references non-existent column '{}.{}'",
658 table.name, col.name, fk.table, fk.column
659 ));
660 }
661 }
662 }
663 }
664 }
665
666 if errors.is_empty() {
667 Ok(())
668 } else {
669 Err(errors)
670 }
671 }
672}
673
674impl Table {
675 pub fn new(name: impl Into<String>) -> Self {
676 Self {
677 name: name.into(),
678 columns: Vec::new(),
679 multi_column_fks: Vec::new(),
680 }
681 }
682
683 pub fn column(mut self, col: Column) -> Self {
684 self.columns.push(col);
685 self
686 }
687
688 pub fn foreign_key(mut self, fk: MultiColumnForeignKey) -> Self {
690 self.multi_column_fks.push(fk);
691 self
692 }
693}
694
695impl Column {
696 pub fn new(name: impl Into<String>, data_type: ColumnType) -> Self {
698 Self {
699 name: name.into(),
700 data_type,
701 nullable: true,
702 primary_key: false,
703 unique: false,
704 default: None,
705 foreign_key: None,
706 check: None,
707 generated: None,
708 }
709 }
710
711 pub fn not_null(mut self) -> Self {
712 self.nullable = false;
713 self
714 }
715
716 pub fn primary_key(mut self) -> Self {
720 if !self.data_type.can_be_primary_key() {
721 panic!(
722 "Column '{}' of type {} cannot be a primary key. \
723 Valid PK types: UUID, SERIAL, BIGSERIAL, INT, BIGINT",
724 self.name,
725 self.data_type.name()
726 );
727 }
728 self.primary_key = true;
729 self.nullable = false;
730 self
731 }
732
733 pub fn unique(mut self) -> Self {
736 if !self.data_type.supports_indexing() {
737 panic!(
738 "Column '{}' of type {} cannot have UNIQUE constraint. \
739 JSONB and BYTEA types do not support standard indexing.",
740 self.name,
741 self.data_type.name()
742 );
743 }
744 self.unique = true;
745 self
746 }
747
748 pub fn default(mut self, val: impl Into<String>) -> Self {
749 self.default = Some(val.into());
750 self
751 }
752
753 pub fn references(mut self, table: &str, column: &str) -> Self {
761 self.foreign_key = Some(ForeignKey {
762 table: table.to_string(),
763 column: column.to_string(),
764 on_delete: FkAction::default(),
765 on_update: FkAction::default(),
766 deferrable: Deferrable::default(),
767 });
768 self
769 }
770
771 pub fn on_delete(mut self, action: FkAction) -> Self {
773 if let Some(ref mut fk) = self.foreign_key {
774 fk.on_delete = action;
775 }
776 self
777 }
778
779 pub fn on_update(mut self, action: FkAction) -> Self {
781 if let Some(ref mut fk) = self.foreign_key {
782 fk.on_update = action;
783 }
784 self
785 }
786
787 pub fn check(mut self, expr: CheckExpr) -> Self {
791 self.check = Some(CheckConstraint { expr, name: None });
792 self
793 }
794
795 pub fn check_named(mut self, name: impl Into<String>, expr: CheckExpr) -> Self {
797 self.check = Some(CheckConstraint {
798 expr,
799 name: Some(name.into()),
800 });
801 self
802 }
803
804 pub fn deferrable(mut self) -> Self {
808 if let Some(ref mut fk) = self.foreign_key {
809 fk.deferrable = Deferrable::Deferrable;
810 }
811 self
812 }
813
814 pub fn initially_deferred(mut self) -> Self {
816 if let Some(ref mut fk) = self.foreign_key {
817 fk.deferrable = Deferrable::InitiallyDeferred;
818 }
819 self
820 }
821
822 pub fn initially_immediate(mut self) -> Self {
824 if let Some(ref mut fk) = self.foreign_key {
825 fk.deferrable = Deferrable::InitiallyImmediate;
826 }
827 self
828 }
829
830 pub fn generated_stored(mut self, expr: impl Into<String>) -> Self {
834 self.generated = Some(Generated::AlwaysStored(expr.into()));
835 self
836 }
837
838 pub fn generated_identity(mut self) -> Self {
840 self.generated = Some(Generated::AlwaysIdentity);
841 self
842 }
843
844 pub fn generated_by_default(mut self) -> Self {
846 self.generated = Some(Generated::ByDefaultIdentity);
847 self
848 }
849}
850
851impl Index {
852 pub fn new(name: impl Into<String>, table: impl Into<String>, columns: Vec<String>) -> Self {
853 Self {
854 name: name.into(),
855 table: table.into(),
856 columns,
857 unique: false,
858 method: IndexMethod::default(),
859 where_clause: None,
860 include: Vec::new(),
861 concurrently: false,
862 expressions: Vec::new(),
863 }
864 }
865
866 pub fn expression(
868 name: impl Into<String>,
869 table: impl Into<String>,
870 expressions: Vec<String>,
871 ) -> Self {
872 Self {
873 name: name.into(),
874 table: table.into(),
875 columns: Vec::new(),
876 unique: false,
877 method: IndexMethod::default(),
878 where_clause: None,
879 include: Vec::new(),
880 concurrently: false,
881 expressions,
882 }
883 }
884
885 pub fn unique(mut self) -> Self {
886 self.unique = true;
887 self
888 }
889
890 pub fn using(mut self, method: IndexMethod) -> Self {
894 self.method = method;
895 self
896 }
897
898 pub fn partial(mut self, expr: CheckExpr) -> Self {
900 self.where_clause = Some(expr);
901 self
902 }
903
904 pub fn include(mut self, cols: Vec<String>) -> Self {
906 self.include = cols;
907 self
908 }
909
910 pub fn concurrently(mut self) -> Self {
912 self.concurrently = true;
913 self
914 }
915}
916
917pub fn to_qail_string(schema: &Schema) -> String {
919 let mut output = String::new();
920 output.push_str("# QAIL Schema\n\n");
921
922 for ext in &schema.extensions {
924 let mut line = format!("extension \"{}\"", ext.name);
925 if let Some(ref s) = ext.schema {
926 line.push_str(&format!(" schema {}", s));
927 }
928 if let Some(ref v) = ext.version {
929 line.push_str(&format!(" version \"{}\"", v));
930 }
931 output.push_str(&line);
932 output.push('\n');
933 }
934 if !schema.extensions.is_empty() {
935 output.push('\n');
936 }
937
938 for enum_type in &schema.enums {
940 let values = enum_type
941 .values
942 .iter()
943 .map(|v| v.as_str())
944 .collect::<Vec<_>>()
945 .join(", ");
946 output.push_str(&format!("enum {} {{ {} }}\n", enum_type.name, values));
947 }
948 if !schema.enums.is_empty() {
949 output.push('\n');
950 }
951
952 for seq in &schema.sequences {
954 if seq.start.is_some()
955 || seq.increment.is_some()
956 || seq.min_value.is_some()
957 || seq.max_value.is_some()
958 || seq.cache.is_some()
959 || seq.cycle
960 || seq.owned_by.is_some()
961 {
962 let mut opts = Vec::new();
963 if let Some(v) = seq.start {
964 opts.push(format!("start {}", v));
965 }
966 if let Some(v) = seq.increment {
967 opts.push(format!("increment {}", v));
968 }
969 if let Some(v) = seq.min_value {
970 opts.push(format!("minvalue {}", v));
971 }
972 if let Some(v) = seq.max_value {
973 opts.push(format!("maxvalue {}", v));
974 }
975 if let Some(v) = seq.cache {
976 opts.push(format!("cache {}", v));
977 }
978 if seq.cycle {
979 opts.push("cycle".to_string());
980 }
981 if let Some(ref o) = seq.owned_by {
982 opts.push(format!("owned_by {}", o));
983 }
984 output.push_str(&format!("sequence {} {{ {} }}\n", seq.name, opts.join(" ")));
985 } else {
986 output.push_str(&format!("sequence {}\n", seq.name));
987 }
988 }
989 if !schema.sequences.is_empty() {
990 output.push('\n');
991 }
992
993 for table in schema.tables.values() {
994 output.push_str(&format!("table {} {{\n", table.name));
995 for col in &table.columns {
996 let mut constraints: Vec<String> = Vec::new();
997 if col.primary_key {
998 constraints.push("primary_key".to_string());
999 }
1000 if !col.nullable && !col.primary_key {
1001 constraints.push("not_null".to_string());
1002 }
1003 if col.unique {
1004 constraints.push("unique".to_string());
1005 }
1006 if let Some(def) = &col.default {
1007 constraints.push(format!("default {}", def));
1008 }
1009 if let Some(ref fk) = col.foreign_key {
1010 constraints.push(format!("references {}({})", fk.table, fk.column));
1011 }
1012
1013 let constraint_str = if constraints.is_empty() {
1014 String::new()
1015 } else {
1016 format!(" {}", constraints.join(" "))
1017 };
1018
1019 output.push_str(&format!(
1020 " {} {}{}\n",
1021 col.name,
1022 col.data_type.to_pg_type(),
1023 constraint_str
1024 ));
1025 }
1026 for fk in &table.multi_column_fks {
1028 output.push_str(&format!(
1029 " foreign_key ({}) references {}({})\n",
1030 fk.columns.join(", "),
1031 fk.ref_table,
1032 fk.ref_columns.join(", ")
1033 ));
1034 }
1035 output.push_str("}\n\n");
1036 }
1037
1038 for idx in &schema.indexes {
1039 let unique = if idx.unique { "unique " } else { "" };
1040 let cols = if !idx.expressions.is_empty() {
1041 idx.expressions.join(", ")
1042 } else {
1043 idx.columns.join(", ")
1044 };
1045 output.push_str(&format!(
1046 "{}index {} on {} ({})\n",
1047 unique, idx.name, idx.table, cols
1048 ));
1049 }
1050
1051 for hint in &schema.migrations {
1052 match hint {
1053 MigrationHint::Rename { from, to } => {
1054 output.push_str(&format!("rename {} -> {}\n", from, to));
1055 }
1056 MigrationHint::Transform { expression, target } => {
1057 output.push_str(&format!("transform {} -> {}\n", expression, target));
1058 }
1059 MigrationHint::Drop { target, confirmed } => {
1060 let confirm = if *confirmed { " confirm" } else { "" };
1061 output.push_str(&format!("drop {}{}\n", target, confirm));
1062 }
1063 }
1064 }
1065
1066 for view in &schema.views {
1068 let prefix = if view.materialized {
1069 "materialized view"
1070 } else {
1071 "view"
1072 };
1073 output.push_str(&format!("{} {} $$\n{}\n$$\n\n", prefix, view.name, view.query));
1074 }
1075
1076 for func in &schema.functions {
1078 let args = func.args.join(", ");
1079 output.push_str(&format!(
1080 "function {}({}) returns {} language {} $$\n{}\n$$\n\n",
1081 func.name, args, func.returns, func.language, func.body
1082 ));
1083 }
1084
1085 for trigger in &schema.triggers {
1087 let events = trigger.events.join(" or ");
1088 output.push_str(&format!(
1089 "trigger {} on {} {} {} execute {}\n",
1090 trigger.name, trigger.table, trigger.timing.to_lowercase(),
1091 events.to_lowercase(), trigger.execute_function
1092 ));
1093 }
1094 if !schema.triggers.is_empty() {
1095 output.push('\n');
1096 }
1097
1098 for grant in &schema.grants {
1100 let privs: Vec<String> = grant.privileges.iter().map(|p| p.to_string().to_lowercase()).collect();
1101 match grant.action {
1102 GrantAction::Grant => {
1103 output.push_str(&format!(
1104 "grant {} on {} to {}\n",
1105 privs.join(", "), grant.on_object, grant.to_role
1106 ));
1107 }
1108 GrantAction::Revoke => {
1109 output.push_str(&format!(
1110 "revoke {} on {} from {}\n",
1111 privs.join(", "), grant.on_object, grant.to_role
1112 ));
1113 }
1114 }
1115 }
1116 if !schema.grants.is_empty() {
1117 output.push('\n');
1118 }
1119
1120 for comment in &schema.comments {
1122 match &comment.target {
1123 CommentTarget::Table(t) => {
1124 output.push_str(&format!("comment on {} \"{}\"\n", t, comment.text));
1125 }
1126 CommentTarget::Column { table, column } => {
1127 output.push_str(&format!(
1128 "comment on {}.{} \"{}\"\n",
1129 table, column, comment.text
1130 ));
1131 }
1132 }
1133 }
1134
1135 output
1136}
1137
1138
1139pub fn schema_to_commands(schema: &Schema) -> Vec<crate::ast::Qail> {
1142 use crate::ast::{Action, Constraint, Expr, IndexDef, Qail};
1143
1144 let mut cmds = Vec::new();
1145
1146 let mut table_order: Vec<&Table> = schema.tables.values().collect();
1148 table_order.sort_by(|a, b| {
1149 let a_has_fk = a.columns.iter().any(|c| c.foreign_key.is_some());
1150 let b_has_fk = b.columns.iter().any(|c| c.foreign_key.is_some());
1151 a_has_fk.cmp(&b_has_fk)
1152 });
1153
1154 for table in table_order {
1155 let columns: Vec<Expr> = table.columns.iter().map(|col| {
1157 let mut constraints = Vec::new();
1158
1159 if col.primary_key {
1160 constraints.push(Constraint::PrimaryKey);
1161 }
1162 if col.nullable {
1163 constraints.push(Constraint::Nullable);
1164 }
1165 if col.unique {
1166 constraints.push(Constraint::Unique);
1167 }
1168 if let Some(def) = &col.default {
1169 constraints.push(Constraint::Default(def.clone()));
1170 }
1171 if let Some(ref fk) = col.foreign_key {
1172 constraints.push(Constraint::References(format!(
1173 "{}({})",
1174 fk.table, fk.column
1175 )));
1176 }
1177
1178 Expr::Def {
1179 name: col.name.clone(),
1180 data_type: col.data_type.to_pg_type(),
1181 constraints,
1182 }
1183 }).collect();
1184
1185 cmds.push(Qail {
1186 action: Action::Make,
1187 table: table.name.clone(),
1188 columns,
1189 ..Default::default()
1190 });
1191 }
1192
1193 for idx in &schema.indexes {
1195 cmds.push(Qail {
1196 action: Action::Index,
1197 table: String::new(),
1198 index_def: Some(IndexDef {
1199 name: idx.name.clone(),
1200 table: idx.table.clone(),
1201 columns: idx.columns.clone(),
1202 unique: idx.unique,
1203 index_type: None,
1204 }),
1205 ..Default::default()
1206 });
1207 }
1208
1209 cmds
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214 use super::*;
1215
1216 #[test]
1217 fn test_schema_builder() {
1218 let mut schema = Schema::new();
1219
1220 let users = Table::new("users")
1221 .column(Column::new("id", ColumnType::Serial).primary_key())
1222 .column(Column::new("name", ColumnType::Text).not_null())
1223 .column(Column::new("email", ColumnType::Text).unique());
1224
1225 schema.add_table(users);
1226 schema.add_index(Index::new("idx_users_email", "users", vec!["email".into()]).unique());
1227
1228 let output = to_qail_string(&schema);
1229 assert!(output.contains("table users"));
1230 assert!(output.contains("id SERIAL primary_key"));
1231 assert!(output.contains("unique index idx_users_email"));
1232 }
1233
1234 #[test]
1235 fn test_migration_hints() {
1236 let mut schema = Schema::new();
1237 schema.add_hint(MigrationHint::Rename {
1238 from: "users.username".into(),
1239 to: "users.name".into(),
1240 });
1241
1242 let output = to_qail_string(&schema);
1243 assert!(output.contains("rename users.username -> users.name"));
1244 }
1245
1246 #[test]
1247 #[should_panic(expected = "cannot be a primary key")]
1248 fn test_invalid_primary_key_type() {
1249 Column::new("data", ColumnType::Text).primary_key();
1251 }
1252
1253 #[test]
1254 #[should_panic(expected = "cannot have UNIQUE")]
1255 fn test_invalid_unique_type() {
1256 Column::new("data", ColumnType::Jsonb).unique();
1258 }
1259
1260 #[test]
1261 fn test_foreign_key_valid() {
1262 let mut schema = Schema::new();
1263
1264 schema.add_table(
1265 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1266 );
1267
1268 schema.add_table(
1269 Table::new("posts")
1270 .column(Column::new("id", ColumnType::Uuid).primary_key())
1271 .column(
1272 Column::new("user_id", ColumnType::Uuid)
1273 .references("users", "id")
1274 .on_delete(FkAction::Cascade),
1275 ),
1276 );
1277
1278 assert!(schema.validate().is_ok());
1280 }
1281
1282 #[test]
1283 fn test_foreign_key_invalid_table() {
1284 let mut schema = Schema::new();
1285
1286 schema.add_table(
1287 Table::new("posts")
1288 .column(Column::new("id", ColumnType::Uuid).primary_key())
1289 .column(Column::new("user_id", ColumnType::Uuid).references("nonexistent", "id")),
1290 );
1291
1292 let result = schema.validate();
1294 assert!(result.is_err());
1295 assert!(result.unwrap_err()[0].contains("non-existent table"));
1296 }
1297
1298 #[test]
1299 fn test_foreign_key_invalid_column() {
1300 let mut schema = Schema::new();
1301
1302 schema.add_table(
1303 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1304 );
1305
1306 schema.add_table(
1307 Table::new("posts")
1308 .column(Column::new("id", ColumnType::Uuid).primary_key())
1309 .column(
1310 Column::new("user_id", ColumnType::Uuid).references("users", "wrong_column"),
1311 ),
1312 );
1313
1314 let result = schema.validate();
1316 assert!(result.is_err());
1317 assert!(result.unwrap_err()[0].contains("non-existent column"));
1318 }
1319}