1use super::policy::{PolicyPermissiveness, PolicyTarget, RlsPolicy};
18use super::types::ColumnType;
19use std::collections::HashMap;
20
21#[derive(Debug, Clone, Default)]
23pub struct Schema {
24 pub tables: HashMap<String, Table>,
26 pub indexes: Vec<Index>,
28 pub migrations: Vec<MigrationHint>,
30 pub extensions: Vec<Extension>,
32 pub comments: Vec<Comment>,
34 pub sequences: Vec<Sequence>,
36 pub enums: Vec<EnumType>,
38 pub views: Vec<ViewDef>,
40 pub functions: Vec<SchemaFunctionDef>,
42 pub triggers: Vec<SchemaTriggerDef>,
44 pub grants: Vec<Grant>,
46 pub policies: Vec<RlsPolicy>,
48 pub resources: Vec<ResourceDef>,
50}
51
52#[derive(Debug, Clone, PartialEq)]
58pub enum ResourceKind {
59 Bucket,
61 Queue,
63 Topic,
65}
66
67impl std::fmt::Display for ResourceKind {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 Self::Bucket => write!(f, "bucket"),
71 Self::Queue => write!(f, "queue"),
72 Self::Topic => write!(f, "topic"),
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
86pub struct ResourceDef {
87 pub name: String,
89 pub kind: ResourceKind,
91 pub provider: Option<String>,
93 pub properties: HashMap<String, String>,
95}
96
97#[derive(Debug, Clone)]
99pub struct Table {
100 pub name: String,
102 pub columns: Vec<Column>,
104 pub multi_column_fks: Vec<MultiColumnForeignKey>,
106 pub enable_rls: bool,
108 pub force_rls: bool,
110}
111
112#[derive(Debug, Clone)]
114pub struct Column {
115 pub name: String,
117 pub data_type: ColumnType,
119 pub nullable: bool,
121 pub primary_key: bool,
123 pub unique: bool,
125 pub default: Option<String>,
127 pub foreign_key: Option<ForeignKey>,
129 pub check: Option<CheckConstraint>,
131 pub generated: Option<Generated>,
133}
134
135#[derive(Debug, Clone)]
137pub struct ForeignKey {
138 pub table: String,
140 pub column: String,
142 pub on_delete: FkAction,
144 pub on_update: FkAction,
146 pub deferrable: Deferrable,
148}
149
150#[derive(Debug, Clone, Default, PartialEq)]
152pub enum FkAction {
153 #[default]
154 NoAction,
156 Cascade,
158 SetNull,
160 SetDefault,
162 Restrict,
164}
165
166#[derive(Debug, Clone)]
168pub struct Index {
169 pub name: String,
171 pub table: String,
173 pub columns: Vec<String>,
175 pub unique: bool,
177 pub method: IndexMethod,
179 pub where_clause: Option<CheckExpr>,
181 pub include: Vec<String>,
183 pub concurrently: bool,
185 pub expressions: Vec<String>,
187}
188
189#[derive(Debug, Clone)]
191pub enum MigrationHint {
192 Rename {
194 from: String,
196 to: String,
198 },
199 Transform {
201 expression: String,
203 target: String,
205 },
206 Drop {
208 target: String,
210 confirmed: bool,
212 },
213}
214
215#[derive(Debug, Clone)]
221pub enum CheckExpr {
222 GreaterThan {
224 column: String,
226 value: i64,
228 },
229 GreaterOrEqual {
231 column: String,
233 value: i64,
235 },
236 LessThan {
238 column: String,
240 value: i64,
242 },
243 LessOrEqual {
245 column: String,
247 value: i64,
249 },
250 Between {
252 column: String,
254 low: i64,
256 high: i64,
258 },
259 In {
261 column: String,
263 values: Vec<String>,
265 },
266 Regex {
268 column: String,
270 pattern: String,
272 },
273 MaxLength {
275 column: String,
277 max: usize,
279 },
280 MinLength {
282 column: String,
284 min: usize,
286 },
287 NotNull {
289 column: String,
291 },
292 And(Box<CheckExpr>, Box<CheckExpr>),
294 Or(Box<CheckExpr>, Box<CheckExpr>),
296 Not(Box<CheckExpr>),
298 Sql(String),
300}
301
302#[derive(Debug, Clone)]
304pub struct CheckConstraint {
305 pub expr: CheckExpr,
307 pub name: Option<String>,
309}
310
311#[derive(Debug, Clone, Default, PartialEq)]
317pub enum Deferrable {
318 #[default]
319 NotDeferrable,
321 Deferrable,
323 InitiallyDeferred,
325 InitiallyImmediate,
327}
328
329#[derive(Debug, Clone)]
335pub enum Generated {
336 AlwaysStored(String),
338 AlwaysIdentity,
340 ByDefaultIdentity,
342}
343
344#[derive(Debug, Clone, Default, PartialEq)]
350pub enum IndexMethod {
351 #[default]
352 BTree,
354 Hash,
356 Gin,
358 Gist,
360 Brin,
362 SpGist,
364}
365
366fn index_method_str(method: &IndexMethod) -> &'static str {
367 match method {
368 IndexMethod::BTree => "btree",
369 IndexMethod::Hash => "hash",
370 IndexMethod::Gin => "gin",
371 IndexMethod::Gist => "gist",
372 IndexMethod::Brin => "brin",
373 IndexMethod::SpGist => "spgist",
374 }
375}
376
377#[derive(Debug, Clone, PartialEq)]
383pub struct Extension {
384 pub name: String,
386 pub schema: Option<String>,
388 pub version: Option<String>,
390}
391
392impl Extension {
393 pub fn new(name: impl Into<String>) -> Self {
395 Self {
396 name: name.into(),
397 schema: None,
398 version: None,
399 }
400 }
401
402 pub fn schema(mut self, schema: impl Into<String>) -> Self {
404 self.schema = Some(schema.into());
405 self
406 }
407
408 pub fn version(mut self, version: impl Into<String>) -> Self {
410 self.version = Some(version.into());
411 self
412 }
413}
414
415#[derive(Debug, Clone, PartialEq)]
417pub struct Comment {
418 pub target: CommentTarget,
420 pub text: String,
422}
423
424#[derive(Debug, Clone, PartialEq)]
426pub enum CommentTarget {
427 Table(String),
429 Column {
431 table: String,
433 column: String,
435 },
436 Raw(String),
438}
439
440impl Comment {
441 pub fn on_table(table: impl Into<String>, text: impl Into<String>) -> Self {
443 Self {
444 target: CommentTarget::Table(table.into()),
445 text: text.into(),
446 }
447 }
448
449 pub fn on_column(
451 table: impl Into<String>,
452 column: impl Into<String>,
453 text: impl Into<String>,
454 ) -> Self {
455 Self {
456 target: CommentTarget::Column {
457 table: table.into(),
458 column: column.into(),
459 },
460 text: text.into(),
461 }
462 }
463
464 pub fn on_raw(target: impl Into<String>, text: impl Into<String>) -> Self {
466 Self {
467 target: CommentTarget::Raw(target.into()),
468 text: text.into(),
469 }
470 }
471}
472
473#[derive(Debug, Clone, PartialEq)]
475pub struct Sequence {
476 pub name: String,
478 pub data_type: Option<String>,
480 pub start: Option<i64>,
482 pub increment: Option<i64>,
484 pub min_value: Option<i64>,
486 pub max_value: Option<i64>,
488 pub cache: Option<i64>,
490 pub cycle: bool,
492 pub owned_by: Option<String>,
494}
495
496impl Sequence {
497 pub fn new(name: impl Into<String>) -> Self {
499 Self {
500 name: name.into(),
501 data_type: None,
502 start: None,
503 increment: None,
504 min_value: None,
505 max_value: None,
506 cache: None,
507 cycle: false,
508 owned_by: None,
509 }
510 }
511
512 pub fn start(mut self, v: i64) -> Self {
514 self.start = Some(v);
515 self
516 }
517
518 pub fn increment(mut self, v: i64) -> Self {
520 self.increment = Some(v);
521 self
522 }
523
524 pub fn min_value(mut self, v: i64) -> Self {
526 self.min_value = Some(v);
527 self
528 }
529
530 pub fn max_value(mut self, v: i64) -> Self {
532 self.max_value = Some(v);
533 self
534 }
535
536 pub fn cache(mut self, v: i64) -> Self {
538 self.cache = Some(v);
539 self
540 }
541
542 pub fn cycle(mut self) -> Self {
544 self.cycle = true;
545 self
546 }
547
548 pub fn owned_by(mut self, col: impl Into<String>) -> Self {
550 self.owned_by = Some(col.into());
551 self
552 }
553}
554
555#[derive(Debug, Clone, PartialEq)]
561pub struct EnumType {
562 pub name: String,
564 pub values: Vec<String>,
566}
567
568impl EnumType {
569 pub fn new(name: impl Into<String>, values: Vec<String>) -> Self {
571 Self {
572 name: name.into(),
573 values,
574 }
575 }
576
577 pub fn add_value(mut self, value: impl Into<String>) -> Self {
579 self.values.push(value.into());
580 self
581 }
582}
583
584#[derive(Debug, Clone, PartialEq)]
586pub struct MultiColumnForeignKey {
587 pub columns: Vec<String>,
589 pub ref_table: String,
591 pub ref_columns: Vec<String>,
593 pub on_delete: FkAction,
595 pub on_update: FkAction,
597 pub deferrable: Deferrable,
599 pub name: Option<String>,
601}
602
603impl MultiColumnForeignKey {
604 pub fn new(
606 columns: Vec<String>,
607 ref_table: impl Into<String>,
608 ref_columns: Vec<String>,
609 ) -> Self {
610 Self {
611 columns,
612 ref_table: ref_table.into(),
613 ref_columns,
614 on_delete: FkAction::default(),
615 on_update: FkAction::default(),
616 deferrable: Deferrable::default(),
617 name: None,
618 }
619 }
620
621 pub fn on_delete(mut self, action: FkAction) -> Self {
623 self.on_delete = action;
624 self
625 }
626
627 pub fn on_update(mut self, action: FkAction) -> Self {
629 self.on_update = action;
630 self
631 }
632
633 pub fn named(mut self, name: impl Into<String>) -> Self {
635 self.name = Some(name.into());
636 self
637 }
638}
639
640#[derive(Debug, Clone, PartialEq)]
646pub struct ViewDef {
647 pub name: String,
649 pub query: String,
651 pub materialized: bool,
653}
654
655impl ViewDef {
656 pub fn new(name: impl Into<String>, query: impl Into<String>) -> Self {
658 Self {
659 name: name.into(),
660 query: query.into(),
661 materialized: false,
662 }
663 }
664
665 pub fn materialized(mut self) -> Self {
667 self.materialized = true;
668 self
669 }
670}
671
672#[derive(Debug, Clone, PartialEq)]
674pub struct SchemaFunctionDef {
675 pub name: String,
677 pub args: Vec<String>,
679 pub returns: String,
681 pub body: String,
683 pub language: String,
685 pub volatility: Option<String>,
687}
688
689impl SchemaFunctionDef {
690 pub fn new(
692 name: impl Into<String>,
693 returns: impl Into<String>,
694 body: impl Into<String>,
695 ) -> Self {
696 Self {
697 name: name.into(),
698 args: Vec::new(),
699 returns: returns.into(),
700 body: body.into(),
701 language: "plpgsql".to_string(),
702 volatility: None,
703 }
704 }
705
706 pub fn language(mut self, lang: impl Into<String>) -> Self {
708 self.language = lang.into();
709 self
710 }
711
712 pub fn arg(mut self, arg: impl Into<String>) -> Self {
714 self.args.push(arg.into());
715 self
716 }
717
718 pub fn volatility(mut self, v: impl Into<String>) -> Self {
720 self.volatility = Some(v.into());
721 self
722 }
723}
724
725#[derive(Debug, Clone, PartialEq)]
727pub struct SchemaTriggerDef {
728 pub name: String,
730 pub table: String,
732 pub timing: String,
734 pub events: Vec<String>,
736 pub update_columns: Vec<String>,
738 pub for_each_row: bool,
740 pub execute_function: String,
742 pub condition: Option<String>,
744}
745
746impl SchemaTriggerDef {
747 pub fn new(
749 name: impl Into<String>,
750 table: impl Into<String>,
751 execute_function: impl Into<String>,
752 ) -> Self {
753 Self {
754 name: name.into(),
755 table: table.into(),
756 timing: "BEFORE".to_string(),
757 events: vec!["INSERT".to_string()],
758 update_columns: Vec::new(),
759 for_each_row: true,
760 execute_function: execute_function.into(),
761 condition: None,
762 }
763 }
764
765 pub fn timing(mut self, t: impl Into<String>) -> Self {
767 self.timing = t.into();
768 self
769 }
770
771 pub fn events(mut self, evts: Vec<String>) -> Self {
773 self.events = evts;
774 self
775 }
776
777 pub fn for_each_statement(mut self) -> Self {
779 self.for_each_row = false;
780 self
781 }
782
783 pub fn condition(mut self, cond: impl Into<String>) -> Self {
785 self.condition = Some(cond.into());
786 self
787 }
788}
789
790#[derive(Debug, Clone, PartialEq)]
792pub struct Grant {
793 pub action: GrantAction,
795 pub privileges: Vec<Privilege>,
797 pub on_object: String,
799 pub to_role: String,
801}
802
803#[derive(Debug, Clone, PartialEq, Default)]
805pub enum GrantAction {
806 #[default]
807 Grant,
809 Revoke,
811}
812
813#[derive(Debug, Clone, PartialEq)]
815pub enum Privilege {
816 All,
818 Select,
820 Insert,
822 Update,
824 Delete,
826 Usage,
828 Execute,
830}
831
832impl std::fmt::Display for Privilege {
833 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
834 match self {
835 Privilege::All => write!(f, "ALL"),
836 Privilege::Select => write!(f, "SELECT"),
837 Privilege::Insert => write!(f, "INSERT"),
838 Privilege::Update => write!(f, "UPDATE"),
839 Privilege::Delete => write!(f, "DELETE"),
840 Privilege::Usage => write!(f, "USAGE"),
841 Privilege::Execute => write!(f, "EXECUTE"),
842 }
843 }
844}
845
846impl Grant {
847 pub fn new(
849 privileges: Vec<Privilege>,
850 on_object: impl Into<String>,
851 to_role: impl Into<String>,
852 ) -> Self {
853 Self {
854 action: GrantAction::Grant,
855 privileges,
856 on_object: on_object.into(),
857 to_role: to_role.into(),
858 }
859 }
860
861 pub fn revoke(
863 privileges: Vec<Privilege>,
864 on_object: impl Into<String>,
865 from_role: impl Into<String>,
866 ) -> Self {
867 Self {
868 action: GrantAction::Revoke,
869 privileges,
870 on_object: on_object.into(),
871 to_role: from_role.into(),
872 }
873 }
874}
875
876impl Schema {
877 pub fn new() -> Self {
879 Self::default()
880 }
881
882 pub fn add_table(&mut self, table: Table) {
884 self.tables.insert(table.name.clone(), table);
885 }
886
887 pub fn add_index(&mut self, index: Index) {
889 self.indexes.push(index);
890 }
891
892 pub fn add_hint(&mut self, hint: MigrationHint) {
894 self.migrations.push(hint);
895 }
896
897 pub fn add_extension(&mut self, ext: Extension) {
899 self.extensions.push(ext);
900 }
901
902 pub fn add_comment(&mut self, comment: Comment) {
904 self.comments.push(comment);
905 }
906
907 pub fn add_sequence(&mut self, seq: Sequence) {
909 self.sequences.push(seq);
910 }
911
912 pub fn add_enum(&mut self, enum_type: EnumType) {
914 self.enums.push(enum_type);
915 }
916
917 pub fn add_view(&mut self, view: ViewDef) {
919 self.views.push(view);
920 }
921
922 pub fn add_function(&mut self, func: SchemaFunctionDef) {
924 self.functions.push(func);
925 }
926
927 pub fn add_trigger(&mut self, trigger: SchemaTriggerDef) {
929 self.triggers.push(trigger);
930 }
931
932 pub fn add_grant(&mut self, grant: Grant) {
934 self.grants.push(grant);
935 }
936
937 pub fn add_resource(&mut self, resource: ResourceDef) {
939 self.resources.push(resource);
940 }
941
942 pub fn add_policy(&mut self, policy: RlsPolicy) {
944 self.policies.push(policy);
945 }
946
947 pub fn validate(&self) -> Result<(), Vec<String>> {
949 let mut errors = Vec::new();
950
951 for table in self.tables.values() {
952 for col in &table.columns {
953 if let Some(ref fk) = col.foreign_key {
954 if !self.tables.contains_key(&fk.table) {
955 errors.push(format!(
956 "FK error: {}.{} references non-existent table '{}'",
957 table.name, col.name, fk.table
958 ));
959 } else {
960 let ref_table = &self.tables[&fk.table];
961 if !ref_table.columns.iter().any(|c| c.name == fk.column) {
962 errors.push(format!(
963 "FK error: {}.{} references non-existent column '{}.{}'",
964 table.name, col.name, fk.table, fk.column
965 ));
966 }
967 }
968 }
969 }
970 }
971
972 if errors.is_empty() {
973 Ok(())
974 } else {
975 Err(errors)
976 }
977 }
978}
979
980impl Table {
981 pub fn new(name: impl Into<String>) -> Self {
983 Self {
984 name: name.into(),
985 columns: Vec::new(),
986 multi_column_fks: Vec::new(),
987 enable_rls: false,
988 force_rls: false,
989 }
990 }
991
992 pub fn column(mut self, col: Column) -> Self {
994 self.columns.push(col);
995 self
996 }
997
998 pub fn foreign_key(mut self, fk: MultiColumnForeignKey) -> Self {
1000 self.multi_column_fks.push(fk);
1001 self
1002 }
1003}
1004
1005impl Column {
1006 fn primary_key_type_error(&self) -> String {
1007 format!(
1008 "Column '{}' of type {} cannot be a primary key. \
1009 Valid PK types: scalar/indexable types \
1010 (UUID, TEXT, VARCHAR, INT, BIGINT, SERIAL, BIGSERIAL, BOOLEAN, FLOAT, DECIMAL, \
1011 TIMESTAMP, TIMESTAMPTZ, DATE, TIME, ENUM, INET, CIDR, MACADDR)",
1012 self.name,
1013 self.data_type.name()
1014 )
1015 }
1016
1017 fn unique_type_error(&self) -> String {
1018 format!(
1019 "Column '{}' of type {} cannot have UNIQUE constraint. \
1020 JSONB and BYTEA types do not support standard indexing.",
1021 self.name,
1022 self.data_type.name()
1023 )
1024 }
1025
1026 pub fn new(name: impl Into<String>, data_type: ColumnType) -> Self {
1028 Self {
1029 name: name.into(),
1030 data_type,
1031 nullable: true,
1032 primary_key: false,
1033 unique: false,
1034 default: None,
1035 foreign_key: None,
1036 check: None,
1037 generated: None,
1038 }
1039 }
1040
1041 pub fn not_null(mut self) -> Self {
1043 self.nullable = false;
1044 self
1045 }
1046
1047 pub fn primary_key(mut self) -> Self {
1054 if !self.data_type.can_be_primary_key() {
1055 #[cfg(debug_assertions)]
1056 eprintln!("QAIL: {}", self.primary_key_type_error());
1057 }
1058 self.primary_key = true;
1059 self.nullable = false;
1060 self
1061 }
1062
1063 pub fn try_primary_key(mut self) -> Result<Self, String> {
1067 if !self.data_type.can_be_primary_key() {
1068 return Err(self.primary_key_type_error());
1069 }
1070 self.primary_key = true;
1071 self.nullable = false;
1072 Ok(self)
1073 }
1074
1075 pub fn unique(mut self) -> Self {
1082 if !self.data_type.supports_indexing() {
1083 #[cfg(debug_assertions)]
1084 eprintln!("QAIL: {}", self.unique_type_error());
1085 }
1086 self.unique = true;
1087 self
1088 }
1089
1090 pub fn try_unique(mut self) -> Result<Self, String> {
1094 if !self.data_type.supports_indexing() {
1095 return Err(self.unique_type_error());
1096 }
1097 self.unique = true;
1098 Ok(self)
1099 }
1100
1101 pub fn default(mut self, val: impl Into<String>) -> Self {
1103 self.default = Some(val.into());
1104 self
1105 }
1106
1107 pub fn references(mut self, table: &str, column: &str) -> Self {
1115 self.foreign_key = Some(ForeignKey {
1116 table: table.to_string(),
1117 column: column.to_string(),
1118 on_delete: FkAction::default(),
1119 on_update: FkAction::default(),
1120 deferrable: Deferrable::default(),
1121 });
1122 self
1123 }
1124
1125 pub fn on_delete(mut self, action: FkAction) -> Self {
1127 if let Some(ref mut fk) = self.foreign_key {
1128 fk.on_delete = action;
1129 }
1130 self
1131 }
1132
1133 pub fn on_update(mut self, action: FkAction) -> Self {
1135 if let Some(ref mut fk) = self.foreign_key {
1136 fk.on_update = action;
1137 }
1138 self
1139 }
1140
1141 pub fn check(mut self, expr: CheckExpr) -> Self {
1145 self.check = Some(CheckConstraint { expr, name: None });
1146 self
1147 }
1148
1149 pub fn check_named(mut self, name: impl Into<String>, expr: CheckExpr) -> Self {
1151 self.check = Some(CheckConstraint {
1152 expr,
1153 name: Some(name.into()),
1154 });
1155 self
1156 }
1157
1158 pub fn deferrable(mut self) -> Self {
1162 if let Some(ref mut fk) = self.foreign_key {
1163 fk.deferrable = Deferrable::Deferrable;
1164 }
1165 self
1166 }
1167
1168 pub fn initially_deferred(mut self) -> Self {
1170 if let Some(ref mut fk) = self.foreign_key {
1171 fk.deferrable = Deferrable::InitiallyDeferred;
1172 }
1173 self
1174 }
1175
1176 pub fn initially_immediate(mut self) -> Self {
1178 if let Some(ref mut fk) = self.foreign_key {
1179 fk.deferrable = Deferrable::InitiallyImmediate;
1180 }
1181 self
1182 }
1183
1184 pub fn generated_stored(mut self, expr: impl Into<String>) -> Self {
1188 self.generated = Some(Generated::AlwaysStored(expr.into()));
1189 self
1190 }
1191
1192 pub fn generated_identity(mut self) -> Self {
1194 self.generated = Some(Generated::AlwaysIdentity);
1195 self
1196 }
1197
1198 pub fn generated_by_default(mut self) -> Self {
1200 self.generated = Some(Generated::ByDefaultIdentity);
1201 self
1202 }
1203}
1204
1205impl Index {
1206 pub fn new(name: impl Into<String>, table: impl Into<String>, columns: Vec<String>) -> Self {
1208 Self {
1209 name: name.into(),
1210 table: table.into(),
1211 columns,
1212 unique: false,
1213 method: IndexMethod::default(),
1214 where_clause: None,
1215 include: Vec::new(),
1216 concurrently: false,
1217 expressions: Vec::new(),
1218 }
1219 }
1220
1221 pub fn expression(
1223 name: impl Into<String>,
1224 table: impl Into<String>,
1225 expressions: Vec<String>,
1226 ) -> Self {
1227 Self {
1228 name: name.into(),
1229 table: table.into(),
1230 columns: Vec::new(),
1231 unique: false,
1232 method: IndexMethod::default(),
1233 where_clause: None,
1234 include: Vec::new(),
1235 concurrently: false,
1236 expressions,
1237 }
1238 }
1239
1240 pub fn unique(mut self) -> Self {
1242 self.unique = true;
1243 self
1244 }
1245
1246 pub fn using(mut self, method: IndexMethod) -> Self {
1250 self.method = method;
1251 self
1252 }
1253
1254 pub fn partial(mut self, expr: CheckExpr) -> Self {
1256 self.where_clause = Some(expr);
1257 self
1258 }
1259
1260 pub fn include(mut self, cols: Vec<String>) -> Self {
1262 self.include = cols;
1263 self
1264 }
1265
1266 pub fn concurrently(mut self) -> Self {
1268 self.concurrently = true;
1269 self
1270 }
1271}
1272
1273fn fk_action_str(action: &FkAction) -> &'static str {
1276 match action {
1277 FkAction::NoAction => "no_action",
1278 FkAction::Cascade => "cascade",
1279 FkAction::SetNull => "set_null",
1280 FkAction::SetDefault => "set_default",
1281 FkAction::Restrict => "restrict",
1282 }
1283}
1284
1285fn check_expr_str(expr: &CheckExpr) -> String {
1287 match expr {
1288 CheckExpr::GreaterThan { column, value } => format!("{} > {}", column, value),
1289 CheckExpr::GreaterOrEqual { column, value } => format!("{} >= {}", column, value),
1290 CheckExpr::LessThan { column, value } => format!("{} < {}", column, value),
1291 CheckExpr::LessOrEqual { column, value } => format!("{} <= {}", column, value),
1292 CheckExpr::Between { column, low, high } => format!("{} between {} {}", column, low, high),
1293 CheckExpr::In { column, values } => format!("{} in [{}]", column, values.join(", ")),
1294 CheckExpr::Regex { column, pattern } => format!("{} ~ '{}'", column, pattern),
1295 CheckExpr::MaxLength { column, max } => format!("length({}) <= {}", column, max),
1296 CheckExpr::MinLength { column, min } => format!("length({}) >= {}", column, min),
1297 CheckExpr::NotNull { column } => format!("{} not_null", column),
1298 CheckExpr::And(l, r) => format!("{} and {}", check_expr_str(l), check_expr_str(r)),
1299 CheckExpr::Or(l, r) => format!("{} or {}", check_expr_str(l), check_expr_str(r)),
1300 CheckExpr::Not(e) => format!("not {}", check_expr_str(e)),
1301 CheckExpr::Sql(sql) => sql.clone(),
1302 }
1303}
1304
1305pub fn to_qail_string(schema: &Schema) -> String {
1307 let mut output = String::new();
1308 output.push_str("# QAIL Schema\n\n");
1309
1310 for ext in &schema.extensions {
1312 let mut line = format!("extension \"{}\"", ext.name);
1313 if let Some(ref s) = ext.schema {
1314 line.push_str(&format!(" schema {}", s));
1315 }
1316 if let Some(ref v) = ext.version {
1317 line.push_str(&format!(" version \"{}\"", v));
1318 }
1319 output.push_str(&line);
1320 output.push('\n');
1321 }
1322 if !schema.extensions.is_empty() {
1323 output.push('\n');
1324 }
1325
1326 for enum_type in &schema.enums {
1328 let values = enum_type
1329 .values
1330 .iter()
1331 .map(|v| v.as_str())
1332 .collect::<Vec<_>>()
1333 .join(", ");
1334 output.push_str(&format!("enum {} {{ {} }}\n", enum_type.name, values));
1335 }
1336 if !schema.enums.is_empty() {
1337 output.push('\n');
1338 }
1339
1340 for seq in &schema.sequences {
1342 if seq.start.is_some()
1343 || seq.increment.is_some()
1344 || seq.min_value.is_some()
1345 || seq.max_value.is_some()
1346 || seq.cache.is_some()
1347 || seq.cycle
1348 || seq.owned_by.is_some()
1349 {
1350 let mut opts = Vec::new();
1351 if let Some(v) = seq.start {
1352 opts.push(format!("start {}", v));
1353 }
1354 if let Some(v) = seq.increment {
1355 opts.push(format!("increment {}", v));
1356 }
1357 if let Some(v) = seq.min_value {
1358 opts.push(format!("minvalue {}", v));
1359 }
1360 if let Some(v) = seq.max_value {
1361 opts.push(format!("maxvalue {}", v));
1362 }
1363 if let Some(v) = seq.cache {
1364 opts.push(format!("cache {}", v));
1365 }
1366 if seq.cycle {
1367 opts.push("cycle".to_string());
1368 }
1369 if let Some(ref o) = seq.owned_by {
1370 opts.push(format!("owned_by {}", o));
1371 }
1372 output.push_str(&format!("sequence {} {{ {} }}\n", seq.name, opts.join(" ")));
1373 } else {
1374 output.push_str(&format!("sequence {}\n", seq.name));
1375 }
1376 }
1377 if !schema.sequences.is_empty() {
1378 output.push('\n');
1379 }
1380
1381 let mut table_names: Vec<&String> = schema.tables.keys().collect();
1382 table_names.sort();
1383 for table_name in table_names {
1384 let table = &schema.tables[table_name];
1385 output.push_str(&format!("table {} {{\n", table.name));
1386 for col in &table.columns {
1387 let mut constraints: Vec<String> = Vec::new();
1388 if col.primary_key {
1389 constraints.push("primary_key".to_string());
1390 }
1391 if !col.nullable && !col.primary_key {
1392 constraints.push("not_null".to_string());
1393 }
1394 if col.unique {
1395 constraints.push("unique".to_string());
1396 }
1397 if let Some(def) = &col.default {
1398 constraints.push(format!("default {}", def));
1399 }
1400 if let Some(ref fk) = col.foreign_key {
1401 let mut fk_str = format!("references {}({})", fk.table, fk.column);
1402 if fk.on_delete != FkAction::NoAction {
1403 fk_str.push_str(&format!(" on_delete {}", fk_action_str(&fk.on_delete)));
1404 }
1405 if fk.on_update != FkAction::NoAction {
1406 fk_str.push_str(&format!(" on_update {}", fk_action_str(&fk.on_update)));
1407 }
1408 match &fk.deferrable {
1409 Deferrable::Deferrable => fk_str.push_str(" deferrable"),
1410 Deferrable::InitiallyDeferred => fk_str.push_str(" initially_deferred"),
1411 Deferrable::InitiallyImmediate => fk_str.push_str(" initially_immediate"),
1412 Deferrable::NotDeferrable => {} }
1414 constraints.push(fk_str);
1415 }
1416 if let Some(ref check) = col.check {
1417 constraints.push(format!("check({})", check_expr_str(&check.expr)));
1418 if let Some(name) = &check.name {
1419 constraints.push(format!("check_name {}", name));
1420 }
1421 }
1422
1423 let constraint_str = if constraints.is_empty() {
1424 String::new()
1425 } else {
1426 format!(" {}", constraints.join(" "))
1427 };
1428
1429 output.push_str(&format!(
1430 " {} {}{}\n",
1431 col.name,
1432 col.data_type.to_pg_type(),
1433 constraint_str
1434 ));
1435 }
1436 for fk in &table.multi_column_fks {
1438 output.push_str(&format!(
1439 " foreign_key ({}) references {}({})\n",
1440 fk.columns.join(", "),
1441 fk.ref_table,
1442 fk.ref_columns.join(", ")
1443 ));
1444 }
1445 if table.enable_rls {
1447 output.push_str(" enable_rls\n");
1448 }
1449 if table.force_rls {
1450 output.push_str(" force_rls\n");
1451 }
1452 output.push_str("}\n\n");
1453 }
1454
1455 for idx in &schema.indexes {
1456 let unique = if idx.unique { "unique " } else { "" };
1457 let cols = if !idx.expressions.is_empty() {
1458 idx.expressions.join(", ")
1459 } else {
1460 idx.columns.join(", ")
1461 };
1462 let mut line = format!("{}index {} on {}", unique, idx.name, idx.table);
1463 if idx.method != IndexMethod::BTree {
1464 line.push_str(" using ");
1465 line.push_str(index_method_str(&idx.method));
1466 }
1467 line.push_str(" (");
1468 line.push_str(&cols);
1469 line.push(')');
1470 if let Some(where_clause) = &idx.where_clause {
1471 line.push_str(" where ");
1472 line.push_str(&check_expr_str(where_clause));
1473 }
1474 output.push_str(&line);
1475 output.push('\n');
1476 }
1477
1478 for hint in &schema.migrations {
1479 match hint {
1480 MigrationHint::Rename { from, to } => {
1481 output.push_str(&format!("rename {} -> {}\n", from, to));
1482 }
1483 MigrationHint::Transform { expression, target } => {
1484 output.push_str(&format!("transform {} -> {}\n", expression, target));
1485 }
1486 MigrationHint::Drop { target, confirmed } => {
1487 let confirm = if *confirmed { " confirm" } else { "" };
1488 output.push_str(&format!("drop {}{}\n", target, confirm));
1489 }
1490 }
1491 }
1492
1493 for view in &schema.views {
1495 let prefix = if view.materialized {
1496 "materialized view"
1497 } else {
1498 "view"
1499 };
1500 output.push_str(&format!(
1501 "{} {} $$\n{}\n$$\n\n",
1502 prefix, view.name, view.query
1503 ));
1504 }
1505
1506 for func in &schema.functions {
1508 let args = func.args.join(", ");
1509 let volatility = func
1510 .volatility
1511 .as_deref()
1512 .filter(|v| !v.trim().is_empty())
1513 .map(|v| format!(" {}", v))
1514 .unwrap_or_default();
1515 output.push_str(&format!(
1516 "function {}({}) returns {} language {}{} $$\n{}\n$$\n\n",
1517 func.name, args, func.returns, func.language, volatility, func.body
1518 ));
1519 }
1520
1521 for trigger in &schema.triggers {
1523 let mut events = Vec::new();
1524 for evt in &trigger.events {
1525 if evt.eq_ignore_ascii_case("UPDATE") && !trigger.update_columns.is_empty() {
1526 events.push(format!("UPDATE OF {}", trigger.update_columns.join(", ")));
1527 } else {
1528 events.push(evt.clone());
1529 }
1530 }
1531 output.push_str(&format!(
1532 "trigger {} on {} {} {} execute {}\n",
1533 trigger.name,
1534 trigger.table,
1535 trigger.timing.to_lowercase(),
1536 events.join(" or ").to_lowercase(),
1537 trigger.execute_function
1538 ));
1539 }
1540 if !schema.triggers.is_empty() {
1541 output.push('\n');
1542 }
1543
1544 for policy in &schema.policies {
1546 let cmd = match policy.target {
1547 PolicyTarget::All => "all",
1548 PolicyTarget::Select => "select",
1549 PolicyTarget::Insert => "insert",
1550 PolicyTarget::Update => "update",
1551 PolicyTarget::Delete => "delete",
1552 };
1553 let perm = match policy.permissiveness {
1554 PolicyPermissiveness::Permissive => "",
1555 PolicyPermissiveness::Restrictive => " restrictive",
1556 };
1557 let role_str = match &policy.role {
1558 Some(r) => format!(" to {}", r),
1559 None => String::new(),
1560 };
1561 output.push_str(&format!(
1562 "policy {} on {} for {}{}{}",
1563 policy.name, policy.table, cmd, role_str, perm
1564 ));
1565 if let Some(ref using) = policy.using {
1566 output.push_str(&format!("\n using $$ {} $$", using));
1567 }
1568 if let Some(ref wc) = policy.with_check {
1569 output.push_str(&format!("\n with_check $$ {} $$", wc));
1570 }
1571 output.push_str("\n\n");
1572 }
1573
1574 for grant in &schema.grants {
1576 let privs: Vec<String> = grant
1577 .privileges
1578 .iter()
1579 .map(|p| p.to_string().to_lowercase())
1580 .collect();
1581 match grant.action {
1582 GrantAction::Grant => {
1583 output.push_str(&format!(
1584 "grant {} on {} to {}\n",
1585 privs.join(", "),
1586 grant.on_object,
1587 grant.to_role
1588 ));
1589 }
1590 GrantAction::Revoke => {
1591 output.push_str(&format!(
1592 "revoke {} on {} from {}\n",
1593 privs.join(", "),
1594 grant.on_object,
1595 grant.to_role
1596 ));
1597 }
1598 }
1599 }
1600 if !schema.grants.is_empty() {
1601 output.push('\n');
1602 }
1603
1604 for comment in &schema.comments {
1606 match &comment.target {
1607 CommentTarget::Table(t) => {
1608 output.push_str(&format!("comment on {} \"{}\"\n", t, comment.text));
1609 }
1610 CommentTarget::Column { table, column } => {
1611 output.push_str(&format!(
1612 "comment on {}.{} \"{}\"\n",
1613 table, column, comment.text
1614 ));
1615 }
1616 CommentTarget::Raw(target) => {
1617 output.push_str(&format!("comment on {} \"{}\"\n", target, comment.text));
1618 }
1619 }
1620 }
1621
1622 output
1623}
1624
1625pub fn schema_to_commands(schema: &Schema) -> Vec<crate::ast::Qail> {
1628 use crate::ast::{Action, ColumnGeneration, Constraint, Expr, IndexDef, Qail};
1629
1630 let mut cmds = Vec::new();
1631
1632 let mut indegree: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
1635 let mut reverse_adj: std::collections::HashMap<String, Vec<String>> =
1636 std::collections::HashMap::new();
1637
1638 for name in schema.tables.keys() {
1639 indegree.insert(name.clone(), 0);
1640 }
1641
1642 for table in schema.tables.values() {
1643 let mut deps = std::collections::HashSet::new();
1644 for col in &table.columns {
1645 if let Some(fk) = &col.foreign_key
1646 && fk.table != table.name
1647 && schema.tables.contains_key(&fk.table)
1648 {
1649 deps.insert(fk.table.clone());
1650 }
1651 }
1652
1653 indegree.insert(table.name.clone(), deps.len());
1654 for dep in deps {
1655 reverse_adj.entry(dep).or_default().push(table.name.clone());
1656 }
1657 }
1658
1659 let mut ready = std::collections::BTreeSet::new();
1660 for (name, deg) in &indegree {
1661 if *deg == 0 {
1662 ready.insert(name.clone());
1663 }
1664 }
1665
1666 let mut ordered_names: Vec<String> = Vec::with_capacity(schema.tables.len());
1667 while let Some(next) = ready.pop_first() {
1668 ordered_names.push(next.clone());
1669 if let Some(dependents) = reverse_adj.get(&next) {
1670 for dep_name in dependents {
1671 if let Some(d) = indegree.get_mut(dep_name)
1672 && *d > 0
1673 {
1674 *d -= 1;
1675 if *d == 0 {
1676 ready.insert(dep_name.clone());
1677 }
1678 }
1679 }
1680 }
1681 }
1682
1683 if ordered_names.len() < schema.tables.len() {
1686 let mut leftovers: Vec<String> = schema
1687 .tables
1688 .keys()
1689 .filter(|name| !ordered_names.contains(*name))
1690 .cloned()
1691 .collect();
1692 leftovers.sort();
1693 ordered_names.extend(leftovers);
1694 }
1695
1696 for table_name in ordered_names {
1697 let table = &schema.tables[&table_name];
1698 let columns: Vec<Expr> = table
1700 .columns
1701 .iter()
1702 .map(|col| {
1703 let mut constraints = Vec::new();
1704
1705 if col.primary_key {
1706 constraints.push(Constraint::PrimaryKey);
1707 }
1708 if col.nullable {
1709 constraints.push(Constraint::Nullable);
1710 }
1711 if col.unique {
1712 constraints.push(Constraint::Unique);
1713 }
1714 if let Some(def) = &col.default {
1715 constraints.push(Constraint::Default(def.clone()));
1716 }
1717 if let Some(ref fk) = col.foreign_key {
1718 constraints.push(Constraint::References(foreign_key_to_sql(fk)));
1719 }
1720 if let Some(check) = &col.check {
1721 let check_sql = check_expr_to_sql(&check.expr);
1722 if let Some(name) = &check.name {
1723 constraints.push(Constraint::Check(vec![format!(
1724 "CONSTRAINT {} CHECK ({})",
1725 name, check_sql
1726 )]));
1727 } else {
1728 constraints.push(Constraint::Check(vec![check_sql]));
1729 }
1730 }
1731 if let Some(generated) = &col.generated {
1732 let gen_constraint = match generated {
1733 Generated::AlwaysStored(expr) => {
1734 Constraint::Generated(ColumnGeneration::Stored(expr.clone()))
1735 }
1736 Generated::AlwaysIdentity => {
1737 Constraint::Generated(ColumnGeneration::Stored("identity".to_string()))
1738 }
1739 Generated::ByDefaultIdentity => Constraint::Generated(
1740 ColumnGeneration::Stored("identity_by_default".to_string()),
1741 ),
1742 };
1743 constraints.push(gen_constraint);
1744 }
1745
1746 Expr::Def {
1747 name: col.name.clone(),
1748 data_type: col.data_type.to_pg_type(),
1749 constraints,
1750 }
1751 })
1752 .collect();
1753
1754 cmds.push(Qail {
1755 action: Action::Make,
1756 table: table.name.clone(),
1757 columns,
1758 ..Default::default()
1759 });
1760 }
1761
1762 for idx in &schema.indexes {
1764 cmds.push(Qail {
1765 action: Action::Index,
1766 table: String::new(),
1767 index_def: Some(IndexDef {
1768 name: idx.name.clone(),
1769 table: idx.table.clone(),
1770 columns: if !idx.expressions.is_empty() {
1771 idx.expressions.clone()
1772 } else {
1773 idx.columns.clone()
1774 },
1775 unique: idx.unique,
1776 index_type: Some(index_method_str(&idx.method).to_string()),
1777 where_clause: idx.where_clause.as_ref().map(check_expr_to_sql),
1778 }),
1779 ..Default::default()
1780 });
1781 }
1782
1783 cmds
1784}
1785
1786fn fk_action_to_sql(action: &FkAction) -> &'static str {
1787 match action {
1788 FkAction::NoAction => "NO ACTION",
1789 FkAction::Cascade => "CASCADE",
1790 FkAction::SetNull => "SET NULL",
1791 FkAction::SetDefault => "SET DEFAULT",
1792 FkAction::Restrict => "RESTRICT",
1793 }
1794}
1795
1796fn deferrable_to_sql(deferrable: &Deferrable) -> Option<&'static str> {
1797 match deferrable {
1798 Deferrable::NotDeferrable => None,
1799 Deferrable::Deferrable => Some("DEFERRABLE"),
1800 Deferrable::InitiallyDeferred => Some("DEFERRABLE INITIALLY DEFERRED"),
1801 Deferrable::InitiallyImmediate => Some("DEFERRABLE INITIALLY IMMEDIATE"),
1802 }
1803}
1804
1805fn foreign_key_to_sql(fk: &ForeignKey) -> String {
1806 let mut target = format!("{}({})", fk.table, fk.column);
1807 if fk.on_delete != FkAction::NoAction {
1808 target.push_str(" ON DELETE ");
1809 target.push_str(fk_action_to_sql(&fk.on_delete));
1810 }
1811 if fk.on_update != FkAction::NoAction {
1812 target.push_str(" ON UPDATE ");
1813 target.push_str(fk_action_to_sql(&fk.on_update));
1814 }
1815 if let Some(def) = deferrable_to_sql(&fk.deferrable) {
1816 target.push(' ');
1817 target.push_str(def);
1818 }
1819 target
1820}
1821
1822fn check_expr_to_sql(expr: &CheckExpr) -> String {
1823 match expr {
1824 CheckExpr::GreaterThan { column, value } => format!("{column} > {value}"),
1825 CheckExpr::GreaterOrEqual { column, value } => format!("{column} >= {value}"),
1826 CheckExpr::LessThan { column, value } => format!("{column} < {value}"),
1827 CheckExpr::LessOrEqual { column, value } => format!("{column} <= {value}"),
1828 CheckExpr::Between { column, low, high } => format!("{column} BETWEEN {low} AND {high}"),
1829 CheckExpr::In { column, values } => {
1830 if values.len() == 1 && looks_like_raw_check_expr(&values[0]) {
1831 return values[0].clone();
1832 }
1833 let quoted = values
1834 .iter()
1835 .map(|v| format!("'{}'", v.replace('\'', "''")))
1836 .collect::<Vec<_>>()
1837 .join(", ");
1838 format!("{column} IN ({quoted})")
1839 }
1840 CheckExpr::Regex { column, pattern } => {
1841 format!("{column} ~ '{}'", pattern.replace('\'', "''"))
1842 }
1843 CheckExpr::MaxLength { column, max } => format!("char_length({column}) <= {max}"),
1844 CheckExpr::MinLength { column, min } => format!("char_length({column}) >= {min}"),
1845 CheckExpr::NotNull { column } => format!("{column} IS NOT NULL"),
1846 CheckExpr::And(left, right) => {
1847 format!(
1848 "({}) AND ({})",
1849 check_expr_to_sql(left),
1850 check_expr_to_sql(right)
1851 )
1852 }
1853 CheckExpr::Or(left, right) => {
1854 format!(
1855 "({}) OR ({})",
1856 check_expr_to_sql(left),
1857 check_expr_to_sql(right)
1858 )
1859 }
1860 CheckExpr::Not(inner) => format!("NOT ({})", check_expr_to_sql(inner)),
1861 CheckExpr::Sql(sql) => sql.clone(),
1862 }
1863}
1864
1865fn looks_like_raw_check_expr(s: &str) -> bool {
1866 s.chars()
1867 .any(|c| c.is_whitespace() || matches!(c, '<' | '>' | '=' | '!' | '(' | ')' | ':'))
1868}
1869
1870#[cfg(test)]
1871mod tests {
1872 use super::*;
1873
1874 #[test]
1875 fn test_schema_builder() {
1876 let mut schema = Schema::new();
1877
1878 let users = Table::new("users")
1879 .column(Column::new("id", ColumnType::Serial).primary_key())
1880 .column(Column::new("name", ColumnType::Text).not_null())
1881 .column(Column::new("email", ColumnType::Text).unique());
1882
1883 schema.add_table(users);
1884 schema.add_index(Index::new("idx_users_email", "users", vec!["email".into()]).unique());
1885
1886 let output = to_qail_string(&schema);
1887 assert!(output.contains("table users"));
1888 assert!(output.contains("id SERIAL primary_key"));
1889 assert!(output.contains("unique index idx_users_email"));
1890 }
1891
1892 #[test]
1893 fn test_migration_hints() {
1894 let mut schema = Schema::new();
1895 schema.add_hint(MigrationHint::Rename {
1896 from: "users.username".into(),
1897 to: "users.name".into(),
1898 });
1899
1900 let output = to_qail_string(&schema);
1901 assert!(output.contains("rename users.username -> users.name"));
1902 }
1903
1904 #[test]
1905 fn test_to_qail_string_includes_function_volatility() {
1906 let mut schema = Schema::new();
1907 let func = SchemaFunctionDef::new(
1908 "is_super_admin",
1909 "boolean",
1910 "BEGIN RETURN true; END;".to_string(),
1911 )
1912 .language("plpgsql")
1913 .volatility("stable");
1914 schema.add_function(func);
1915
1916 let output = to_qail_string(&schema);
1917 assert!(
1918 output.contains("function is_super_admin() returns boolean language plpgsql stable $$")
1919 );
1920 }
1921
1922 #[test]
1923 fn test_invalid_primary_key_type_strict() {
1924 let err = Column::new("data", ColumnType::Jsonb)
1925 .try_primary_key()
1926 .expect_err("JSONB should be rejected by strict PK policy");
1927 assert!(err.contains("cannot be a primary key"));
1928 }
1929
1930 #[test]
1931 fn test_invalid_primary_key_type_fail_soft() {
1932 let col = Column::new("data", ColumnType::Jsonb).primary_key();
1933 assert!(col.primary_key);
1934 assert!(!col.nullable);
1935 }
1936
1937 #[test]
1938 fn test_invalid_unique_type_strict() {
1939 let err = Column::new("data", ColumnType::Jsonb)
1940 .try_unique()
1941 .expect_err("JSONB should be rejected by strict UNIQUE policy");
1942 assert!(err.contains("cannot have UNIQUE"));
1943 }
1944
1945 #[test]
1946 fn test_invalid_unique_type_fail_soft() {
1947 let col = Column::new("data", ColumnType::Jsonb).unique();
1948 assert!(col.unique);
1949 }
1950
1951 #[test]
1952 fn test_foreign_key_valid() {
1953 let mut schema = Schema::new();
1954
1955 schema.add_table(
1956 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1957 );
1958
1959 schema.add_table(
1960 Table::new("posts")
1961 .column(Column::new("id", ColumnType::Uuid).primary_key())
1962 .column(
1963 Column::new("user_id", ColumnType::Uuid)
1964 .references("users", "id")
1965 .on_delete(FkAction::Cascade),
1966 ),
1967 );
1968
1969 assert!(schema.validate().is_ok());
1971 }
1972
1973 #[test]
1974 fn test_foreign_key_invalid_table() {
1975 let mut schema = Schema::new();
1976
1977 schema.add_table(
1978 Table::new("posts")
1979 .column(Column::new("id", ColumnType::Uuid).primary_key())
1980 .column(Column::new("user_id", ColumnType::Uuid).references("nonexistent", "id")),
1981 );
1982
1983 let result = schema.validate();
1985 assert!(result.is_err());
1986 assert!(result.unwrap_err()[0].contains("non-existent table"));
1987 }
1988
1989 #[test]
1990 fn test_foreign_key_invalid_column() {
1991 let mut schema = Schema::new();
1992
1993 schema.add_table(
1994 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1995 );
1996
1997 schema.add_table(
1998 Table::new("posts")
1999 .column(Column::new("id", ColumnType::Uuid).primary_key())
2000 .column(
2001 Column::new("user_id", ColumnType::Uuid).references("users", "wrong_column"),
2002 ),
2003 );
2004
2005 let result = schema.validate();
2007 assert!(result.is_err());
2008 assert!(result.unwrap_err()[0].contains("non-existent column"));
2009 }
2010
2011 #[test]
2012 fn test_schema_to_commands_preserves_fk_actions_and_checks() {
2013 let mut schema = Schema::new();
2014 schema.add_table(
2015 Table::new("orgs").column(Column::new("id", ColumnType::Uuid).primary_key()),
2016 );
2017 schema.add_table(
2018 Table::new("users")
2019 .column(Column::new("id", ColumnType::Uuid).primary_key())
2020 .column(
2021 Column::new("org_id", ColumnType::Uuid)
2022 .references("orgs", "id")
2023 .on_delete(FkAction::Cascade)
2024 .on_update(FkAction::Restrict),
2025 )
2026 .column(
2027 Column::new("age", ColumnType::Int).check(CheckExpr::GreaterOrEqual {
2028 column: "age".to_string(),
2029 value: 18,
2030 }),
2031 ),
2032 );
2033
2034 let cmds = schema_to_commands(&schema);
2035 let users_cmd = cmds
2036 .iter()
2037 .find(|c| c.action == crate::ast::Action::Make && c.table == "users")
2038 .expect("users create command should exist");
2039 let org_id_constraints = users_cmd
2040 .columns
2041 .iter()
2042 .find_map(|e| match e {
2043 crate::ast::Expr::Def {
2044 name, constraints, ..
2045 } if name == "org_id" => Some(constraints),
2046 _ => None,
2047 })
2048 .expect("org_id should exist");
2049 let age_constraints = users_cmd
2050 .columns
2051 .iter()
2052 .find_map(|e| match e {
2053 crate::ast::Expr::Def {
2054 name, constraints, ..
2055 } if name == "age" => Some(constraints),
2056 _ => None,
2057 })
2058 .expect("age should exist");
2059
2060 assert!(
2061 org_id_constraints.iter().any(|c| matches!(
2062 c,
2063 crate::ast::Constraint::References(target)
2064 if target.contains("orgs(id)")
2065 && target.contains("ON DELETE CASCADE")
2066 && target.contains("ON UPDATE RESTRICT")
2067 )),
2068 "foreign key action clauses should be preserved"
2069 );
2070 assert!(
2071 age_constraints
2072 .iter()
2073 .any(|c| matches!(c, crate::ast::Constraint::Check(vals) if vals.len() == 1)),
2074 "check expressions should be preserved"
2075 );
2076 }
2077}