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 Hnsw,
366 IvfFlat,
368}
369
370pub(crate) fn index_method_str(method: &IndexMethod) -> &'static str {
371 match method {
372 IndexMethod::BTree => "btree",
373 IndexMethod::Hash => "hash",
374 IndexMethod::Gin => "gin",
375 IndexMethod::Gist => "gist",
376 IndexMethod::Brin => "brin",
377 IndexMethod::SpGist => "spgist",
378 IndexMethod::Hnsw => "hnsw",
379 IndexMethod::IvfFlat => "ivfflat",
380 }
381}
382
383#[derive(Debug, Clone, PartialEq)]
389pub struct Extension {
390 pub name: String,
392 pub schema: Option<String>,
394 pub version: Option<String>,
396}
397
398impl Extension {
399 pub fn new(name: impl Into<String>) -> Self {
401 Self {
402 name: name.into(),
403 schema: None,
404 version: None,
405 }
406 }
407
408 pub fn schema(mut self, schema: impl Into<String>) -> Self {
410 self.schema = Some(schema.into());
411 self
412 }
413
414 pub fn version(mut self, version: impl Into<String>) -> Self {
416 self.version = Some(version.into());
417 self
418 }
419}
420
421#[derive(Debug, Clone, PartialEq)]
423pub struct Comment {
424 pub target: CommentTarget,
426 pub text: String,
428}
429
430#[derive(Debug, Clone, PartialEq)]
432pub enum CommentTarget {
433 Table(String),
435 Column {
437 table: String,
439 column: String,
441 },
442 Raw(String),
444}
445
446impl Comment {
447 pub fn on_table(table: impl Into<String>, text: impl Into<String>) -> Self {
449 Self {
450 target: CommentTarget::Table(table.into()),
451 text: text.into(),
452 }
453 }
454
455 pub fn on_column(
457 table: impl Into<String>,
458 column: impl Into<String>,
459 text: impl Into<String>,
460 ) -> Self {
461 Self {
462 target: CommentTarget::Column {
463 table: table.into(),
464 column: column.into(),
465 },
466 text: text.into(),
467 }
468 }
469
470 pub fn on_raw(target: impl Into<String>, text: impl Into<String>) -> Self {
472 Self {
473 target: CommentTarget::Raw(target.into()),
474 text: text.into(),
475 }
476 }
477}
478
479#[derive(Debug, Clone, PartialEq)]
481pub struct Sequence {
482 pub name: String,
484 pub data_type: Option<String>,
486 pub start: Option<i64>,
488 pub increment: Option<i64>,
490 pub min_value: Option<i64>,
492 pub max_value: Option<i64>,
494 pub cache: Option<i64>,
496 pub cycle: bool,
498 pub owned_by: Option<String>,
500}
501
502impl Sequence {
503 pub fn new(name: impl Into<String>) -> Self {
505 Self {
506 name: name.into(),
507 data_type: None,
508 start: None,
509 increment: None,
510 min_value: None,
511 max_value: None,
512 cache: None,
513 cycle: false,
514 owned_by: None,
515 }
516 }
517
518 pub fn start(mut self, v: i64) -> Self {
520 self.start = Some(v);
521 self
522 }
523
524 pub fn increment(mut self, v: i64) -> Self {
526 self.increment = Some(v);
527 self
528 }
529
530 pub fn min_value(mut self, v: i64) -> Self {
532 self.min_value = Some(v);
533 self
534 }
535
536 pub fn max_value(mut self, v: i64) -> Self {
538 self.max_value = Some(v);
539 self
540 }
541
542 pub fn cache(mut self, v: i64) -> Self {
544 self.cache = Some(v);
545 self
546 }
547
548 pub fn cycle(mut self) -> Self {
550 self.cycle = true;
551 self
552 }
553
554 pub fn owned_by(mut self, col: impl Into<String>) -> Self {
556 self.owned_by = Some(col.into());
557 self
558 }
559}
560
561#[derive(Debug, Clone, PartialEq)]
567pub struct EnumType {
568 pub name: String,
570 pub values: Vec<String>,
572}
573
574impl EnumType {
575 pub fn new(name: impl Into<String>, values: Vec<String>) -> Self {
577 Self {
578 name: name.into(),
579 values,
580 }
581 }
582
583 pub fn add_value(mut self, value: impl Into<String>) -> Self {
585 self.values.push(value.into());
586 self
587 }
588}
589
590#[derive(Debug, Clone, PartialEq)]
592pub struct MultiColumnForeignKey {
593 pub columns: Vec<String>,
595 pub ref_table: String,
597 pub ref_columns: Vec<String>,
599 pub on_delete: FkAction,
601 pub on_update: FkAction,
603 pub deferrable: Deferrable,
605 pub name: Option<String>,
607}
608
609impl MultiColumnForeignKey {
610 pub fn new(
612 columns: Vec<String>,
613 ref_table: impl Into<String>,
614 ref_columns: Vec<String>,
615 ) -> Self {
616 Self {
617 columns,
618 ref_table: ref_table.into(),
619 ref_columns,
620 on_delete: FkAction::default(),
621 on_update: FkAction::default(),
622 deferrable: Deferrable::default(),
623 name: None,
624 }
625 }
626
627 pub fn on_delete(mut self, action: FkAction) -> Self {
629 self.on_delete = action;
630 self
631 }
632
633 pub fn on_update(mut self, action: FkAction) -> Self {
635 self.on_update = action;
636 self
637 }
638
639 pub fn named(mut self, name: impl Into<String>) -> Self {
641 self.name = Some(name.into());
642 self
643 }
644}
645
646#[derive(Debug, Clone, PartialEq)]
652pub struct ViewDef {
653 pub name: String,
655 pub query: String,
657 pub materialized: bool,
659}
660
661impl ViewDef {
662 pub fn new(name: impl Into<String>, query: impl Into<String>) -> Self {
664 Self {
665 name: name.into(),
666 query: query.into(),
667 materialized: false,
668 }
669 }
670
671 pub fn materialized(mut self) -> Self {
673 self.materialized = true;
674 self
675 }
676}
677
678#[derive(Debug, Clone, PartialEq)]
680pub struct SchemaFunctionDef {
681 pub name: String,
683 pub args: Vec<String>,
685 pub returns: String,
687 pub body: String,
689 pub language: String,
691 pub volatility: Option<String>,
693}
694
695impl SchemaFunctionDef {
696 pub fn new(
698 name: impl Into<String>,
699 returns: impl Into<String>,
700 body: impl Into<String>,
701 ) -> Self {
702 Self {
703 name: name.into(),
704 args: Vec::new(),
705 returns: returns.into(),
706 body: body.into(),
707 language: "plpgsql".to_string(),
708 volatility: None,
709 }
710 }
711
712 pub fn language(mut self, lang: impl Into<String>) -> Self {
714 self.language = lang.into();
715 self
716 }
717
718 pub fn arg(mut self, arg: impl Into<String>) -> Self {
720 self.args.push(arg.into());
721 self
722 }
723
724 pub fn volatility(mut self, v: impl Into<String>) -> Self {
726 self.volatility = Some(v.into());
727 self
728 }
729}
730
731#[derive(Debug, Clone, PartialEq)]
733pub struct SchemaTriggerDef {
734 pub name: String,
736 pub table: String,
738 pub timing: String,
740 pub events: Vec<String>,
742 pub update_columns: Vec<String>,
744 pub for_each_row: bool,
746 pub execute_function: String,
748 pub condition: Option<String>,
750}
751
752impl SchemaTriggerDef {
753 pub fn new(
755 name: impl Into<String>,
756 table: impl Into<String>,
757 execute_function: impl Into<String>,
758 ) -> Self {
759 Self {
760 name: name.into(),
761 table: table.into(),
762 timing: "BEFORE".to_string(),
763 events: vec!["INSERT".to_string()],
764 update_columns: Vec::new(),
765 for_each_row: true,
766 execute_function: execute_function.into(),
767 condition: None,
768 }
769 }
770
771 pub fn timing(mut self, t: impl Into<String>) -> Self {
773 self.timing = t.into();
774 self
775 }
776
777 pub fn events(mut self, evts: Vec<String>) -> Self {
779 self.events = evts;
780 self
781 }
782
783 pub fn for_each_statement(mut self) -> Self {
785 self.for_each_row = false;
786 self
787 }
788
789 pub fn condition(mut self, cond: impl Into<String>) -> Self {
791 self.condition = Some(cond.into());
792 self
793 }
794}
795
796#[derive(Debug, Clone, PartialEq)]
798pub struct Grant {
799 pub action: GrantAction,
801 pub privileges: Vec<Privilege>,
803 pub on_object: String,
805 pub to_role: String,
807}
808
809#[derive(Debug, Clone, PartialEq, Default)]
811pub enum GrantAction {
812 #[default]
813 Grant,
815 Revoke,
817}
818
819#[derive(Debug, Clone, PartialEq)]
821pub enum Privilege {
822 All,
824 Select,
826 Insert,
828 Update,
830 Delete,
832 Usage,
834 Execute,
836}
837
838impl std::fmt::Display for Privilege {
839 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
840 match self {
841 Privilege::All => write!(f, "ALL"),
842 Privilege::Select => write!(f, "SELECT"),
843 Privilege::Insert => write!(f, "INSERT"),
844 Privilege::Update => write!(f, "UPDATE"),
845 Privilege::Delete => write!(f, "DELETE"),
846 Privilege::Usage => write!(f, "USAGE"),
847 Privilege::Execute => write!(f, "EXECUTE"),
848 }
849 }
850}
851
852impl Grant {
853 pub fn new(
855 privileges: Vec<Privilege>,
856 on_object: impl Into<String>,
857 to_role: impl Into<String>,
858 ) -> Self {
859 Self {
860 action: GrantAction::Grant,
861 privileges,
862 on_object: on_object.into(),
863 to_role: to_role.into(),
864 }
865 }
866
867 pub fn revoke(
869 privileges: Vec<Privilege>,
870 on_object: impl Into<String>,
871 from_role: impl Into<String>,
872 ) -> Self {
873 Self {
874 action: GrantAction::Revoke,
875 privileges,
876 on_object: on_object.into(),
877 to_role: from_role.into(),
878 }
879 }
880}
881
882impl Schema {
883 pub fn new() -> Self {
885 Self::default()
886 }
887
888 pub fn add_table(&mut self, table: Table) {
890 self.tables.insert(table.name.clone(), table);
891 }
892
893 pub fn add_index(&mut self, index: Index) {
895 self.indexes.push(index);
896 }
897
898 pub fn add_hint(&mut self, hint: MigrationHint) {
900 self.migrations.push(hint);
901 }
902
903 pub fn add_extension(&mut self, ext: Extension) {
905 self.extensions.push(ext);
906 }
907
908 pub fn add_comment(&mut self, comment: Comment) {
910 self.comments.push(comment);
911 }
912
913 pub fn add_sequence(&mut self, seq: Sequence) {
915 self.sequences.push(seq);
916 }
917
918 pub fn add_enum(&mut self, enum_type: EnumType) {
920 self.enums.push(enum_type);
921 }
922
923 pub fn add_view(&mut self, view: ViewDef) {
925 self.views.push(view);
926 }
927
928 pub fn add_function(&mut self, func: SchemaFunctionDef) {
930 self.functions.push(func);
931 }
932
933 pub fn add_trigger(&mut self, trigger: SchemaTriggerDef) {
935 self.triggers.push(trigger);
936 }
937
938 pub fn add_grant(&mut self, grant: Grant) {
940 self.grants.push(grant);
941 }
942
943 pub fn add_resource(&mut self, resource: ResourceDef) {
945 self.resources.push(resource);
946 }
947
948 pub fn add_policy(&mut self, policy: RlsPolicy) {
950 self.policies.push(policy);
951 }
952
953 pub fn validate(&self) -> Result<(), Vec<String>> {
955 let mut errors = Vec::new();
956
957 for table in self.tables.values() {
958 for col in &table.columns {
959 if let Some(ref fk) = col.foreign_key {
960 if !self.tables.contains_key(&fk.table) {
961 errors.push(format!(
962 "FK error: {}.{} references non-existent table '{}'",
963 table.name, col.name, fk.table
964 ));
965 } else {
966 let ref_table = &self.tables[&fk.table];
967 if !ref_table.columns.iter().any(|c| c.name == fk.column) {
968 errors.push(format!(
969 "FK error: {}.{} references non-existent column '{}.{}'",
970 table.name, col.name, fk.table, fk.column
971 ));
972 }
973 }
974 }
975 }
976 }
977
978 if errors.is_empty() {
979 Ok(())
980 } else {
981 Err(errors)
982 }
983 }
984}
985
986impl Table {
987 pub fn new(name: impl Into<String>) -> Self {
989 Self {
990 name: name.into(),
991 columns: Vec::new(),
992 multi_column_fks: Vec::new(),
993 enable_rls: false,
994 force_rls: false,
995 }
996 }
997
998 pub fn column(mut self, col: Column) -> Self {
1000 self.columns.push(col);
1001 self
1002 }
1003
1004 pub fn foreign_key(mut self, fk: MultiColumnForeignKey) -> Self {
1006 self.multi_column_fks.push(fk);
1007 self
1008 }
1009}
1010
1011impl Column {
1012 fn primary_key_type_error(&self) -> String {
1013 format!(
1014 "Column '{}' of type {} cannot be a primary key. \
1015 Valid PK types: scalar/indexable types \
1016 (UUID, TEXT, VARCHAR, INT, BIGINT, SERIAL, BIGSERIAL, BOOLEAN, FLOAT, DECIMAL, \
1017 TIMESTAMP, TIMESTAMPTZ, DATE, TIME, ENUM, INET, CIDR, MACADDR)",
1018 self.name,
1019 self.data_type.name()
1020 )
1021 }
1022
1023 fn unique_type_error(&self) -> String {
1024 format!(
1025 "Column '{}' of type {} cannot have UNIQUE constraint. \
1026 JSONB and BYTEA types do not support standard indexing.",
1027 self.name,
1028 self.data_type.name()
1029 )
1030 }
1031
1032 pub fn new(name: impl Into<String>, data_type: ColumnType) -> Self {
1034 Self {
1035 name: name.into(),
1036 data_type,
1037 nullable: true,
1038 primary_key: false,
1039 unique: false,
1040 default: None,
1041 foreign_key: None,
1042 check: None,
1043 generated: None,
1044 }
1045 }
1046
1047 pub fn not_null(mut self) -> Self {
1049 self.nullable = false;
1050 self
1051 }
1052
1053 pub fn primary_key(mut self) -> Self {
1060 if !self.data_type.can_be_primary_key() {
1061 #[cfg(debug_assertions)]
1062 eprintln!("QAIL: {}", self.primary_key_type_error());
1063 }
1064 self.primary_key = true;
1065 self.nullable = false;
1066 self
1067 }
1068
1069 pub fn try_primary_key(mut self) -> Result<Self, String> {
1073 if !self.data_type.can_be_primary_key() {
1074 return Err(self.primary_key_type_error());
1075 }
1076 self.primary_key = true;
1077 self.nullable = false;
1078 Ok(self)
1079 }
1080
1081 pub fn unique(mut self) -> Self {
1088 if !self.data_type.supports_indexing() {
1089 #[cfg(debug_assertions)]
1090 eprintln!("QAIL: {}", self.unique_type_error());
1091 }
1092 self.unique = true;
1093 self
1094 }
1095
1096 pub fn try_unique(mut self) -> Result<Self, String> {
1100 if !self.data_type.supports_indexing() {
1101 return Err(self.unique_type_error());
1102 }
1103 self.unique = true;
1104 Ok(self)
1105 }
1106
1107 pub fn default(mut self, val: impl Into<String>) -> Self {
1109 self.default = Some(val.into());
1110 self
1111 }
1112
1113 pub fn references(mut self, table: &str, column: &str) -> Self {
1121 self.foreign_key = Some(ForeignKey {
1122 table: table.to_string(),
1123 column: column.to_string(),
1124 on_delete: FkAction::default(),
1125 on_update: FkAction::default(),
1126 deferrable: Deferrable::default(),
1127 });
1128 self
1129 }
1130
1131 pub fn on_delete(mut self, action: FkAction) -> Self {
1133 if let Some(ref mut fk) = self.foreign_key {
1134 fk.on_delete = action;
1135 }
1136 self
1137 }
1138
1139 pub fn on_update(mut self, action: FkAction) -> Self {
1141 if let Some(ref mut fk) = self.foreign_key {
1142 fk.on_update = action;
1143 }
1144 self
1145 }
1146
1147 pub fn check(mut self, expr: CheckExpr) -> Self {
1151 self.check = Some(CheckConstraint { expr, name: None });
1152 self
1153 }
1154
1155 pub fn check_named(mut self, name: impl Into<String>, expr: CheckExpr) -> Self {
1157 self.check = Some(CheckConstraint {
1158 expr,
1159 name: Some(name.into()),
1160 });
1161 self
1162 }
1163
1164 pub fn deferrable(mut self) -> Self {
1168 if let Some(ref mut fk) = self.foreign_key {
1169 fk.deferrable = Deferrable::Deferrable;
1170 }
1171 self
1172 }
1173
1174 pub fn initially_deferred(mut self) -> Self {
1176 if let Some(ref mut fk) = self.foreign_key {
1177 fk.deferrable = Deferrable::InitiallyDeferred;
1178 }
1179 self
1180 }
1181
1182 pub fn initially_immediate(mut self) -> Self {
1184 if let Some(ref mut fk) = self.foreign_key {
1185 fk.deferrable = Deferrable::InitiallyImmediate;
1186 }
1187 self
1188 }
1189
1190 pub fn generated_stored(mut self, expr: impl Into<String>) -> Self {
1194 self.generated = Some(Generated::AlwaysStored(expr.into()));
1195 self
1196 }
1197
1198 pub fn generated_identity(mut self) -> Self {
1200 self.generated = Some(Generated::AlwaysIdentity);
1201 self
1202 }
1203
1204 pub fn generated_by_default(mut self) -> Self {
1206 self.generated = Some(Generated::ByDefaultIdentity);
1207 self
1208 }
1209}
1210
1211impl Index {
1212 pub fn new(name: impl Into<String>, table: impl Into<String>, columns: Vec<String>) -> Self {
1214 Self {
1215 name: name.into(),
1216 table: table.into(),
1217 columns,
1218 unique: false,
1219 method: IndexMethod::default(),
1220 where_clause: None,
1221 include: Vec::new(),
1222 concurrently: false,
1223 expressions: Vec::new(),
1224 }
1225 }
1226
1227 pub fn expression(
1229 name: impl Into<String>,
1230 table: impl Into<String>,
1231 expressions: Vec<String>,
1232 ) -> Self {
1233 Self {
1234 name: name.into(),
1235 table: table.into(),
1236 columns: Vec::new(),
1237 unique: false,
1238 method: IndexMethod::default(),
1239 where_clause: None,
1240 include: Vec::new(),
1241 concurrently: false,
1242 expressions,
1243 }
1244 }
1245
1246 pub fn unique(mut self) -> Self {
1248 self.unique = true;
1249 self
1250 }
1251
1252 pub fn using(mut self, method: IndexMethod) -> Self {
1256 self.method = method;
1257 self
1258 }
1259
1260 pub fn partial(mut self, expr: CheckExpr) -> Self {
1262 self.where_clause = Some(expr);
1263 self
1264 }
1265
1266 pub fn include(mut self, cols: Vec<String>) -> Self {
1268 self.include = cols;
1269 self
1270 }
1271
1272 pub fn concurrently(mut self) -> Self {
1274 self.concurrently = true;
1275 self
1276 }
1277}
1278
1279fn fk_action_str(action: &FkAction) -> &'static str {
1282 match action {
1283 FkAction::NoAction => "no_action",
1284 FkAction::Cascade => "cascade",
1285 FkAction::SetNull => "set_null",
1286 FkAction::SetDefault => "set_default",
1287 FkAction::Restrict => "restrict",
1288 }
1289}
1290
1291fn format_qail_value_token(value: &str, extra_special: &[char]) -> String {
1292 let needs_quotes = value.is_empty()
1293 || value.chars().any(|ch| {
1294 ch.is_whitespace() || matches!(ch, ',' | '\'' | '"') || extra_special.contains(&ch)
1295 });
1296
1297 if needs_quotes {
1298 format!("\"{}\"", value.replace('"', "\"\""))
1299 } else {
1300 value.to_string()
1301 }
1302}
1303
1304fn format_check_in_value(value: &str) -> String {
1305 format_qail_value_token(value, &['[', ']'])
1306}
1307
1308fn check_expr_str(expr: &CheckExpr) -> String {
1310 match expr {
1311 CheckExpr::GreaterThan { column, value } => format!("{} > {}", column, value),
1312 CheckExpr::GreaterOrEqual { column, value } => format!("{} >= {}", column, value),
1313 CheckExpr::LessThan { column, value } => format!("{} < {}", column, value),
1314 CheckExpr::LessOrEqual { column, value } => format!("{} <= {}", column, value),
1315 CheckExpr::Between { column, low, high } => format!("{} between {} {}", column, low, high),
1316 CheckExpr::In { column, values } => format!(
1317 "{} in [{}]",
1318 column,
1319 values
1320 .iter()
1321 .map(|value| format_check_in_value(value))
1322 .collect::<Vec<_>>()
1323 .join(", ")
1324 ),
1325 CheckExpr::Regex { column, pattern } => {
1326 format!("{} ~ '{}'", column, pattern.replace('\'', "''"))
1327 }
1328 CheckExpr::MaxLength { column, max } => format!("length({}) <= {}", column, max),
1329 CheckExpr::MinLength { column, min } => format!("length({}) >= {}", column, min),
1330 CheckExpr::NotNull { column } => format!("{} not_null", column),
1331 CheckExpr::And(l, r) => format!("{} and {}", check_expr_str(l), check_expr_str(r)),
1332 CheckExpr::Or(l, r) => format!("{} or {}", check_expr_str(l), check_expr_str(r)),
1333 CheckExpr::Not(e) => format!("not {}", check_expr_str(e)),
1334 CheckExpr::Sql(sql) => sql.clone(),
1335 }
1336}
1337
1338fn format_enum_value(value: &str) -> String {
1339 format_qail_value_token(value, &['{', '}'])
1340}
1341
1342fn dollar_quote_qail_body(body: &str) -> String {
1343 let delimiter = if !body.contains("$$") {
1344 "$$".to_string()
1345 } else {
1346 let mut idx = 0usize;
1347 loop {
1348 let candidate = if idx == 0 {
1349 "$qail$".to_string()
1350 } else {
1351 format!("$qail{idx}$")
1352 };
1353 if !body.contains(&candidate) {
1354 break candidate;
1355 }
1356 idx = idx.saturating_add(1);
1357 }
1358 };
1359
1360 format!("{delimiter}\n{body}\n{delimiter}")
1361}
1362
1363pub fn to_qail_string(schema: &Schema) -> String {
1365 let mut output = String::new();
1366 output.push_str("# QAIL Schema\n\n");
1367
1368 for ext in &schema.extensions {
1370 let mut line = format!("extension {}", quote_qail_string(&ext.name));
1371 if let Some(ref s) = ext.schema {
1372 line.push_str(&format!(" schema {}", quote_qail_string(s)));
1373 }
1374 if let Some(ref v) = ext.version {
1375 line.push_str(&format!(" version {}", quote_qail_string(v)));
1376 }
1377 output.push_str(&line);
1378 output.push('\n');
1379 }
1380 if !schema.extensions.is_empty() {
1381 output.push('\n');
1382 }
1383
1384 for enum_type in &schema.enums {
1386 let values = enum_type
1387 .values
1388 .iter()
1389 .map(|v| format_enum_value(v))
1390 .collect::<Vec<_>>()
1391 .join(", ");
1392 output.push_str(&format!("enum {} {{ {} }}\n", enum_type.name, values));
1393 }
1394 if !schema.enums.is_empty() {
1395 output.push('\n');
1396 }
1397
1398 for seq in &schema.sequences {
1400 if seq.start.is_some()
1401 || seq.increment.is_some()
1402 || seq.min_value.is_some()
1403 || seq.max_value.is_some()
1404 || seq.cache.is_some()
1405 || seq.cycle
1406 || seq.owned_by.is_some()
1407 {
1408 let mut opts = Vec::new();
1409 if let Some(v) = seq.start {
1410 opts.push(format!("start {}", v));
1411 }
1412 if let Some(v) = seq.increment {
1413 opts.push(format!("increment {}", v));
1414 }
1415 if let Some(v) = seq.min_value {
1416 opts.push(format!("minvalue {}", v));
1417 }
1418 if let Some(v) = seq.max_value {
1419 opts.push(format!("maxvalue {}", v));
1420 }
1421 if let Some(v) = seq.cache {
1422 opts.push(format!("cache {}", v));
1423 }
1424 if seq.cycle {
1425 opts.push("cycle".to_string());
1426 }
1427 if let Some(ref o) = seq.owned_by {
1428 opts.push(format!("owned_by {}", o));
1429 }
1430 output.push_str(&format!("sequence {} {{ {} }}\n", seq.name, opts.join(" ")));
1431 } else {
1432 output.push_str(&format!("sequence {}\n", seq.name));
1433 }
1434 }
1435 if !schema.sequences.is_empty() {
1436 output.push('\n');
1437 }
1438
1439 let mut table_names: Vec<&String> = schema.tables.keys().collect();
1440 table_names.sort();
1441 for table_name in table_names {
1442 let table = &schema.tables[table_name];
1443 output.push_str(&format!("table {} {{\n", table.name));
1444 for col in &table.columns {
1445 let mut constraints: Vec<String> = Vec::new();
1446 if col.primary_key {
1447 constraints.push("primary_key".to_string());
1448 }
1449 if !col.nullable && !col.primary_key {
1450 constraints.push("not_null".to_string());
1451 }
1452 if col.unique {
1453 constraints.push("unique".to_string());
1454 }
1455 if let Some(def) = &col.default {
1456 constraints.push(format!("default {}", def));
1457 }
1458 if let Some(generated) = &col.generated {
1459 match generated {
1460 Generated::AlwaysStored(expr) => {
1461 constraints.push(format!("generated_stored({})", expr));
1462 }
1463 Generated::AlwaysIdentity => {
1464 constraints.push("generated_identity".to_string());
1465 }
1466 Generated::ByDefaultIdentity => {
1467 constraints.push("generated_by_default_identity".to_string());
1468 }
1469 }
1470 }
1471 if let Some(ref fk) = col.foreign_key {
1472 let mut fk_str = format!("references {}({})", fk.table, fk.column);
1473 if fk.on_delete != FkAction::NoAction {
1474 fk_str.push_str(&format!(" on_delete {}", fk_action_str(&fk.on_delete)));
1475 }
1476 if fk.on_update != FkAction::NoAction {
1477 fk_str.push_str(&format!(" on_update {}", fk_action_str(&fk.on_update)));
1478 }
1479 match &fk.deferrable {
1480 Deferrable::Deferrable => fk_str.push_str(" deferrable"),
1481 Deferrable::InitiallyDeferred => fk_str.push_str(" initially_deferred"),
1482 Deferrable::InitiallyImmediate => fk_str.push_str(" initially_immediate"),
1483 Deferrable::NotDeferrable => {} }
1485 constraints.push(fk_str);
1486 }
1487 if let Some(ref check) = col.check {
1488 constraints.push(format!("check({})", check_expr_str(&check.expr)));
1489 if let Some(name) = &check.name {
1490 constraints.push(format!("check_name {}", name));
1491 }
1492 }
1493
1494 let constraint_str = if constraints.is_empty() {
1495 String::new()
1496 } else {
1497 format!(" {}", constraints.join(" "))
1498 };
1499
1500 output.push_str(&format!(
1501 " {} {}{}\n",
1502 col.name,
1503 col.data_type.to_pg_type(),
1504 constraint_str
1505 ));
1506 }
1507 for fk in &table.multi_column_fks {
1509 output.push_str(&format!(
1510 " foreign_key ({}) references {}({})\n",
1511 fk.columns.join(", "),
1512 fk.ref_table,
1513 fk.ref_columns.join(", ")
1514 ));
1515 }
1516 if table.enable_rls {
1518 output.push_str(" enable_rls\n");
1519 }
1520 if table.force_rls {
1521 output.push_str(" force_rls\n");
1522 }
1523 output.push_str("}\n\n");
1524 }
1525
1526 for idx in &schema.indexes {
1527 let unique = if idx.unique { "unique " } else { "" };
1528 let cols = if !idx.expressions.is_empty() {
1529 idx.expressions.join(", ")
1530 } else {
1531 idx.columns.join(", ")
1532 };
1533 let mut line = format!("{}index {} on {}", unique, idx.name, idx.table);
1534 if idx.method != IndexMethod::BTree {
1535 line.push_str(" using ");
1536 line.push_str(index_method_str(&idx.method));
1537 }
1538 line.push_str(" (");
1539 line.push_str(&cols);
1540 line.push(')');
1541 if let Some(where_clause) = &idx.where_clause {
1542 line.push_str(" where ");
1543 line.push_str(&check_expr_str(where_clause));
1544 }
1545 output.push_str(&line);
1546 output.push('\n');
1547 }
1548
1549 for hint in &schema.migrations {
1550 match hint {
1551 MigrationHint::Rename { from, to } => {
1552 output.push_str(&format!("rename {} -> {}\n", from, to));
1553 }
1554 MigrationHint::Transform { expression, target } => {
1555 output.push_str(&format!("transform {} -> {}\n", expression, target));
1556 }
1557 MigrationHint::Drop { target, confirmed } => {
1558 let confirm = if *confirmed { " confirm" } else { "" };
1559 output.push_str(&format!("drop {}{}\n", target, confirm));
1560 }
1561 }
1562 }
1563
1564 for view in &schema.views {
1566 let prefix = if view.materialized {
1567 "materialized view"
1568 } else {
1569 "view"
1570 };
1571 let body = dollar_quote_qail_body(&view.query);
1572 output.push_str(&format!("{} {} {}\n\n", prefix, view.name, body));
1573 }
1574
1575 for func in &schema.functions {
1577 let args = func.args.join(", ");
1578 let volatility = func
1579 .volatility
1580 .as_deref()
1581 .filter(|v| !v.trim().is_empty())
1582 .map(|v| format!(" {}", v))
1583 .unwrap_or_default();
1584 let body = dollar_quote_qail_body(&func.body);
1585 output.push_str(&format!(
1586 "function {}({}) returns {} language {}{} {}\n\n",
1587 func.name, args, func.returns, func.language, volatility, body
1588 ));
1589 }
1590
1591 for trigger in &schema.triggers {
1593 let mut events = Vec::new();
1594 for evt in &trigger.events {
1595 if evt.eq_ignore_ascii_case("UPDATE") && !trigger.update_columns.is_empty() {
1596 events.push(format!("UPDATE OF {}", trigger.update_columns.join(", ")));
1597 } else {
1598 events.push(evt.clone());
1599 }
1600 }
1601 output.push_str(&format!(
1602 "trigger {} on {} {} {} execute {}\n",
1603 trigger.name,
1604 trigger.table,
1605 trigger.timing.to_lowercase(),
1606 events.join(" or ").to_lowercase(),
1607 trigger.execute_function
1608 ));
1609 }
1610 if !schema.triggers.is_empty() {
1611 output.push('\n');
1612 }
1613
1614 for policy in &schema.policies {
1616 let cmd = match policy.target {
1617 PolicyTarget::All => "all",
1618 PolicyTarget::Select => "select",
1619 PolicyTarget::Insert => "insert",
1620 PolicyTarget::Update => "update",
1621 PolicyTarget::Delete => "delete",
1622 };
1623 let perm = match policy.permissiveness {
1624 PolicyPermissiveness::Permissive => "",
1625 PolicyPermissiveness::Restrictive => " restrictive",
1626 };
1627 let role_str = match &policy.role {
1628 Some(r) => format!(" to {}", r),
1629 None => String::new(),
1630 };
1631 output.push_str(&format!(
1632 "policy {} on {} for {}{}{}",
1633 policy.name, policy.table, cmd, role_str, perm
1634 ));
1635 if let Some(ref using) = policy.using {
1636 output.push_str(&format!("\n using $$ {} $$", using));
1637 }
1638 if let Some(ref wc) = policy.with_check {
1639 output.push_str(&format!("\n with_check $$ {} $$", wc));
1640 }
1641 output.push_str("\n\n");
1642 }
1643
1644 for grant in &schema.grants {
1646 let privs: Vec<String> = grant
1647 .privileges
1648 .iter()
1649 .map(|p| p.to_string().to_lowercase())
1650 .collect();
1651 match grant.action {
1652 GrantAction::Grant => {
1653 output.push_str(&format!(
1654 "grant {} on {} to {}\n",
1655 privs.join(", "),
1656 grant.on_object,
1657 grant.to_role
1658 ));
1659 }
1660 GrantAction::Revoke => {
1661 output.push_str(&format!(
1662 "revoke {} on {} from {}\n",
1663 privs.join(", "),
1664 grant.on_object,
1665 grant.to_role
1666 ));
1667 }
1668 }
1669 }
1670 if !schema.grants.is_empty() {
1671 output.push('\n');
1672 }
1673
1674 for comment in &schema.comments {
1676 let text = quote_qail_string(&comment.text);
1677 match &comment.target {
1678 CommentTarget::Table(t) => {
1679 output.push_str(&format!("comment on {} {}\n", t, text));
1680 }
1681 CommentTarget::Column { table, column } => {
1682 output.push_str(&format!("comment on {}.{} {}\n", table, column, text));
1683 }
1684 CommentTarget::Raw(target) => {
1685 output.push_str(&format!("comment on {} {}\n", target, text));
1686 }
1687 }
1688 }
1689
1690 output
1691}
1692
1693fn quote_qail_string(value: &str) -> String {
1694 format!("\"{}\"", value.replace('"', "\"\""))
1695}
1696
1697pub fn schema_to_commands(schema: &Schema) -> Vec<crate::ast::Qail> {
1700 use crate::ast::{Action, ColumnGeneration, Constraint, Expr, IndexDef, Qail};
1701
1702 let mut cmds = Vec::new();
1703
1704 let mut indegree: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
1707 let mut reverse_adj: std::collections::HashMap<String, Vec<String>> =
1708 std::collections::HashMap::new();
1709
1710 for name in schema.tables.keys() {
1711 indegree.insert(name.clone(), 0);
1712 }
1713
1714 for table in schema.tables.values() {
1715 let mut deps = std::collections::HashSet::new();
1716 for col in &table.columns {
1717 if let Some(fk) = &col.foreign_key
1718 && fk.table != table.name
1719 && schema.tables.contains_key(&fk.table)
1720 {
1721 deps.insert(fk.table.clone());
1722 }
1723 }
1724 for fk in &table.multi_column_fks {
1725 if fk.ref_table != table.name && schema.tables.contains_key(&fk.ref_table) {
1726 deps.insert(fk.ref_table.clone());
1727 }
1728 }
1729
1730 indegree.insert(table.name.clone(), deps.len());
1731 for dep in deps {
1732 reverse_adj.entry(dep).or_default().push(table.name.clone());
1733 }
1734 }
1735
1736 let mut ready = std::collections::BTreeSet::new();
1737 for (name, deg) in &indegree {
1738 if *deg == 0 {
1739 ready.insert(name.clone());
1740 }
1741 }
1742
1743 let mut ordered_names: Vec<String> = Vec::with_capacity(schema.tables.len());
1744 while let Some(next) = ready.pop_first() {
1745 ordered_names.push(next.clone());
1746 if let Some(dependents) = reverse_adj.get(&next) {
1747 for dep_name in dependents {
1748 if let Some(d) = indegree.get_mut(dep_name)
1749 && *d > 0
1750 {
1751 *d -= 1;
1752 if *d == 0 {
1753 ready.insert(dep_name.clone());
1754 }
1755 }
1756 }
1757 }
1758 }
1759
1760 if ordered_names.len() < schema.tables.len() {
1763 let mut leftovers: Vec<String> = schema
1764 .tables
1765 .keys()
1766 .filter(|name| !ordered_names.contains(*name))
1767 .cloned()
1768 .collect();
1769 leftovers.sort();
1770 ordered_names.extend(leftovers);
1771 }
1772
1773 for table_name in ordered_names {
1774 let table = &schema.tables[&table_name];
1775 let columns: Vec<Expr> = table
1777 .columns
1778 .iter()
1779 .map(|col| {
1780 let mut constraints = Vec::new();
1781
1782 if col.primary_key {
1783 constraints.push(Constraint::PrimaryKey);
1784 }
1785 if col.nullable {
1786 constraints.push(Constraint::Nullable);
1787 }
1788 if col.unique {
1789 constraints.push(Constraint::Unique);
1790 }
1791 if let Some(def) = &col.default {
1792 constraints.push(Constraint::Default(def.clone()));
1793 }
1794 if let Some(ref fk) = col.foreign_key {
1795 constraints.push(Constraint::References(foreign_key_to_sql(fk)));
1796 }
1797 if let Some(check) = &col.check {
1798 let check_sql = check_expr_to_sql(&check.expr);
1799 if let Some(name) = &check.name {
1800 constraints.push(Constraint::Check(vec![format!(
1801 "CONSTRAINT {} CHECK ({})",
1802 name, check_sql
1803 )]));
1804 } else {
1805 constraints.push(Constraint::Check(vec![check_sql]));
1806 }
1807 }
1808 if let Some(generated) = &col.generated {
1809 let gen_constraint = match generated {
1810 Generated::AlwaysStored(expr) => {
1811 Constraint::Generated(ColumnGeneration::Stored(expr.clone()))
1812 }
1813 Generated::AlwaysIdentity => {
1814 Constraint::Generated(ColumnGeneration::Stored("identity".to_string()))
1815 }
1816 Generated::ByDefaultIdentity => Constraint::Generated(
1817 ColumnGeneration::Stored("identity_by_default".to_string()),
1818 ),
1819 };
1820 constraints.push(gen_constraint);
1821 }
1822
1823 Expr::Def {
1824 name: col.name.clone(),
1825 data_type: col.data_type.to_pg_type(),
1826 constraints,
1827 }
1828 })
1829 .collect();
1830
1831 cmds.push(Qail {
1832 action: Action::Make,
1833 table: table.name.clone(),
1834 columns,
1835 ..Default::default()
1836 });
1837 }
1838
1839 for idx in &schema.indexes {
1841 cmds.push(Qail {
1842 action: Action::Index,
1843 table: String::new(),
1844 index_def: Some(IndexDef {
1845 name: idx.name.clone(),
1846 table: idx.table.clone(),
1847 columns: if !idx.expressions.is_empty() {
1848 idx.expressions.clone()
1849 } else {
1850 idx.columns.clone()
1851 },
1852 unique: idx.unique,
1853 index_type: Some(index_method_str(&idx.method).to_string()),
1854 where_clause: idx.where_clause.as_ref().map(check_expr_to_sql),
1855 }),
1856 ..Default::default()
1857 });
1858 }
1859
1860 let mut fk_table_names: Vec<&String> = schema
1861 .tables
1862 .iter()
1863 .filter(|(_, table)| !table.multi_column_fks.is_empty())
1864 .map(|(name, _)| name)
1865 .collect();
1866 fk_table_names.sort();
1867 for table_name in fk_table_names {
1868 let table = &schema.tables[table_name];
1869 for fk in &table.multi_column_fks {
1870 cmds.push(multi_column_fk_to_alter_command(&table.name, fk));
1871 }
1872 }
1873
1874 cmds
1875}
1876
1877pub(super) fn multi_column_fk_to_table_constraint(
1878 fk: &MultiColumnForeignKey,
1879) -> crate::ast::TableConstraint {
1880 crate::ast::TableConstraint::ForeignKey {
1881 name: fk.name.clone(),
1882 columns: fk.columns.clone(),
1883 ref_table: fk.ref_table.clone(),
1884 ref_columns: fk.ref_columns.clone(),
1885 }
1886}
1887
1888pub(super) fn multi_column_fk_to_alter_command(
1889 table_name: &str,
1890 fk: &MultiColumnForeignKey,
1891) -> crate::ast::Qail {
1892 crate::ast::Qail {
1893 action: crate::ast::Action::Alter,
1894 table: table_name.to_string(),
1895 table_constraints: vec![multi_column_fk_to_table_constraint(fk)],
1896 ..Default::default()
1897 }
1898}
1899
1900fn fk_action_to_sql(action: &FkAction) -> &'static str {
1901 match action {
1902 FkAction::NoAction => "NO ACTION",
1903 FkAction::Cascade => "CASCADE",
1904 FkAction::SetNull => "SET NULL",
1905 FkAction::SetDefault => "SET DEFAULT",
1906 FkAction::Restrict => "RESTRICT",
1907 }
1908}
1909
1910fn deferrable_to_sql(deferrable: &Deferrable) -> Option<&'static str> {
1911 match deferrable {
1912 Deferrable::NotDeferrable => None,
1913 Deferrable::Deferrable => Some("DEFERRABLE"),
1914 Deferrable::InitiallyDeferred => Some("DEFERRABLE INITIALLY DEFERRED"),
1915 Deferrable::InitiallyImmediate => Some("DEFERRABLE INITIALLY IMMEDIATE"),
1916 }
1917}
1918
1919pub(crate) fn foreign_key_to_sql(fk: &ForeignKey) -> String {
1920 let mut target = format!("{}({})", fk.table, fk.column);
1921 if fk.on_delete != FkAction::NoAction {
1922 target.push_str(" ON DELETE ");
1923 target.push_str(fk_action_to_sql(&fk.on_delete));
1924 }
1925 if fk.on_update != FkAction::NoAction {
1926 target.push_str(" ON UPDATE ");
1927 target.push_str(fk_action_to_sql(&fk.on_update));
1928 }
1929 if let Some(def) = deferrable_to_sql(&fk.deferrable) {
1930 target.push(' ');
1931 target.push_str(def);
1932 }
1933 target
1934}
1935
1936pub(crate) fn check_expr_to_sql(expr: &CheckExpr) -> String {
1937 match expr {
1938 CheckExpr::GreaterThan { column, value } => format!("{column} > {value}"),
1939 CheckExpr::GreaterOrEqual { column, value } => format!("{column} >= {value}"),
1940 CheckExpr::LessThan { column, value } => format!("{column} < {value}"),
1941 CheckExpr::LessOrEqual { column, value } => format!("{column} <= {value}"),
1942 CheckExpr::Between { column, low, high } => format!("{column} BETWEEN {low} AND {high}"),
1943 CheckExpr::In { column, values } => {
1944 if values.len() == 1 && looks_like_raw_check_expr(&values[0]) {
1945 return values[0].clone();
1946 }
1947 let quoted = values
1948 .iter()
1949 .map(|v| format!("'{}'", v.replace('\'', "''")))
1950 .collect::<Vec<_>>()
1951 .join(", ");
1952 format!("{column} IN ({quoted})")
1953 }
1954 CheckExpr::Regex { column, pattern } => {
1955 format!("{column} ~ '{}'", pattern.replace('\'', "''"))
1956 }
1957 CheckExpr::MaxLength { column, max } => format!("char_length({column}) <= {max}"),
1958 CheckExpr::MinLength { column, min } => format!("char_length({column}) >= {min}"),
1959 CheckExpr::NotNull { column } => format!("{column} IS NOT NULL"),
1960 CheckExpr::And(left, right) => {
1961 format!(
1962 "({}) AND ({})",
1963 check_expr_to_sql(left),
1964 check_expr_to_sql(right)
1965 )
1966 }
1967 CheckExpr::Or(left, right) => {
1968 format!(
1969 "({}) OR ({})",
1970 check_expr_to_sql(left),
1971 check_expr_to_sql(right)
1972 )
1973 }
1974 CheckExpr::Not(inner) => format!("NOT ({})", check_expr_to_sql(inner)),
1975 CheckExpr::Sql(sql) => sql.clone(),
1976 }
1977}
1978
1979fn looks_like_raw_check_expr(s: &str) -> bool {
1980 s.chars()
1981 .any(|c| c.is_whitespace() || matches!(c, '<' | '>' | '=' | '!' | '(' | ')' | ':'))
1982}
1983
1984#[cfg(test)]
1985mod tests {
1986 use super::*;
1987
1988 #[test]
1989 fn test_schema_builder() {
1990 let mut schema = Schema::new();
1991
1992 let users = Table::new("users")
1993 .column(Column::new("id", ColumnType::Serial).primary_key())
1994 .column(Column::new("name", ColumnType::Text).not_null())
1995 .column(Column::new("email", ColumnType::Text).unique());
1996
1997 schema.add_table(users);
1998 schema.add_index(Index::new("idx_users_email", "users", vec!["email".into()]).unique());
1999
2000 let output = to_qail_string(&schema);
2001 assert!(output.contains("table users"));
2002 assert!(output.contains("id SERIAL primary_key"));
2003 assert!(output.contains("unique index idx_users_email"));
2004 }
2005
2006 #[test]
2007 fn test_to_qail_string_preserves_vector_index_methods() {
2008 let mut schema = Schema::new();
2009 schema.add_index(
2010 Index::new(
2011 "idx_docs_embedding_hnsw",
2012 "documents",
2013 vec!["embedding vector_l2_ops".into()],
2014 )
2015 .using(IndexMethod::Hnsw),
2016 );
2017 schema.add_index(
2018 Index::new(
2019 "idx_docs_embedding_ivfflat",
2020 "documents",
2021 vec!["embedding vector_cosine_ops".into()],
2022 )
2023 .using(IndexMethod::IvfFlat),
2024 );
2025
2026 let output = to_qail_string(&schema);
2027
2028 assert!(output.contains(
2029 "index idx_docs_embedding_hnsw on documents using hnsw (embedding vector_l2_ops)"
2030 ));
2031 assert!(output.contains(
2032 "index idx_docs_embedding_ivfflat on documents using ivfflat (embedding vector_cosine_ops)"
2033 ));
2034 }
2035
2036 #[test]
2037 fn test_migration_hints() {
2038 let mut schema = Schema::new();
2039 schema.add_hint(MigrationHint::Rename {
2040 from: "users.username".into(),
2041 to: "users.name".into(),
2042 });
2043
2044 let output = to_qail_string(&schema);
2045 assert!(output.contains("rename users.username -> users.name"));
2046 }
2047
2048 #[test]
2049 fn test_to_qail_string_includes_function_volatility() {
2050 let mut schema = Schema::new();
2051 let func = SchemaFunctionDef::new(
2052 "is_super_admin",
2053 "boolean",
2054 "BEGIN RETURN true; END;".to_string(),
2055 )
2056 .language("plpgsql")
2057 .volatility("stable");
2058 schema.add_function(func);
2059
2060 let output = to_qail_string(&schema);
2061 assert!(
2062 output.contains("function is_super_admin() returns boolean language plpgsql stable $$")
2063 );
2064 }
2065
2066 #[test]
2067 fn test_invalid_primary_key_type_strict() {
2068 let err = Column::new("data", ColumnType::Jsonb)
2069 .try_primary_key()
2070 .expect_err("JSONB should be rejected by strict PK policy");
2071 assert!(err.contains("cannot be a primary key"));
2072 }
2073
2074 #[test]
2075 fn test_invalid_primary_key_type_fail_soft() {
2076 let col = Column::new("data", ColumnType::Jsonb).primary_key();
2077 assert!(col.primary_key);
2078 assert!(!col.nullable);
2079 }
2080
2081 #[test]
2082 fn test_invalid_unique_type_strict() {
2083 let err = Column::new("data", ColumnType::Jsonb)
2084 .try_unique()
2085 .expect_err("JSONB should be rejected by strict UNIQUE policy");
2086 assert!(err.contains("cannot have UNIQUE"));
2087 }
2088
2089 #[test]
2090 fn test_invalid_unique_type_fail_soft() {
2091 let col = Column::new("data", ColumnType::Jsonb).unique();
2092 assert!(col.unique);
2093 }
2094
2095 #[test]
2096 fn test_foreign_key_valid() {
2097 let mut schema = Schema::new();
2098
2099 schema.add_table(
2100 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
2101 );
2102
2103 schema.add_table(
2104 Table::new("posts")
2105 .column(Column::new("id", ColumnType::Uuid).primary_key())
2106 .column(
2107 Column::new("user_id", ColumnType::Uuid)
2108 .references("users", "id")
2109 .on_delete(FkAction::Cascade),
2110 ),
2111 );
2112
2113 assert!(schema.validate().is_ok());
2115 }
2116
2117 #[test]
2118 fn test_foreign_key_invalid_table() {
2119 let mut schema = Schema::new();
2120
2121 schema.add_table(
2122 Table::new("posts")
2123 .column(Column::new("id", ColumnType::Uuid).primary_key())
2124 .column(Column::new("user_id", ColumnType::Uuid).references("nonexistent", "id")),
2125 );
2126
2127 let result = schema.validate();
2129 assert!(result.is_err());
2130 assert!(result.unwrap_err()[0].contains("non-existent table"));
2131 }
2132
2133 #[test]
2134 fn test_foreign_key_invalid_column() {
2135 let mut schema = Schema::new();
2136
2137 schema.add_table(
2138 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
2139 );
2140
2141 schema.add_table(
2142 Table::new("posts")
2143 .column(Column::new("id", ColumnType::Uuid).primary_key())
2144 .column(
2145 Column::new("user_id", ColumnType::Uuid).references("users", "wrong_column"),
2146 ),
2147 );
2148
2149 let result = schema.validate();
2151 assert!(result.is_err());
2152 assert!(result.unwrap_err()[0].contains("non-existent column"));
2153 }
2154
2155 #[test]
2156 fn test_schema_to_commands_preserves_fk_actions_and_checks() {
2157 let mut schema = Schema::new();
2158 schema.add_table(
2159 Table::new("orgs").column(Column::new("id", ColumnType::Uuid).primary_key()),
2160 );
2161 schema.add_table(
2162 Table::new("users")
2163 .column(Column::new("id", ColumnType::Uuid).primary_key())
2164 .column(
2165 Column::new("org_id", ColumnType::Uuid)
2166 .references("orgs", "id")
2167 .on_delete(FkAction::Cascade)
2168 .on_update(FkAction::Restrict),
2169 )
2170 .column(
2171 Column::new("age", ColumnType::Int).check(CheckExpr::GreaterOrEqual {
2172 column: "age".to_string(),
2173 value: 18,
2174 }),
2175 ),
2176 );
2177
2178 let cmds = schema_to_commands(&schema);
2179 let users_cmd = cmds
2180 .iter()
2181 .find(|c| c.action == crate::ast::Action::Make && c.table == "users")
2182 .expect("users create command should exist");
2183 let org_id_constraints = users_cmd
2184 .columns
2185 .iter()
2186 .find_map(|e| match e {
2187 crate::ast::Expr::Def {
2188 name, constraints, ..
2189 } if name == "org_id" => Some(constraints),
2190 _ => None,
2191 })
2192 .expect("org_id should exist");
2193 let age_constraints = users_cmd
2194 .columns
2195 .iter()
2196 .find_map(|e| match e {
2197 crate::ast::Expr::Def {
2198 name, constraints, ..
2199 } if name == "age" => Some(constraints),
2200 _ => None,
2201 })
2202 .expect("age should exist");
2203
2204 assert!(
2205 org_id_constraints.iter().any(|c| matches!(
2206 c,
2207 crate::ast::Constraint::References(target)
2208 if target.contains("orgs(id)")
2209 && target.contains("ON DELETE CASCADE")
2210 && target.contains("ON UPDATE RESTRICT")
2211 )),
2212 "foreign key action clauses should be preserved"
2213 );
2214 assert!(
2215 age_constraints
2216 .iter()
2217 .any(|c| matches!(c, crate::ast::Constraint::Check(vals) if vals.len() == 1)),
2218 "check expressions should be preserved"
2219 );
2220 }
2221
2222 #[test]
2223 fn schema_to_commands_preserves_multi_column_foreign_keys() {
2224 use crate::transpiler::ToSql;
2225
2226 let mut schema = Schema::new();
2227 schema.add_table(
2228 Table::new("schedules")
2229 .column(Column::new("route_id", ColumnType::Text))
2230 .column(Column::new("schedule_id", ColumnType::Text)),
2231 );
2232 schema.add_index(
2233 Index::new(
2234 "idx_schedules_route_schedule",
2235 "schedules",
2236 vec!["route_id".to_string(), "schedule_id".to_string()],
2237 )
2238 .unique(),
2239 );
2240 schema.add_table(
2241 Table::new("trips")
2242 .column(Column::new("route_id", ColumnType::Text))
2243 .column(Column::new("schedule_id", ColumnType::Text))
2244 .foreign_key(MultiColumnForeignKey::new(
2245 vec!["route_id".to_string(), "schedule_id".to_string()],
2246 "schedules",
2247 vec!["route_id".to_string(), "schedule_id".to_string()],
2248 )),
2249 );
2250
2251 let cmds = schema_to_commands(&schema);
2252 let schedules_idx = cmds
2253 .iter()
2254 .position(|c| c.action == crate::ast::Action::Make && c.table == "schedules")
2255 .expect("schedules create command should exist");
2256 let trips_idx = cmds
2257 .iter()
2258 .position(|c| c.action == crate::ast::Action::Make && c.table == "trips")
2259 .expect("trips create command should exist");
2260 let unique_idx = cmds
2261 .iter()
2262 .position(|c| {
2263 c.action == crate::ast::Action::Index
2264 && c.index_def
2265 .as_ref()
2266 .is_some_and(|idx| idx.name == "idx_schedules_route_schedule")
2267 })
2268 .expect("unique index command should exist");
2269 let add_fk_idx = cmds
2270 .iter()
2271 .position(|c| c.action == crate::ast::Action::Alter && c.table == "trips")
2272 .expect("trips composite foreign key ALTER command should exist");
2273
2274 assert!(schedules_idx < unique_idx);
2275 assert!(trips_idx < unique_idx);
2276 assert!(unique_idx < add_fk_idx);
2277
2278 let trips_cmd = cmds
2279 .iter()
2280 .find(|c| c.action == crate::ast::Action::Make && c.table == "trips")
2281 .expect("trips create command should exist");
2282 assert!(
2283 trips_cmd.table_constraints.is_empty(),
2284 "composite foreign keys should not be emitted inline on CREATE TABLE"
2285 );
2286
2287 let add_fk_cmd = &cmds[add_fk_idx];
2288 assert!(
2289 add_fk_cmd
2290 .table_constraints
2291 .iter()
2292 .any(|constraint| matches!(
2293 constraint,
2294 crate::ast::TableConstraint::ForeignKey {
2295 columns,
2296 ref_table,
2297 ref_columns,
2298 ..
2299 } if columns == &["route_id", "schedule_id"]
2300 && ref_table == "schedules"
2301 && ref_columns == &["route_id", "schedule_id"]
2302 )),
2303 "multi-column foreign key should be represented in generated commands"
2304 );
2305
2306 let sql = add_fk_cmd.to_sql();
2307 assert!(
2308 sql.contains(
2309 "ALTER TABLE trips ADD FOREIGN KEY (route_id, schedule_id) REFERENCES schedules(route_id, schedule_id)"
2310 ),
2311 "generated SQL should include composite foreign key, got: {sql}"
2312 );
2313 }
2314}