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 pub fn deferrable(mut self) -> Self {
647 self.deferrable = Deferrable::Deferrable;
648 self
649 }
650
651 pub fn initially_deferred(mut self) -> Self {
653 self.deferrable = Deferrable::InitiallyDeferred;
654 self
655 }
656
657 pub fn initially_immediate(mut self) -> Self {
659 self.deferrable = Deferrable::InitiallyImmediate;
660 self
661 }
662}
663
664#[derive(Debug, Clone, PartialEq)]
670pub struct ViewDef {
671 pub name: String,
673 pub query: String,
675 pub materialized: bool,
677}
678
679impl ViewDef {
680 pub fn new(name: impl Into<String>, query: impl Into<String>) -> Self {
682 Self {
683 name: name.into(),
684 query: query.into(),
685 materialized: false,
686 }
687 }
688
689 pub fn materialized(mut self) -> Self {
691 self.materialized = true;
692 self
693 }
694}
695
696#[derive(Debug, Clone, PartialEq)]
698pub struct SchemaFunctionDef {
699 pub name: String,
701 pub args: Vec<String>,
703 pub returns: String,
705 pub body: String,
707 pub language: String,
709 pub volatility: Option<String>,
711}
712
713impl SchemaFunctionDef {
714 pub fn new(
716 name: impl Into<String>,
717 returns: impl Into<String>,
718 body: impl Into<String>,
719 ) -> Self {
720 Self {
721 name: name.into(),
722 args: Vec::new(),
723 returns: returns.into(),
724 body: body.into(),
725 language: "plpgsql".to_string(),
726 volatility: None,
727 }
728 }
729
730 pub fn language(mut self, lang: impl Into<String>) -> Self {
732 self.language = lang.into();
733 self
734 }
735
736 pub fn arg(mut self, arg: impl Into<String>) -> Self {
738 self.args.push(arg.into());
739 self
740 }
741
742 pub fn volatility(mut self, v: impl Into<String>) -> Self {
744 self.volatility = Some(v.into());
745 self
746 }
747}
748
749#[derive(Debug, Clone, PartialEq)]
751pub struct SchemaTriggerDef {
752 pub name: String,
754 pub table: String,
756 pub timing: String,
758 pub events: Vec<String>,
760 pub update_columns: Vec<String>,
762 pub for_each_row: bool,
764 pub execute_function: String,
766 pub condition: Option<String>,
768}
769
770impl SchemaTriggerDef {
771 pub fn new(
773 name: impl Into<String>,
774 table: impl Into<String>,
775 execute_function: impl Into<String>,
776 ) -> Self {
777 Self {
778 name: name.into(),
779 table: table.into(),
780 timing: "BEFORE".to_string(),
781 events: vec!["INSERT".to_string()],
782 update_columns: Vec::new(),
783 for_each_row: true,
784 execute_function: execute_function.into(),
785 condition: None,
786 }
787 }
788
789 pub fn timing(mut self, t: impl Into<String>) -> Self {
791 self.timing = t.into();
792 self
793 }
794
795 pub fn events(mut self, evts: Vec<String>) -> Self {
797 self.events = evts;
798 self
799 }
800
801 pub fn for_each_statement(mut self) -> Self {
803 self.for_each_row = false;
804 self
805 }
806
807 pub fn condition(mut self, cond: impl Into<String>) -> Self {
809 self.condition = Some(cond.into());
810 self
811 }
812}
813
814#[derive(Debug, Clone, PartialEq)]
816pub struct Grant {
817 pub action: GrantAction,
819 pub privileges: Vec<Privilege>,
821 pub on_object: String,
823 pub to_role: String,
825}
826
827#[derive(Debug, Clone, PartialEq, Default)]
829pub enum GrantAction {
830 #[default]
831 Grant,
833 Revoke,
835}
836
837#[derive(Debug, Clone, PartialEq)]
839pub enum Privilege {
840 All,
842 Select,
844 Insert,
846 Update,
848 Delete,
850 Usage,
852 Execute,
854}
855
856impl std::fmt::Display for Privilege {
857 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
858 match self {
859 Privilege::All => write!(f, "ALL"),
860 Privilege::Select => write!(f, "SELECT"),
861 Privilege::Insert => write!(f, "INSERT"),
862 Privilege::Update => write!(f, "UPDATE"),
863 Privilege::Delete => write!(f, "DELETE"),
864 Privilege::Usage => write!(f, "USAGE"),
865 Privilege::Execute => write!(f, "EXECUTE"),
866 }
867 }
868}
869
870impl Grant {
871 pub fn new(
873 privileges: Vec<Privilege>,
874 on_object: impl Into<String>,
875 to_role: impl Into<String>,
876 ) -> Self {
877 Self {
878 action: GrantAction::Grant,
879 privileges,
880 on_object: on_object.into(),
881 to_role: to_role.into(),
882 }
883 }
884
885 pub fn revoke(
887 privileges: Vec<Privilege>,
888 on_object: impl Into<String>,
889 from_role: impl Into<String>,
890 ) -> Self {
891 Self {
892 action: GrantAction::Revoke,
893 privileges,
894 on_object: on_object.into(),
895 to_role: from_role.into(),
896 }
897 }
898}
899
900impl Schema {
901 pub fn new() -> Self {
903 Self::default()
904 }
905
906 pub fn add_table(&mut self, table: Table) {
908 self.tables.insert(table.name.clone(), table);
909 }
910
911 pub fn add_index(&mut self, index: Index) {
913 self.indexes.push(index);
914 }
915
916 pub fn add_hint(&mut self, hint: MigrationHint) {
918 self.migrations.push(hint);
919 }
920
921 pub fn add_extension(&mut self, ext: Extension) {
923 self.extensions.push(ext);
924 }
925
926 pub fn add_comment(&mut self, comment: Comment) {
928 self.comments.push(comment);
929 }
930
931 pub fn add_sequence(&mut self, seq: Sequence) {
933 self.sequences.push(seq);
934 }
935
936 pub fn add_enum(&mut self, enum_type: EnumType) {
938 self.enums.push(enum_type);
939 }
940
941 pub fn add_view(&mut self, view: ViewDef) {
943 self.views.push(view);
944 }
945
946 pub fn add_function(&mut self, func: SchemaFunctionDef) {
948 self.functions.push(func);
949 }
950
951 pub fn add_trigger(&mut self, trigger: SchemaTriggerDef) {
953 self.triggers.push(trigger);
954 }
955
956 pub fn add_grant(&mut self, grant: Grant) {
958 self.grants.push(grant);
959 }
960
961 pub fn add_resource(&mut self, resource: ResourceDef) {
963 self.resources.push(resource);
964 }
965
966 pub fn add_policy(&mut self, policy: RlsPolicy) {
968 self.policies.push(policy);
969 }
970
971 pub fn validate(&self) -> Result<(), Vec<String>> {
973 let mut errors = Vec::new();
974
975 for table in self.tables.values() {
976 for col in &table.columns {
977 if let Some(ref fk) = col.foreign_key {
978 if !self.tables.contains_key(&fk.table) {
979 errors.push(format!(
980 "FK error: {}.{} references non-existent table '{}'",
981 table.name, col.name, fk.table
982 ));
983 } else {
984 let ref_table = &self.tables[&fk.table];
985 if !ref_table.columns.iter().any(|c| c.name == fk.column) {
986 errors.push(format!(
987 "FK error: {}.{} references non-existent column '{}.{}'",
988 table.name, col.name, fk.table, fk.column
989 ));
990 }
991 }
992 }
993 }
994 }
995
996 if errors.is_empty() {
997 Ok(())
998 } else {
999 Err(errors)
1000 }
1001 }
1002}
1003
1004impl Table {
1005 pub fn new(name: impl Into<String>) -> Self {
1007 Self {
1008 name: name.into(),
1009 columns: Vec::new(),
1010 multi_column_fks: Vec::new(),
1011 enable_rls: false,
1012 force_rls: false,
1013 }
1014 }
1015
1016 pub fn column(mut self, col: Column) -> Self {
1018 self.columns.push(col);
1019 self
1020 }
1021
1022 pub fn foreign_key(mut self, fk: MultiColumnForeignKey) -> Self {
1024 self.multi_column_fks.push(fk);
1025 self
1026 }
1027}
1028
1029impl Column {
1030 fn primary_key_type_error(&self) -> String {
1031 format!(
1032 "Column '{}' of type {} cannot be a primary key. \
1033 Valid PK types: scalar/indexable types \
1034 (UUID, TEXT, VARCHAR, INT, BIGINT, SERIAL, BIGSERIAL, BOOLEAN, FLOAT, DECIMAL, \
1035 TIMESTAMP, TIMESTAMPTZ, DATE, TIME, ENUM, INET, CIDR, MACADDR)",
1036 self.name,
1037 self.data_type.name()
1038 )
1039 }
1040
1041 fn unique_type_error(&self) -> String {
1042 format!(
1043 "Column '{}' of type {} cannot have UNIQUE constraint. \
1044 JSONB and BYTEA types do not support standard indexing.",
1045 self.name,
1046 self.data_type.name()
1047 )
1048 }
1049
1050 pub fn new(name: impl Into<String>, data_type: ColumnType) -> Self {
1052 Self {
1053 name: name.into(),
1054 data_type,
1055 nullable: true,
1056 primary_key: false,
1057 unique: false,
1058 default: None,
1059 foreign_key: None,
1060 check: None,
1061 generated: None,
1062 }
1063 }
1064
1065 pub fn not_null(mut self) -> Self {
1067 self.nullable = false;
1068 self
1069 }
1070
1071 pub fn primary_key(mut self) -> Self {
1078 if !self.data_type.can_be_primary_key() {
1079 #[cfg(debug_assertions)]
1080 eprintln!("QAIL: {}", self.primary_key_type_error());
1081 }
1082 self.primary_key = true;
1083 self.nullable = false;
1084 self
1085 }
1086
1087 pub fn try_primary_key(mut self) -> Result<Self, String> {
1091 if !self.data_type.can_be_primary_key() {
1092 return Err(self.primary_key_type_error());
1093 }
1094 self.primary_key = true;
1095 self.nullable = false;
1096 Ok(self)
1097 }
1098
1099 pub fn unique(mut self) -> Self {
1106 if !self.data_type.supports_indexing() {
1107 #[cfg(debug_assertions)]
1108 eprintln!("QAIL: {}", self.unique_type_error());
1109 }
1110 self.unique = true;
1111 self
1112 }
1113
1114 pub fn try_unique(mut self) -> Result<Self, String> {
1118 if !self.data_type.supports_indexing() {
1119 return Err(self.unique_type_error());
1120 }
1121 self.unique = true;
1122 Ok(self)
1123 }
1124
1125 pub fn default(mut self, val: impl Into<String>) -> Self {
1127 self.default = Some(val.into());
1128 self
1129 }
1130
1131 pub fn references(mut self, table: &str, column: &str) -> Self {
1139 self.foreign_key = Some(ForeignKey {
1140 table: table.to_string(),
1141 column: column.to_string(),
1142 on_delete: FkAction::default(),
1143 on_update: FkAction::default(),
1144 deferrable: Deferrable::default(),
1145 });
1146 self
1147 }
1148
1149 pub fn on_delete(mut self, action: FkAction) -> Self {
1151 if let Some(ref mut fk) = self.foreign_key {
1152 fk.on_delete = action;
1153 }
1154 self
1155 }
1156
1157 pub fn on_update(mut self, action: FkAction) -> Self {
1159 if let Some(ref mut fk) = self.foreign_key {
1160 fk.on_update = action;
1161 }
1162 self
1163 }
1164
1165 pub fn check(mut self, expr: CheckExpr) -> Self {
1169 self.check = Some(CheckConstraint { expr, name: None });
1170 self
1171 }
1172
1173 pub fn check_named(mut self, name: impl Into<String>, expr: CheckExpr) -> Self {
1175 self.check = Some(CheckConstraint {
1176 expr,
1177 name: Some(name.into()),
1178 });
1179 self
1180 }
1181
1182 pub fn deferrable(mut self) -> Self {
1186 if let Some(ref mut fk) = self.foreign_key {
1187 fk.deferrable = Deferrable::Deferrable;
1188 }
1189 self
1190 }
1191
1192 pub fn initially_deferred(mut self) -> Self {
1194 if let Some(ref mut fk) = self.foreign_key {
1195 fk.deferrable = Deferrable::InitiallyDeferred;
1196 }
1197 self
1198 }
1199
1200 pub fn initially_immediate(mut self) -> Self {
1202 if let Some(ref mut fk) = self.foreign_key {
1203 fk.deferrable = Deferrable::InitiallyImmediate;
1204 }
1205 self
1206 }
1207
1208 pub fn generated_stored(mut self, expr: impl Into<String>) -> Self {
1212 self.generated = Some(Generated::AlwaysStored(expr.into()));
1213 self
1214 }
1215
1216 pub fn generated_identity(mut self) -> Self {
1218 self.generated = Some(Generated::AlwaysIdentity);
1219 self
1220 }
1221
1222 pub fn generated_by_default(mut self) -> Self {
1224 self.generated = Some(Generated::ByDefaultIdentity);
1225 self
1226 }
1227}
1228
1229impl Index {
1230 pub fn new(name: impl Into<String>, table: impl Into<String>, columns: Vec<String>) -> Self {
1232 Self {
1233 name: name.into(),
1234 table: table.into(),
1235 columns,
1236 unique: false,
1237 method: IndexMethod::default(),
1238 where_clause: None,
1239 include: Vec::new(),
1240 concurrently: false,
1241 expressions: Vec::new(),
1242 }
1243 }
1244
1245 pub fn expression(
1247 name: impl Into<String>,
1248 table: impl Into<String>,
1249 expressions: Vec<String>,
1250 ) -> Self {
1251 Self {
1252 name: name.into(),
1253 table: table.into(),
1254 columns: Vec::new(),
1255 unique: false,
1256 method: IndexMethod::default(),
1257 where_clause: None,
1258 include: Vec::new(),
1259 concurrently: false,
1260 expressions,
1261 }
1262 }
1263
1264 pub fn unique(mut self) -> Self {
1266 self.unique = true;
1267 self
1268 }
1269
1270 pub fn using(mut self, method: IndexMethod) -> Self {
1274 self.method = method;
1275 self
1276 }
1277
1278 pub fn partial(mut self, expr: CheckExpr) -> Self {
1280 self.where_clause = Some(expr);
1281 self
1282 }
1283
1284 pub fn include(mut self, cols: Vec<String>) -> Self {
1286 self.include = cols;
1287 self
1288 }
1289
1290 pub fn concurrently(mut self) -> Self {
1292 self.concurrently = true;
1293 self
1294 }
1295}
1296
1297fn fk_action_str(action: &FkAction) -> &'static str {
1300 match action {
1301 FkAction::NoAction => "no_action",
1302 FkAction::Cascade => "cascade",
1303 FkAction::SetNull => "set_null",
1304 FkAction::SetDefault => "set_default",
1305 FkAction::Restrict => "restrict",
1306 }
1307}
1308
1309fn format_qail_value_token(value: &str, extra_special: &[char]) -> String {
1310 let needs_quotes = value.is_empty()
1311 || value.chars().any(|ch| {
1312 ch.is_whitespace() || matches!(ch, ',' | '\'' | '"') || extra_special.contains(&ch)
1313 });
1314
1315 if needs_quotes {
1316 format!("\"{}\"", value.replace('"', "\"\""))
1317 } else {
1318 value.to_string()
1319 }
1320}
1321
1322fn format_check_in_value(value: &str) -> String {
1323 format_qail_value_token(value, &['[', ']'])
1324}
1325
1326fn check_expr_str(expr: &CheckExpr) -> String {
1328 match expr {
1329 CheckExpr::GreaterThan { column, value } => format!("{} > {}", column, value),
1330 CheckExpr::GreaterOrEqual { column, value } => format!("{} >= {}", column, value),
1331 CheckExpr::LessThan { column, value } => format!("{} < {}", column, value),
1332 CheckExpr::LessOrEqual { column, value } => format!("{} <= {}", column, value),
1333 CheckExpr::Between { column, low, high } => format!("{} between {} {}", column, low, high),
1334 CheckExpr::In { column, values } => format!(
1335 "{} in [{}]",
1336 column,
1337 values
1338 .iter()
1339 .map(|value| format_check_in_value(value))
1340 .collect::<Vec<_>>()
1341 .join(", ")
1342 ),
1343 CheckExpr::Regex { column, pattern } => {
1344 format!("{} ~ '{}'", column, pattern.replace('\'', "''"))
1345 }
1346 CheckExpr::MaxLength { column, max } => format!("length({}) <= {}", column, max),
1347 CheckExpr::MinLength { column, min } => format!("length({}) >= {}", column, min),
1348 CheckExpr::NotNull { column } => format!("{} not_null", column),
1349 CheckExpr::And(l, r) => format!("{} and {}", check_expr_str(l), check_expr_str(r)),
1350 CheckExpr::Or(l, r) => format!("{} or {}", check_expr_str(l), check_expr_str(r)),
1351 CheckExpr::Not(e) => format!("not {}", check_expr_str(e)),
1352 CheckExpr::Sql(sql) => sql.clone(),
1353 }
1354}
1355
1356fn format_enum_value(value: &str) -> String {
1357 format_qail_value_token(value, &['{', '}'])
1358}
1359
1360fn dollar_quote_qail_body(body: &str) -> String {
1361 let delimiter = if !body.contains("$$") {
1362 "$$".to_string()
1363 } else {
1364 let mut idx = 0usize;
1365 loop {
1366 let candidate = if idx == 0 {
1367 "$qail$".to_string()
1368 } else {
1369 format!("$qail{idx}$")
1370 };
1371 if !body.contains(&candidate) {
1372 break candidate;
1373 }
1374 idx = idx.saturating_add(1);
1375 }
1376 };
1377
1378 format!("{delimiter}\n{body}\n{delimiter}")
1379}
1380
1381pub fn to_qail_string(schema: &Schema) -> String {
1383 let mut output = String::new();
1384 output.push_str("# QAIL Schema\n\n");
1385
1386 for ext in &schema.extensions {
1388 let mut line = format!("extension {}", quote_qail_string(&ext.name));
1389 if let Some(ref s) = ext.schema {
1390 line.push_str(&format!(" schema {}", quote_qail_string(s)));
1391 }
1392 if let Some(ref v) = ext.version {
1393 line.push_str(&format!(" version {}", quote_qail_string(v)));
1394 }
1395 output.push_str(&line);
1396 output.push('\n');
1397 }
1398 if !schema.extensions.is_empty() {
1399 output.push('\n');
1400 }
1401
1402 for enum_type in &schema.enums {
1404 let values = enum_type
1405 .values
1406 .iter()
1407 .map(|v| format_enum_value(v))
1408 .collect::<Vec<_>>()
1409 .join(", ");
1410 output.push_str(&format!("enum {} {{ {} }}\n", enum_type.name, values));
1411 }
1412 if !schema.enums.is_empty() {
1413 output.push('\n');
1414 }
1415
1416 for seq in &schema.sequences {
1418 if seq.start.is_some()
1419 || seq.increment.is_some()
1420 || seq.min_value.is_some()
1421 || seq.max_value.is_some()
1422 || seq.cache.is_some()
1423 || seq.cycle
1424 || seq.owned_by.is_some()
1425 {
1426 let mut opts = Vec::new();
1427 if let Some(v) = seq.start {
1428 opts.push(format!("start {}", v));
1429 }
1430 if let Some(v) = seq.increment {
1431 opts.push(format!("increment {}", v));
1432 }
1433 if let Some(v) = seq.min_value {
1434 opts.push(format!("minvalue {}", v));
1435 }
1436 if let Some(v) = seq.max_value {
1437 opts.push(format!("maxvalue {}", v));
1438 }
1439 if let Some(v) = seq.cache {
1440 opts.push(format!("cache {}", v));
1441 }
1442 if seq.cycle {
1443 opts.push("cycle".to_string());
1444 }
1445 if let Some(ref o) = seq.owned_by {
1446 opts.push(format!("owned_by {}", o));
1447 }
1448 output.push_str(&format!("sequence {} {{ {} }}\n", seq.name, opts.join(" ")));
1449 } else {
1450 output.push_str(&format!("sequence {}\n", seq.name));
1451 }
1452 }
1453 if !schema.sequences.is_empty() {
1454 output.push('\n');
1455 }
1456
1457 let mut table_names: Vec<&String> = schema.tables.keys().collect();
1458 table_names.sort();
1459 for table_name in table_names {
1460 let table = &schema.tables[table_name];
1461 output.push_str(&format!("table {} {{\n", table.name));
1462 for col in &table.columns {
1463 let mut constraints: Vec<String> = Vec::new();
1464 if col.primary_key {
1465 constraints.push("primary_key".to_string());
1466 }
1467 if !col.nullable && !col.primary_key {
1468 constraints.push("not_null".to_string());
1469 }
1470 if col.unique {
1471 constraints.push("unique".to_string());
1472 }
1473 if let Some(def) = &col.default {
1474 constraints.push(format!("default {}", def));
1475 }
1476 if let Some(generated) = &col.generated {
1477 match generated {
1478 Generated::AlwaysStored(expr) => {
1479 constraints.push(format!("generated_stored({})", expr));
1480 }
1481 Generated::AlwaysIdentity => {
1482 constraints.push("generated_identity".to_string());
1483 }
1484 Generated::ByDefaultIdentity => {
1485 constraints.push("generated_by_default_identity".to_string());
1486 }
1487 }
1488 }
1489 if let Some(ref fk) = col.foreign_key {
1490 let mut fk_str = format!("references {}({})", fk.table, fk.column);
1491 if fk.on_delete != FkAction::NoAction {
1492 fk_str.push_str(&format!(" on_delete {}", fk_action_str(&fk.on_delete)));
1493 }
1494 if fk.on_update != FkAction::NoAction {
1495 fk_str.push_str(&format!(" on_update {}", fk_action_str(&fk.on_update)));
1496 }
1497 match &fk.deferrable {
1498 Deferrable::Deferrable => fk_str.push_str(" deferrable"),
1499 Deferrable::InitiallyDeferred => fk_str.push_str(" initially_deferred"),
1500 Deferrable::InitiallyImmediate => fk_str.push_str(" initially_immediate"),
1501 Deferrable::NotDeferrable => {} }
1503 constraints.push(fk_str);
1504 }
1505 if let Some(ref check) = col.check {
1506 constraints.push(format!("check({})", check_expr_str(&check.expr)));
1507 if let Some(name) = &check.name {
1508 constraints.push(format!("check_name {}", name));
1509 }
1510 }
1511
1512 let constraint_str = if constraints.is_empty() {
1513 String::new()
1514 } else {
1515 format!(" {}", constraints.join(" "))
1516 };
1517
1518 output.push_str(&format!(
1519 " {} {}{}\n",
1520 col.name,
1521 col.data_type.to_pg_type(),
1522 constraint_str
1523 ));
1524 }
1525 for fk in &table.multi_column_fks {
1527 let mut fk_line = format!(
1528 " foreign_key ({}) references {}({})\n",
1529 fk.columns.join(", "),
1530 fk.ref_table,
1531 fk.ref_columns.join(", ")
1532 );
1533 if fk.name.is_some()
1534 || fk.on_delete != FkAction::NoAction
1535 || fk.on_update != FkAction::NoAction
1536 || fk.deferrable != Deferrable::NotDeferrable
1537 {
1538 fk_line.pop();
1539 if let Some(name) = &fk.name {
1540 fk_line.push_str(&format!(" constraint {}", name));
1541 }
1542 if fk.on_delete != FkAction::NoAction {
1543 fk_line.push_str(&format!(" on_delete {}", fk_action_str(&fk.on_delete)));
1544 }
1545 if fk.on_update != FkAction::NoAction {
1546 fk_line.push_str(&format!(" on_update {}", fk_action_str(&fk.on_update)));
1547 }
1548 match &fk.deferrable {
1549 Deferrable::Deferrable => fk_line.push_str(" deferrable"),
1550 Deferrable::InitiallyDeferred => fk_line.push_str(" initially_deferred"),
1551 Deferrable::InitiallyImmediate => fk_line.push_str(" initially_immediate"),
1552 Deferrable::NotDeferrable => {}
1553 }
1554 fk_line.push('\n');
1555 }
1556 output.push_str(&fk_line);
1557 }
1558 if table.enable_rls {
1560 output.push_str(" enable_rls\n");
1561 }
1562 if table.force_rls {
1563 output.push_str(" force_rls\n");
1564 }
1565 output.push_str("}\n\n");
1566 }
1567
1568 for idx in &schema.indexes {
1569 let unique = if idx.unique { "unique " } else { "" };
1570 let cols = if !idx.expressions.is_empty() {
1571 idx.expressions.join(", ")
1572 } else {
1573 idx.columns.join(", ")
1574 };
1575 let mut line = format!("{}index {} on {}", unique, idx.name, idx.table);
1576 if idx.method != IndexMethod::BTree {
1577 line.push_str(" using ");
1578 line.push_str(index_method_str(&idx.method));
1579 }
1580 line.push_str(" (");
1581 line.push_str(&cols);
1582 line.push(')');
1583 if let Some(where_clause) = &idx.where_clause {
1584 line.push_str(" where ");
1585 line.push_str(&check_expr_str(where_clause));
1586 }
1587 output.push_str(&line);
1588 output.push('\n');
1589 }
1590
1591 for hint in &schema.migrations {
1592 match hint {
1593 MigrationHint::Rename { from, to } => {
1594 output.push_str(&format!("rename {} -> {}\n", from, to));
1595 }
1596 MigrationHint::Transform { expression, target } => {
1597 output.push_str(&format!("transform {} -> {}\n", expression, target));
1598 }
1599 MigrationHint::Drop { target, confirmed } => {
1600 let confirm = if *confirmed { " confirm" } else { "" };
1601 output.push_str(&format!("drop {}{}\n", target, confirm));
1602 }
1603 }
1604 }
1605
1606 for view in &schema.views {
1608 let prefix = if view.materialized {
1609 "materialized view"
1610 } else {
1611 "view"
1612 };
1613 let body = dollar_quote_qail_body(&view.query);
1614 output.push_str(&format!("{} {} {}\n\n", prefix, view.name, body));
1615 }
1616
1617 for func in &schema.functions {
1619 let args = func.args.join(", ");
1620 let volatility = func
1621 .volatility
1622 .as_deref()
1623 .filter(|v| !v.trim().is_empty())
1624 .map(|v| format!(" {}", v))
1625 .unwrap_or_default();
1626 let body = dollar_quote_qail_body(&func.body);
1627 output.push_str(&format!(
1628 "function {}({}) returns {} language {}{} {}\n\n",
1629 func.name, args, func.returns, func.language, volatility, body
1630 ));
1631 }
1632
1633 for trigger in &schema.triggers {
1635 let mut events = Vec::new();
1636 for evt in &trigger.events {
1637 if evt.eq_ignore_ascii_case("UPDATE") && !trigger.update_columns.is_empty() {
1638 events.push(format!("UPDATE OF {}", trigger.update_columns.join(", ")));
1639 } else {
1640 events.push(evt.clone());
1641 }
1642 }
1643 output.push_str(&format!(
1644 "trigger {} on {} {} {} execute {}\n",
1645 trigger.name,
1646 trigger.table,
1647 trigger.timing.to_lowercase(),
1648 events.join(" or ").to_lowercase(),
1649 trigger.execute_function
1650 ));
1651 }
1652 if !schema.triggers.is_empty() {
1653 output.push('\n');
1654 }
1655
1656 for policy in &schema.policies {
1658 let cmd = match policy.target {
1659 PolicyTarget::All => "all",
1660 PolicyTarget::Select => "select",
1661 PolicyTarget::Insert => "insert",
1662 PolicyTarget::Update => "update",
1663 PolicyTarget::Delete => "delete",
1664 };
1665 let perm = match policy.permissiveness {
1666 PolicyPermissiveness::Permissive => "",
1667 PolicyPermissiveness::Restrictive => " restrictive",
1668 };
1669 let role_str = match &policy.role {
1670 Some(r) => format!(" to {}", r),
1671 None => String::new(),
1672 };
1673 output.push_str(&format!(
1674 "policy {} on {} for {}{}{}",
1675 policy.name, policy.table, cmd, role_str, perm
1676 ));
1677 if let Some(ref using) = policy.using {
1678 output.push_str(&format!("\n using $$ {} $$", using));
1679 }
1680 if let Some(ref wc) = policy.with_check {
1681 output.push_str(&format!("\n with_check $$ {} $$", wc));
1682 }
1683 output.push_str("\n\n");
1684 }
1685
1686 for grant in &schema.grants {
1688 let privs: Vec<String> = grant
1689 .privileges
1690 .iter()
1691 .map(|p| p.to_string().to_lowercase())
1692 .collect();
1693 match grant.action {
1694 GrantAction::Grant => {
1695 output.push_str(&format!(
1696 "grant {} on {} to {}\n",
1697 privs.join(", "),
1698 grant.on_object,
1699 grant.to_role
1700 ));
1701 }
1702 GrantAction::Revoke => {
1703 output.push_str(&format!(
1704 "revoke {} on {} from {}\n",
1705 privs.join(", "),
1706 grant.on_object,
1707 grant.to_role
1708 ));
1709 }
1710 }
1711 }
1712 if !schema.grants.is_empty() {
1713 output.push('\n');
1714 }
1715
1716 for comment in &schema.comments {
1718 let text = quote_qail_string(&comment.text);
1719 match &comment.target {
1720 CommentTarget::Table(t) => {
1721 output.push_str(&format!("comment on {} {}\n", t, text));
1722 }
1723 CommentTarget::Column { table, column } => {
1724 output.push_str(&format!("comment on {}.{} {}\n", table, column, text));
1725 }
1726 CommentTarget::Raw(target) => {
1727 output.push_str(&format!("comment on {} {}\n", target, text));
1728 }
1729 }
1730 }
1731
1732 output
1733}
1734
1735fn quote_qail_string(value: &str) -> String {
1736 format!("\"{}\"", value.replace('"', "\"\""))
1737}
1738
1739pub fn schema_to_commands(schema: &Schema) -> Vec<crate::ast::Qail> {
1742 use crate::ast::{Action, ColumnGeneration, Constraint, Expr, IndexDef, Qail};
1743
1744 let mut cmds = Vec::new();
1745
1746 let mut indegree: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
1749 let mut reverse_adj: std::collections::HashMap<String, Vec<String>> =
1750 std::collections::HashMap::new();
1751
1752 for name in schema.tables.keys() {
1753 indegree.insert(name.clone(), 0);
1754 }
1755
1756 for table in schema.tables.values() {
1757 let mut deps = std::collections::HashSet::new();
1758 for col in &table.columns {
1759 if let Some(fk) = &col.foreign_key
1760 && fk.table != table.name
1761 && schema.tables.contains_key(&fk.table)
1762 {
1763 deps.insert(fk.table.clone());
1764 }
1765 }
1766 for fk in &table.multi_column_fks {
1767 if fk.ref_table != table.name && schema.tables.contains_key(&fk.ref_table) {
1768 deps.insert(fk.ref_table.clone());
1769 }
1770 }
1771
1772 indegree.insert(table.name.clone(), deps.len());
1773 for dep in deps {
1774 reverse_adj.entry(dep).or_default().push(table.name.clone());
1775 }
1776 }
1777
1778 let mut ready = std::collections::BTreeSet::new();
1779 for (name, deg) in &indegree {
1780 if *deg == 0 {
1781 ready.insert(name.clone());
1782 }
1783 }
1784
1785 let mut ordered_names: Vec<String> = Vec::with_capacity(schema.tables.len());
1786 while let Some(next) = ready.pop_first() {
1787 ordered_names.push(next.clone());
1788 if let Some(dependents) = reverse_adj.get(&next) {
1789 for dep_name in dependents {
1790 if let Some(d) = indegree.get_mut(dep_name)
1791 && *d > 0
1792 {
1793 *d -= 1;
1794 if *d == 0 {
1795 ready.insert(dep_name.clone());
1796 }
1797 }
1798 }
1799 }
1800 }
1801
1802 if ordered_names.len() < schema.tables.len() {
1805 let mut leftovers: Vec<String> = schema
1806 .tables
1807 .keys()
1808 .filter(|name| !ordered_names.contains(*name))
1809 .cloned()
1810 .collect();
1811 leftovers.sort();
1812 ordered_names.extend(leftovers);
1813 }
1814
1815 for table_name in ordered_names {
1816 let table = &schema.tables[&table_name];
1817 let columns: Vec<Expr> = table
1819 .columns
1820 .iter()
1821 .map(|col| {
1822 let mut constraints = Vec::new();
1823
1824 if col.primary_key {
1825 constraints.push(Constraint::PrimaryKey);
1826 }
1827 if col.nullable {
1828 constraints.push(Constraint::Nullable);
1829 }
1830 if col.unique {
1831 constraints.push(Constraint::Unique);
1832 }
1833 if let Some(def) = &col.default {
1834 constraints.push(Constraint::Default(def.clone()));
1835 }
1836 if let Some(ref fk) = col.foreign_key {
1837 constraints.push(Constraint::References(foreign_key_to_sql(fk)));
1838 }
1839 if let Some(check) = &col.check {
1840 let check_sql = check_expr_to_sql(&check.expr);
1841 if let Some(name) = &check.name {
1842 constraints.push(Constraint::Check(vec![format!(
1843 "CONSTRAINT {} CHECK ({})",
1844 name, check_sql
1845 )]));
1846 } else {
1847 constraints.push(Constraint::Check(vec![check_sql]));
1848 }
1849 }
1850 if let Some(generated) = &col.generated {
1851 let gen_constraint = match generated {
1852 Generated::AlwaysStored(expr) => {
1853 Constraint::Generated(ColumnGeneration::Stored(expr.clone()))
1854 }
1855 Generated::AlwaysIdentity => {
1856 Constraint::Generated(ColumnGeneration::Stored("identity".to_string()))
1857 }
1858 Generated::ByDefaultIdentity => Constraint::Generated(
1859 ColumnGeneration::Stored("identity_by_default".to_string()),
1860 ),
1861 };
1862 constraints.push(gen_constraint);
1863 }
1864
1865 Expr::Def {
1866 name: col.name.clone(),
1867 data_type: col.data_type.to_pg_type(),
1868 constraints,
1869 }
1870 })
1871 .collect();
1872
1873 cmds.push(Qail {
1874 action: Action::Make,
1875 table: table.name.clone(),
1876 columns,
1877 ..Default::default()
1878 });
1879 }
1880
1881 for idx in &schema.indexes {
1883 cmds.push(Qail {
1884 action: Action::Index,
1885 table: String::new(),
1886 index_def: Some(IndexDef {
1887 name: idx.name.clone(),
1888 table: idx.table.clone(),
1889 columns: if !idx.expressions.is_empty() {
1890 idx.expressions.clone()
1891 } else {
1892 idx.columns.clone()
1893 },
1894 unique: idx.unique,
1895 index_type: Some(index_method_str(&idx.method).to_string()),
1896 where_clause: idx.where_clause.as_ref().map(check_expr_to_sql),
1897 }),
1898 ..Default::default()
1899 });
1900 }
1901
1902 let mut fk_table_names: Vec<&String> = schema
1903 .tables
1904 .iter()
1905 .filter(|(_, table)| !table.multi_column_fks.is_empty())
1906 .map(|(name, _)| name)
1907 .collect();
1908 fk_table_names.sort();
1909 for table_name in fk_table_names {
1910 let table = &schema.tables[table_name];
1911 for fk in &table.multi_column_fks {
1912 cmds.push(multi_column_fk_to_alter_command(&table.name, fk));
1913 }
1914 }
1915
1916 cmds
1917}
1918
1919pub(super) fn multi_column_fk_to_table_constraint(
1920 fk: &MultiColumnForeignKey,
1921) -> crate::ast::TableConstraint {
1922 crate::ast::TableConstraint::ForeignKey {
1923 name: fk.name.clone(),
1924 columns: fk.columns.clone(),
1925 ref_table: fk.ref_table.clone(),
1926 ref_columns: fk.ref_columns.clone(),
1927 on_delete: (fk.on_delete != FkAction::NoAction)
1928 .then(|| fk_action_to_sql(&fk.on_delete).to_string()),
1929 on_update: (fk.on_update != FkAction::NoAction)
1930 .then(|| fk_action_to_sql(&fk.on_update).to_string()),
1931 deferrable: deferrable_to_sql(&fk.deferrable).map(str::to_string),
1932 }
1933}
1934
1935pub(super) fn multi_column_fk_to_alter_command(
1936 table_name: &str,
1937 fk: &MultiColumnForeignKey,
1938) -> crate::ast::Qail {
1939 crate::ast::Qail {
1940 action: crate::ast::Action::Alter,
1941 table: table_name.to_string(),
1942 table_constraints: vec![multi_column_fk_to_table_constraint(fk)],
1943 ..Default::default()
1944 }
1945}
1946
1947fn fk_action_to_sql(action: &FkAction) -> &'static str {
1948 match action {
1949 FkAction::NoAction => "NO ACTION",
1950 FkAction::Cascade => "CASCADE",
1951 FkAction::SetNull => "SET NULL",
1952 FkAction::SetDefault => "SET DEFAULT",
1953 FkAction::Restrict => "RESTRICT",
1954 }
1955}
1956
1957fn deferrable_to_sql(deferrable: &Deferrable) -> Option<&'static str> {
1958 match deferrable {
1959 Deferrable::NotDeferrable => None,
1960 Deferrable::Deferrable => Some("DEFERRABLE"),
1961 Deferrable::InitiallyDeferred => Some("DEFERRABLE INITIALLY DEFERRED"),
1962 Deferrable::InitiallyImmediate => Some("DEFERRABLE INITIALLY IMMEDIATE"),
1963 }
1964}
1965
1966pub(crate) fn foreign_key_to_sql(fk: &ForeignKey) -> String {
1967 let mut target = format!("{}({})", fk.table, fk.column);
1968 if fk.on_delete != FkAction::NoAction {
1969 target.push_str(" ON DELETE ");
1970 target.push_str(fk_action_to_sql(&fk.on_delete));
1971 }
1972 if fk.on_update != FkAction::NoAction {
1973 target.push_str(" ON UPDATE ");
1974 target.push_str(fk_action_to_sql(&fk.on_update));
1975 }
1976 if let Some(def) = deferrable_to_sql(&fk.deferrable) {
1977 target.push(' ');
1978 target.push_str(def);
1979 }
1980 target
1981}
1982
1983pub(crate) fn check_expr_to_sql(expr: &CheckExpr) -> String {
1984 match expr {
1985 CheckExpr::GreaterThan { column, value } => format!("{column} > {value}"),
1986 CheckExpr::GreaterOrEqual { column, value } => format!("{column} >= {value}"),
1987 CheckExpr::LessThan { column, value } => format!("{column} < {value}"),
1988 CheckExpr::LessOrEqual { column, value } => format!("{column} <= {value}"),
1989 CheckExpr::Between { column, low, high } => format!("{column} BETWEEN {low} AND {high}"),
1990 CheckExpr::In { column, values } => {
1991 if values.len() == 1 && looks_like_raw_check_expr(&values[0]) {
1992 return values[0].clone();
1993 }
1994 let quoted = values
1995 .iter()
1996 .map(|v| format!("'{}'", v.replace('\'', "''")))
1997 .collect::<Vec<_>>()
1998 .join(", ");
1999 format!("{column} IN ({quoted})")
2000 }
2001 CheckExpr::Regex { column, pattern } => {
2002 format!("{column} ~ '{}'", pattern.replace('\'', "''"))
2003 }
2004 CheckExpr::MaxLength { column, max } => format!("char_length({column}) <= {max}"),
2005 CheckExpr::MinLength { column, min } => format!("char_length({column}) >= {min}"),
2006 CheckExpr::NotNull { column } => format!("{column} IS NOT NULL"),
2007 CheckExpr::And(left, right) => {
2008 format!(
2009 "({}) AND ({})",
2010 check_expr_to_sql(left),
2011 check_expr_to_sql(right)
2012 )
2013 }
2014 CheckExpr::Or(left, right) => {
2015 format!(
2016 "({}) OR ({})",
2017 check_expr_to_sql(left),
2018 check_expr_to_sql(right)
2019 )
2020 }
2021 CheckExpr::Not(inner) => format!("NOT ({})", check_expr_to_sql(inner)),
2022 CheckExpr::Sql(sql) => sql.clone(),
2023 }
2024}
2025
2026fn looks_like_raw_check_expr(s: &str) -> bool {
2027 s.chars()
2028 .any(|c| c.is_whitespace() || matches!(c, '<' | '>' | '=' | '!' | '(' | ')' | ':'))
2029}
2030
2031#[cfg(test)]
2032mod tests {
2033 use super::*;
2034
2035 #[test]
2036 fn test_schema_builder() {
2037 let mut schema = Schema::new();
2038
2039 let users = Table::new("users")
2040 .column(Column::new("id", ColumnType::Serial).primary_key())
2041 .column(Column::new("name", ColumnType::Text).not_null())
2042 .column(Column::new("email", ColumnType::Text).unique());
2043
2044 schema.add_table(users);
2045 schema.add_index(Index::new("idx_users_email", "users", vec!["email".into()]).unique());
2046
2047 let output = to_qail_string(&schema);
2048 assert!(output.contains("table users"));
2049 assert!(output.contains("id SERIAL primary_key"));
2050 assert!(output.contains("unique index idx_users_email"));
2051 }
2052
2053 #[test]
2054 fn test_to_qail_string_preserves_vector_index_methods() {
2055 let mut schema = Schema::new();
2056 schema.add_index(
2057 Index::new(
2058 "idx_docs_embedding_hnsw",
2059 "documents",
2060 vec!["embedding vector_l2_ops".into()],
2061 )
2062 .using(IndexMethod::Hnsw),
2063 );
2064 schema.add_index(
2065 Index::new(
2066 "idx_docs_embedding_ivfflat",
2067 "documents",
2068 vec!["embedding vector_cosine_ops".into()],
2069 )
2070 .using(IndexMethod::IvfFlat),
2071 );
2072
2073 let output = to_qail_string(&schema);
2074
2075 assert!(output.contains(
2076 "index idx_docs_embedding_hnsw on documents using hnsw (embedding vector_l2_ops)"
2077 ));
2078 assert!(output.contains(
2079 "index idx_docs_embedding_ivfflat on documents using ivfflat (embedding vector_cosine_ops)"
2080 ));
2081 }
2082
2083 #[test]
2084 fn test_migration_hints() {
2085 let mut schema = Schema::new();
2086 schema.add_hint(MigrationHint::Rename {
2087 from: "users.username".into(),
2088 to: "users.name".into(),
2089 });
2090
2091 let output = to_qail_string(&schema);
2092 assert!(output.contains("rename users.username -> users.name"));
2093 }
2094
2095 #[test]
2096 fn test_to_qail_string_includes_function_volatility() {
2097 let mut schema = Schema::new();
2098 let func = SchemaFunctionDef::new(
2099 "is_super_admin",
2100 "boolean",
2101 "BEGIN RETURN true; END;".to_string(),
2102 )
2103 .language("plpgsql")
2104 .volatility("stable");
2105 schema.add_function(func);
2106
2107 let output = to_qail_string(&schema);
2108 assert!(
2109 output.contains("function is_super_admin() returns boolean language plpgsql stable $$")
2110 );
2111 }
2112
2113 #[test]
2114 fn test_invalid_primary_key_type_strict() {
2115 let err = Column::new("data", ColumnType::Jsonb)
2116 .try_primary_key()
2117 .expect_err("JSONB should be rejected by strict PK policy");
2118 assert!(err.contains("cannot be a primary key"));
2119 }
2120
2121 #[test]
2122 fn test_invalid_primary_key_type_fail_soft() {
2123 let col = Column::new("data", ColumnType::Jsonb).primary_key();
2124 assert!(col.primary_key);
2125 assert!(!col.nullable);
2126 }
2127
2128 #[test]
2129 fn test_invalid_unique_type_strict() {
2130 let err = Column::new("data", ColumnType::Jsonb)
2131 .try_unique()
2132 .expect_err("JSONB should be rejected by strict UNIQUE policy");
2133 assert!(err.contains("cannot have UNIQUE"));
2134 }
2135
2136 #[test]
2137 fn test_invalid_unique_type_fail_soft() {
2138 let col = Column::new("data", ColumnType::Jsonb).unique();
2139 assert!(col.unique);
2140 }
2141
2142 #[test]
2143 fn test_foreign_key_valid() {
2144 let mut schema = Schema::new();
2145
2146 schema.add_table(
2147 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
2148 );
2149
2150 schema.add_table(
2151 Table::new("posts")
2152 .column(Column::new("id", ColumnType::Uuid).primary_key())
2153 .column(
2154 Column::new("user_id", ColumnType::Uuid)
2155 .references("users", "id")
2156 .on_delete(FkAction::Cascade),
2157 ),
2158 );
2159
2160 assert!(schema.validate().is_ok());
2162 }
2163
2164 #[test]
2165 fn test_foreign_key_invalid_table() {
2166 let mut schema = Schema::new();
2167
2168 schema.add_table(
2169 Table::new("posts")
2170 .column(Column::new("id", ColumnType::Uuid).primary_key())
2171 .column(Column::new("user_id", ColumnType::Uuid).references("nonexistent", "id")),
2172 );
2173
2174 let result = schema.validate();
2176 assert!(result.is_err());
2177 assert!(result.unwrap_err()[0].contains("non-existent table"));
2178 }
2179
2180 #[test]
2181 fn test_foreign_key_invalid_column() {
2182 let mut schema = Schema::new();
2183
2184 schema.add_table(
2185 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
2186 );
2187
2188 schema.add_table(
2189 Table::new("posts")
2190 .column(Column::new("id", ColumnType::Uuid).primary_key())
2191 .column(
2192 Column::new("user_id", ColumnType::Uuid).references("users", "wrong_column"),
2193 ),
2194 );
2195
2196 let result = schema.validate();
2198 assert!(result.is_err());
2199 assert!(result.unwrap_err()[0].contains("non-existent column"));
2200 }
2201
2202 #[test]
2203 fn test_schema_to_commands_preserves_fk_actions_and_checks() {
2204 let mut schema = Schema::new();
2205 schema.add_table(
2206 Table::new("orgs").column(Column::new("id", ColumnType::Uuid).primary_key()),
2207 );
2208 schema.add_table(
2209 Table::new("users")
2210 .column(Column::new("id", ColumnType::Uuid).primary_key())
2211 .column(
2212 Column::new("org_id", ColumnType::Uuid)
2213 .references("orgs", "id")
2214 .on_delete(FkAction::Cascade)
2215 .on_update(FkAction::Restrict),
2216 )
2217 .column(
2218 Column::new("age", ColumnType::Int).check(CheckExpr::GreaterOrEqual {
2219 column: "age".to_string(),
2220 value: 18,
2221 }),
2222 ),
2223 );
2224
2225 let cmds = schema_to_commands(&schema);
2226 let users_cmd = cmds
2227 .iter()
2228 .find(|c| c.action == crate::ast::Action::Make && c.table == "users")
2229 .expect("users create command should exist");
2230 let org_id_constraints = users_cmd
2231 .columns
2232 .iter()
2233 .find_map(|e| match e {
2234 crate::ast::Expr::Def {
2235 name, constraints, ..
2236 } if name == "org_id" => Some(constraints),
2237 _ => None,
2238 })
2239 .expect("org_id should exist");
2240 let age_constraints = users_cmd
2241 .columns
2242 .iter()
2243 .find_map(|e| match e {
2244 crate::ast::Expr::Def {
2245 name, constraints, ..
2246 } if name == "age" => Some(constraints),
2247 _ => None,
2248 })
2249 .expect("age should exist");
2250
2251 assert!(
2252 org_id_constraints.iter().any(|c| matches!(
2253 c,
2254 crate::ast::Constraint::References(target)
2255 if target.contains("orgs(id)")
2256 && target.contains("ON DELETE CASCADE")
2257 && target.contains("ON UPDATE RESTRICT")
2258 )),
2259 "foreign key action clauses should be preserved"
2260 );
2261 assert!(
2262 age_constraints
2263 .iter()
2264 .any(|c| matches!(c, crate::ast::Constraint::Check(vals) if vals.len() == 1)),
2265 "check expressions should be preserved"
2266 );
2267 }
2268
2269 #[test]
2270 fn schema_to_commands_preserves_multi_column_foreign_keys() {
2271 use crate::transpiler::ToSql;
2272
2273 let mut schema = Schema::new();
2274 schema.add_table(
2275 Table::new("schedules")
2276 .column(Column::new("route_id", ColumnType::Text))
2277 .column(Column::new("schedule_id", ColumnType::Text)),
2278 );
2279 schema.add_index(
2280 Index::new(
2281 "idx_schedules_route_schedule",
2282 "schedules",
2283 vec!["route_id".to_string(), "schedule_id".to_string()],
2284 )
2285 .unique(),
2286 );
2287 schema.add_table(
2288 Table::new("trips")
2289 .column(Column::new("route_id", ColumnType::Text))
2290 .column(Column::new("schedule_id", ColumnType::Text))
2291 .foreign_key(
2292 MultiColumnForeignKey::new(
2293 vec!["route_id".to_string(), "schedule_id".to_string()],
2294 "schedules",
2295 vec!["route_id".to_string(), "schedule_id".to_string()],
2296 )
2297 .named("fk_trips_schedule")
2298 .on_delete(FkAction::Cascade)
2299 .on_update(FkAction::Restrict)
2300 .initially_deferred(),
2301 ),
2302 );
2303
2304 let cmds = schema_to_commands(&schema);
2305 let schedules_idx = cmds
2306 .iter()
2307 .position(|c| c.action == crate::ast::Action::Make && c.table == "schedules")
2308 .expect("schedules create command should exist");
2309 let trips_idx = cmds
2310 .iter()
2311 .position(|c| c.action == crate::ast::Action::Make && c.table == "trips")
2312 .expect("trips create command should exist");
2313 let unique_idx = cmds
2314 .iter()
2315 .position(|c| {
2316 c.action == crate::ast::Action::Index
2317 && c.index_def
2318 .as_ref()
2319 .is_some_and(|idx| idx.name == "idx_schedules_route_schedule")
2320 })
2321 .expect("unique index command should exist");
2322 let add_fk_idx = cmds
2323 .iter()
2324 .position(|c| c.action == crate::ast::Action::Alter && c.table == "trips")
2325 .expect("trips composite foreign key ALTER command should exist");
2326
2327 assert!(schedules_idx < unique_idx);
2328 assert!(trips_idx < unique_idx);
2329 assert!(unique_idx < add_fk_idx);
2330
2331 let trips_cmd = cmds
2332 .iter()
2333 .find(|c| c.action == crate::ast::Action::Make && c.table == "trips")
2334 .expect("trips create command should exist");
2335 assert!(
2336 trips_cmd.table_constraints.is_empty(),
2337 "composite foreign keys should not be emitted inline on CREATE TABLE"
2338 );
2339
2340 let add_fk_cmd = &cmds[add_fk_idx];
2341 assert!(
2342 add_fk_cmd
2343 .table_constraints
2344 .iter()
2345 .any(|constraint| matches!(
2346 constraint,
2347 crate::ast::TableConstraint::ForeignKey {
2348 name,
2349 columns,
2350 ref_table,
2351 ref_columns,
2352 on_delete,
2353 on_update,
2354 deferrable,
2355 } if columns == &["route_id", "schedule_id"]
2356 && name.as_deref() == Some("fk_trips_schedule")
2357 && ref_table == "schedules"
2358 && ref_columns == &["route_id", "schedule_id"]
2359 && on_delete.as_deref() == Some("CASCADE")
2360 && on_update.as_deref() == Some("RESTRICT")
2361 && deferrable.as_deref() == Some("DEFERRABLE INITIALLY DEFERRED")
2362 )),
2363 "multi-column foreign key should be represented in generated commands"
2364 );
2365
2366 let sql = add_fk_cmd.to_sql();
2367 assert!(
2368 sql.contains(
2369 "ALTER TABLE trips ADD CONSTRAINT fk_trips_schedule FOREIGN KEY (route_id, schedule_id) REFERENCES schedules(route_id, schedule_id) ON DELETE CASCADE ON UPDATE RESTRICT DEFERRABLE INITIALLY DEFERRED"
2370 ),
2371 "generated SQL should include composite foreign key, got: {sql}"
2372 );
2373 }
2374}