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}
299
300#[derive(Debug, Clone)]
302pub struct CheckConstraint {
303 pub expr: CheckExpr,
305 pub name: Option<String>,
307}
308
309#[derive(Debug, Clone, Default, PartialEq)]
315pub enum Deferrable {
316 #[default]
317 NotDeferrable,
319 Deferrable,
321 InitiallyDeferred,
323 InitiallyImmediate,
325}
326
327#[derive(Debug, Clone)]
333pub enum Generated {
334 AlwaysStored(String),
336 AlwaysIdentity,
338 ByDefaultIdentity,
340}
341
342#[derive(Debug, Clone, Default, PartialEq)]
348pub enum IndexMethod {
349 #[default]
350 BTree,
352 Hash,
354 Gin,
356 Gist,
358 Brin,
360 SpGist,
362}
363
364#[derive(Debug, Clone, PartialEq)]
370pub struct Extension {
371 pub name: String,
373 pub schema: Option<String>,
375 pub version: Option<String>,
377}
378
379impl Extension {
380 pub fn new(name: impl Into<String>) -> Self {
382 Self {
383 name: name.into(),
384 schema: None,
385 version: None,
386 }
387 }
388
389 pub fn schema(mut self, schema: impl Into<String>) -> Self {
391 self.schema = Some(schema.into());
392 self
393 }
394
395 pub fn version(mut self, version: impl Into<String>) -> Self {
397 self.version = Some(version.into());
398 self
399 }
400}
401
402#[derive(Debug, Clone, PartialEq)]
404pub struct Comment {
405 pub target: CommentTarget,
407 pub text: String,
409}
410
411#[derive(Debug, Clone, PartialEq)]
413pub enum CommentTarget {
414 Table(String),
416 Column {
418 table: String,
420 column: String,
422 },
423}
424
425impl Comment {
426 pub fn on_table(table: impl Into<String>, text: impl Into<String>) -> Self {
428 Self {
429 target: CommentTarget::Table(table.into()),
430 text: text.into(),
431 }
432 }
433
434 pub fn on_column(
436 table: impl Into<String>,
437 column: impl Into<String>,
438 text: impl Into<String>,
439 ) -> Self {
440 Self {
441 target: CommentTarget::Column {
442 table: table.into(),
443 column: column.into(),
444 },
445 text: text.into(),
446 }
447 }
448}
449
450#[derive(Debug, Clone, PartialEq)]
452pub struct Sequence {
453 pub name: String,
455 pub data_type: Option<String>,
457 pub start: Option<i64>,
459 pub increment: Option<i64>,
461 pub min_value: Option<i64>,
463 pub max_value: Option<i64>,
465 pub cache: Option<i64>,
467 pub cycle: bool,
469 pub owned_by: Option<String>,
471}
472
473impl Sequence {
474 pub fn new(name: impl Into<String>) -> Self {
476 Self {
477 name: name.into(),
478 data_type: None,
479 start: None,
480 increment: None,
481 min_value: None,
482 max_value: None,
483 cache: None,
484 cycle: false,
485 owned_by: None,
486 }
487 }
488
489 pub fn start(mut self, v: i64) -> Self {
491 self.start = Some(v);
492 self
493 }
494
495 pub fn increment(mut self, v: i64) -> Self {
497 self.increment = Some(v);
498 self
499 }
500
501 pub fn min_value(mut self, v: i64) -> Self {
503 self.min_value = Some(v);
504 self
505 }
506
507 pub fn max_value(mut self, v: i64) -> Self {
509 self.max_value = Some(v);
510 self
511 }
512
513 pub fn cache(mut self, v: i64) -> Self {
515 self.cache = Some(v);
516 self
517 }
518
519 pub fn cycle(mut self) -> Self {
521 self.cycle = true;
522 self
523 }
524
525 pub fn owned_by(mut self, col: impl Into<String>) -> Self {
527 self.owned_by = Some(col.into());
528 self
529 }
530}
531
532#[derive(Debug, Clone, PartialEq)]
538pub struct EnumType {
539 pub name: String,
541 pub values: Vec<String>,
543}
544
545impl EnumType {
546 pub fn new(name: impl Into<String>, values: Vec<String>) -> Self {
548 Self {
549 name: name.into(),
550 values,
551 }
552 }
553
554 pub fn add_value(mut self, value: impl Into<String>) -> Self {
556 self.values.push(value.into());
557 self
558 }
559}
560
561#[derive(Debug, Clone, PartialEq)]
563pub struct MultiColumnForeignKey {
564 pub columns: Vec<String>,
566 pub ref_table: String,
568 pub ref_columns: Vec<String>,
570 pub on_delete: FkAction,
572 pub on_update: FkAction,
574 pub deferrable: Deferrable,
576 pub name: Option<String>,
578}
579
580impl MultiColumnForeignKey {
581 pub fn new(
583 columns: Vec<String>,
584 ref_table: impl Into<String>,
585 ref_columns: Vec<String>,
586 ) -> Self {
587 Self {
588 columns,
589 ref_table: ref_table.into(),
590 ref_columns,
591 on_delete: FkAction::default(),
592 on_update: FkAction::default(),
593 deferrable: Deferrable::default(),
594 name: None,
595 }
596 }
597
598 pub fn on_delete(mut self, action: FkAction) -> Self {
600 self.on_delete = action;
601 self
602 }
603
604 pub fn on_update(mut self, action: FkAction) -> Self {
606 self.on_update = action;
607 self
608 }
609
610 pub fn named(mut self, name: impl Into<String>) -> Self {
612 self.name = Some(name.into());
613 self
614 }
615}
616
617#[derive(Debug, Clone, PartialEq)]
623pub struct ViewDef {
624 pub name: String,
626 pub query: String,
628 pub materialized: bool,
630}
631
632impl ViewDef {
633 pub fn new(name: impl Into<String>, query: impl Into<String>) -> Self {
635 Self {
636 name: name.into(),
637 query: query.into(),
638 materialized: false,
639 }
640 }
641
642 pub fn materialized(mut self) -> Self {
644 self.materialized = true;
645 self
646 }
647}
648
649#[derive(Debug, Clone, PartialEq)]
651pub struct SchemaFunctionDef {
652 pub name: String,
654 pub args: Vec<String>,
656 pub returns: String,
658 pub body: String,
660 pub language: String,
662 pub volatility: Option<String>,
664}
665
666impl SchemaFunctionDef {
667 pub fn new(
669 name: impl Into<String>,
670 returns: impl Into<String>,
671 body: impl Into<String>,
672 ) -> Self {
673 Self {
674 name: name.into(),
675 args: Vec::new(),
676 returns: returns.into(),
677 body: body.into(),
678 language: "plpgsql".to_string(),
679 volatility: None,
680 }
681 }
682
683 pub fn language(mut self, lang: impl Into<String>) -> Self {
685 self.language = lang.into();
686 self
687 }
688
689 pub fn arg(mut self, arg: impl Into<String>) -> Self {
691 self.args.push(arg.into());
692 self
693 }
694
695 pub fn volatility(mut self, v: impl Into<String>) -> Self {
697 self.volatility = Some(v.into());
698 self
699 }
700}
701
702#[derive(Debug, Clone, PartialEq)]
704pub struct SchemaTriggerDef {
705 pub name: String,
707 pub table: String,
709 pub timing: String,
711 pub events: Vec<String>,
713 pub for_each_row: bool,
715 pub execute_function: String,
717 pub condition: Option<String>,
719}
720
721impl SchemaTriggerDef {
722 pub fn new(
724 name: impl Into<String>,
725 table: impl Into<String>,
726 execute_function: impl Into<String>,
727 ) -> Self {
728 Self {
729 name: name.into(),
730 table: table.into(),
731 timing: "BEFORE".to_string(),
732 events: vec!["INSERT".to_string()],
733 for_each_row: true,
734 execute_function: execute_function.into(),
735 condition: None,
736 }
737 }
738
739 pub fn timing(mut self, t: impl Into<String>) -> Self {
741 self.timing = t.into();
742 self
743 }
744
745 pub fn events(mut self, evts: Vec<String>) -> Self {
747 self.events = evts;
748 self
749 }
750
751 pub fn for_each_statement(mut self) -> Self {
753 self.for_each_row = false;
754 self
755 }
756
757 pub fn condition(mut self, cond: impl Into<String>) -> Self {
759 self.condition = Some(cond.into());
760 self
761 }
762}
763
764#[derive(Debug, Clone, PartialEq)]
766pub struct Grant {
767 pub action: GrantAction,
769 pub privileges: Vec<Privilege>,
771 pub on_object: String,
773 pub to_role: String,
775}
776
777#[derive(Debug, Clone, PartialEq, Default)]
779pub enum GrantAction {
780 #[default]
781 Grant,
783 Revoke,
785}
786
787#[derive(Debug, Clone, PartialEq)]
789pub enum Privilege {
790 All,
792 Select,
794 Insert,
796 Update,
798 Delete,
800 Usage,
802 Execute,
804}
805
806impl std::fmt::Display for Privilege {
807 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808 match self {
809 Privilege::All => write!(f, "ALL"),
810 Privilege::Select => write!(f, "SELECT"),
811 Privilege::Insert => write!(f, "INSERT"),
812 Privilege::Update => write!(f, "UPDATE"),
813 Privilege::Delete => write!(f, "DELETE"),
814 Privilege::Usage => write!(f, "USAGE"),
815 Privilege::Execute => write!(f, "EXECUTE"),
816 }
817 }
818}
819
820impl Grant {
821 pub fn new(
823 privileges: Vec<Privilege>,
824 on_object: impl Into<String>,
825 to_role: impl Into<String>,
826 ) -> Self {
827 Self {
828 action: GrantAction::Grant,
829 privileges,
830 on_object: on_object.into(),
831 to_role: to_role.into(),
832 }
833 }
834
835 pub fn revoke(
837 privileges: Vec<Privilege>,
838 on_object: impl Into<String>,
839 from_role: impl Into<String>,
840 ) -> Self {
841 Self {
842 action: GrantAction::Revoke,
843 privileges,
844 on_object: on_object.into(),
845 to_role: from_role.into(),
846 }
847 }
848}
849
850impl Schema {
851 pub fn new() -> Self {
853 Self::default()
854 }
855
856 pub fn add_table(&mut self, table: Table) {
858 self.tables.insert(table.name.clone(), table);
859 }
860
861 pub fn add_index(&mut self, index: Index) {
863 self.indexes.push(index);
864 }
865
866 pub fn add_hint(&mut self, hint: MigrationHint) {
868 self.migrations.push(hint);
869 }
870
871 pub fn add_extension(&mut self, ext: Extension) {
873 self.extensions.push(ext);
874 }
875
876 pub fn add_comment(&mut self, comment: Comment) {
878 self.comments.push(comment);
879 }
880
881 pub fn add_sequence(&mut self, seq: Sequence) {
883 self.sequences.push(seq);
884 }
885
886 pub fn add_enum(&mut self, enum_type: EnumType) {
888 self.enums.push(enum_type);
889 }
890
891 pub fn add_view(&mut self, view: ViewDef) {
893 self.views.push(view);
894 }
895
896 pub fn add_function(&mut self, func: SchemaFunctionDef) {
898 self.functions.push(func);
899 }
900
901 pub fn add_trigger(&mut self, trigger: SchemaTriggerDef) {
903 self.triggers.push(trigger);
904 }
905
906 pub fn add_grant(&mut self, grant: Grant) {
908 self.grants.push(grant);
909 }
910
911 pub fn add_resource(&mut self, resource: ResourceDef) {
913 self.resources.push(resource);
914 }
915
916 pub fn add_policy(&mut self, policy: RlsPolicy) {
918 self.policies.push(policy);
919 }
920
921 pub fn validate(&self) -> Result<(), Vec<String>> {
923 let mut errors = Vec::new();
924
925 for table in self.tables.values() {
926 for col in &table.columns {
927 if let Some(ref fk) = col.foreign_key {
928 if !self.tables.contains_key(&fk.table) {
929 errors.push(format!(
930 "FK error: {}.{} references non-existent table '{}'",
931 table.name, col.name, fk.table
932 ));
933 } else {
934 let ref_table = &self.tables[&fk.table];
935 if !ref_table.columns.iter().any(|c| c.name == fk.column) {
936 errors.push(format!(
937 "FK error: {}.{} references non-existent column '{}.{}'",
938 table.name, col.name, fk.table, fk.column
939 ));
940 }
941 }
942 }
943 }
944 }
945
946 if errors.is_empty() {
947 Ok(())
948 } else {
949 Err(errors)
950 }
951 }
952}
953
954impl Table {
955 pub fn new(name: impl Into<String>) -> Self {
957 Self {
958 name: name.into(),
959 columns: Vec::new(),
960 multi_column_fks: Vec::new(),
961 enable_rls: false,
962 force_rls: false,
963 }
964 }
965
966 pub fn column(mut self, col: Column) -> Self {
968 self.columns.push(col);
969 self
970 }
971
972 pub fn foreign_key(mut self, fk: MultiColumnForeignKey) -> Self {
974 self.multi_column_fks.push(fk);
975 self
976 }
977}
978
979impl Column {
980 fn primary_key_type_error(&self) -> String {
981 format!(
982 "Column '{}' of type {} cannot be a primary key. \
983 Valid PK types: scalar/indexable types \
984 (UUID, TEXT, VARCHAR, INT, BIGINT, SERIAL, BIGSERIAL, BOOLEAN, FLOAT, DECIMAL, \
985 TIMESTAMP, TIMESTAMPTZ, DATE, TIME, ENUM, INET, CIDR, MACADDR)",
986 self.name,
987 self.data_type.name()
988 )
989 }
990
991 fn unique_type_error(&self) -> String {
992 format!(
993 "Column '{}' of type {} cannot have UNIQUE constraint. \
994 JSONB and BYTEA types do not support standard indexing.",
995 self.name,
996 self.data_type.name()
997 )
998 }
999
1000 pub fn new(name: impl Into<String>, data_type: ColumnType) -> Self {
1002 Self {
1003 name: name.into(),
1004 data_type,
1005 nullable: true,
1006 primary_key: false,
1007 unique: false,
1008 default: None,
1009 foreign_key: None,
1010 check: None,
1011 generated: None,
1012 }
1013 }
1014
1015 pub fn not_null(mut self) -> Self {
1017 self.nullable = false;
1018 self
1019 }
1020
1021 pub fn primary_key(mut self) -> Self {
1028 if !self.data_type.can_be_primary_key() {
1029 #[cfg(debug_assertions)]
1030 eprintln!("QAIL: {}", self.primary_key_type_error());
1031 }
1032 self.primary_key = true;
1033 self.nullable = false;
1034 self
1035 }
1036
1037 pub fn try_primary_key(mut self) -> Result<Self, String> {
1041 if !self.data_type.can_be_primary_key() {
1042 return Err(self.primary_key_type_error());
1043 }
1044 self.primary_key = true;
1045 self.nullable = false;
1046 Ok(self)
1047 }
1048
1049 pub fn unique(mut self) -> Self {
1056 if !self.data_type.supports_indexing() {
1057 #[cfg(debug_assertions)]
1058 eprintln!("QAIL: {}", self.unique_type_error());
1059 }
1060 self.unique = true;
1061 self
1062 }
1063
1064 pub fn try_unique(mut self) -> Result<Self, String> {
1068 if !self.data_type.supports_indexing() {
1069 return Err(self.unique_type_error());
1070 }
1071 self.unique = true;
1072 Ok(self)
1073 }
1074
1075 pub fn default(mut self, val: impl Into<String>) -> Self {
1077 self.default = Some(val.into());
1078 self
1079 }
1080
1081 pub fn references(mut self, table: &str, column: &str) -> Self {
1089 self.foreign_key = Some(ForeignKey {
1090 table: table.to_string(),
1091 column: column.to_string(),
1092 on_delete: FkAction::default(),
1093 on_update: FkAction::default(),
1094 deferrable: Deferrable::default(),
1095 });
1096 self
1097 }
1098
1099 pub fn on_delete(mut self, action: FkAction) -> Self {
1101 if let Some(ref mut fk) = self.foreign_key {
1102 fk.on_delete = action;
1103 }
1104 self
1105 }
1106
1107 pub fn on_update(mut self, action: FkAction) -> Self {
1109 if let Some(ref mut fk) = self.foreign_key {
1110 fk.on_update = action;
1111 }
1112 self
1113 }
1114
1115 pub fn check(mut self, expr: CheckExpr) -> Self {
1119 self.check = Some(CheckConstraint { expr, name: None });
1120 self
1121 }
1122
1123 pub fn check_named(mut self, name: impl Into<String>, expr: CheckExpr) -> Self {
1125 self.check = Some(CheckConstraint {
1126 expr,
1127 name: Some(name.into()),
1128 });
1129 self
1130 }
1131
1132 pub fn deferrable(mut self) -> Self {
1136 if let Some(ref mut fk) = self.foreign_key {
1137 fk.deferrable = Deferrable::Deferrable;
1138 }
1139 self
1140 }
1141
1142 pub fn initially_deferred(mut self) -> Self {
1144 if let Some(ref mut fk) = self.foreign_key {
1145 fk.deferrable = Deferrable::InitiallyDeferred;
1146 }
1147 self
1148 }
1149
1150 pub fn initially_immediate(mut self) -> Self {
1152 if let Some(ref mut fk) = self.foreign_key {
1153 fk.deferrable = Deferrable::InitiallyImmediate;
1154 }
1155 self
1156 }
1157
1158 pub fn generated_stored(mut self, expr: impl Into<String>) -> Self {
1162 self.generated = Some(Generated::AlwaysStored(expr.into()));
1163 self
1164 }
1165
1166 pub fn generated_identity(mut self) -> Self {
1168 self.generated = Some(Generated::AlwaysIdentity);
1169 self
1170 }
1171
1172 pub fn generated_by_default(mut self) -> Self {
1174 self.generated = Some(Generated::ByDefaultIdentity);
1175 self
1176 }
1177}
1178
1179impl Index {
1180 pub fn new(name: impl Into<String>, table: impl Into<String>, columns: Vec<String>) -> Self {
1182 Self {
1183 name: name.into(),
1184 table: table.into(),
1185 columns,
1186 unique: false,
1187 method: IndexMethod::default(),
1188 where_clause: None,
1189 include: Vec::new(),
1190 concurrently: false,
1191 expressions: Vec::new(),
1192 }
1193 }
1194
1195 pub fn expression(
1197 name: impl Into<String>,
1198 table: impl Into<String>,
1199 expressions: Vec<String>,
1200 ) -> Self {
1201 Self {
1202 name: name.into(),
1203 table: table.into(),
1204 columns: Vec::new(),
1205 unique: false,
1206 method: IndexMethod::default(),
1207 where_clause: None,
1208 include: Vec::new(),
1209 concurrently: false,
1210 expressions,
1211 }
1212 }
1213
1214 pub fn unique(mut self) -> Self {
1216 self.unique = true;
1217 self
1218 }
1219
1220 pub fn using(mut self, method: IndexMethod) -> Self {
1224 self.method = method;
1225 self
1226 }
1227
1228 pub fn partial(mut self, expr: CheckExpr) -> Self {
1230 self.where_clause = Some(expr);
1231 self
1232 }
1233
1234 pub fn include(mut self, cols: Vec<String>) -> Self {
1236 self.include = cols;
1237 self
1238 }
1239
1240 pub fn concurrently(mut self) -> Self {
1242 self.concurrently = true;
1243 self
1244 }
1245}
1246
1247fn fk_action_str(action: &FkAction) -> &'static str {
1250 match action {
1251 FkAction::NoAction => "no_action",
1252 FkAction::Cascade => "cascade",
1253 FkAction::SetNull => "set_null",
1254 FkAction::SetDefault => "set_default",
1255 FkAction::Restrict => "restrict",
1256 }
1257}
1258
1259fn check_expr_str(expr: &CheckExpr) -> String {
1261 match expr {
1262 CheckExpr::GreaterThan { column, value } => format!("{} > {}", column, value),
1263 CheckExpr::GreaterOrEqual { column, value } => format!("{} >= {}", column, value),
1264 CheckExpr::LessThan { column, value } => format!("{} < {}", column, value),
1265 CheckExpr::LessOrEqual { column, value } => format!("{} <= {}", column, value),
1266 CheckExpr::Between { column, low, high } => format!("{} between {} {}", column, low, high),
1267 CheckExpr::In { column, values } => format!("{} in [{}]", column, values.join(", ")),
1268 CheckExpr::Regex { column, pattern } => format!("{} ~ '{}'", column, pattern),
1269 CheckExpr::MaxLength { column, max } => format!("length({}) <= {}", column, max),
1270 CheckExpr::MinLength { column, min } => format!("length({}) >= {}", column, min),
1271 CheckExpr::NotNull { column } => format!("{} not_null", column),
1272 CheckExpr::And(l, r) => format!("{} and {}", check_expr_str(l), check_expr_str(r)),
1273 CheckExpr::Or(l, r) => format!("{} or {}", check_expr_str(l), check_expr_str(r)),
1274 CheckExpr::Not(e) => format!("not {}", check_expr_str(e)),
1275 }
1276}
1277
1278pub fn to_qail_string(schema: &Schema) -> String {
1280 let mut output = String::new();
1281 output.push_str("# QAIL Schema\n\n");
1282
1283 for ext in &schema.extensions {
1285 let mut line = format!("extension \"{}\"", ext.name);
1286 if let Some(ref s) = ext.schema {
1287 line.push_str(&format!(" schema {}", s));
1288 }
1289 if let Some(ref v) = ext.version {
1290 line.push_str(&format!(" version \"{}\"", v));
1291 }
1292 output.push_str(&line);
1293 output.push('\n');
1294 }
1295 if !schema.extensions.is_empty() {
1296 output.push('\n');
1297 }
1298
1299 for enum_type in &schema.enums {
1301 let values = enum_type
1302 .values
1303 .iter()
1304 .map(|v| v.as_str())
1305 .collect::<Vec<_>>()
1306 .join(", ");
1307 output.push_str(&format!("enum {} {{ {} }}\n", enum_type.name, values));
1308 }
1309 if !schema.enums.is_empty() {
1310 output.push('\n');
1311 }
1312
1313 for seq in &schema.sequences {
1315 if seq.start.is_some()
1316 || seq.increment.is_some()
1317 || seq.min_value.is_some()
1318 || seq.max_value.is_some()
1319 || seq.cache.is_some()
1320 || seq.cycle
1321 || seq.owned_by.is_some()
1322 {
1323 let mut opts = Vec::new();
1324 if let Some(v) = seq.start {
1325 opts.push(format!("start {}", v));
1326 }
1327 if let Some(v) = seq.increment {
1328 opts.push(format!("increment {}", v));
1329 }
1330 if let Some(v) = seq.min_value {
1331 opts.push(format!("minvalue {}", v));
1332 }
1333 if let Some(v) = seq.max_value {
1334 opts.push(format!("maxvalue {}", v));
1335 }
1336 if let Some(v) = seq.cache {
1337 opts.push(format!("cache {}", v));
1338 }
1339 if seq.cycle {
1340 opts.push("cycle".to_string());
1341 }
1342 if let Some(ref o) = seq.owned_by {
1343 opts.push(format!("owned_by {}", o));
1344 }
1345 output.push_str(&format!("sequence {} {{ {} }}\n", seq.name, opts.join(" ")));
1346 } else {
1347 output.push_str(&format!("sequence {}\n", seq.name));
1348 }
1349 }
1350 if !schema.sequences.is_empty() {
1351 output.push('\n');
1352 }
1353
1354 let mut table_names: Vec<&String> = schema.tables.keys().collect();
1355 table_names.sort();
1356 for table_name in table_names {
1357 let table = &schema.tables[table_name];
1358 output.push_str(&format!("table {} {{\n", table.name));
1359 for col in &table.columns {
1360 let mut constraints: Vec<String> = Vec::new();
1361 if col.primary_key {
1362 constraints.push("primary_key".to_string());
1363 }
1364 if !col.nullable && !col.primary_key {
1365 constraints.push("not_null".to_string());
1366 }
1367 if col.unique {
1368 constraints.push("unique".to_string());
1369 }
1370 if let Some(def) = &col.default {
1371 constraints.push(format!("default {}", def));
1372 }
1373 if let Some(ref fk) = col.foreign_key {
1374 let mut fk_str = format!("references {}({})", fk.table, fk.column);
1375 if fk.on_delete != FkAction::NoAction {
1376 fk_str.push_str(&format!(" on_delete {}", fk_action_str(&fk.on_delete)));
1377 }
1378 if fk.on_update != FkAction::NoAction {
1379 fk_str.push_str(&format!(" on_update {}", fk_action_str(&fk.on_update)));
1380 }
1381 match &fk.deferrable {
1382 Deferrable::Deferrable => fk_str.push_str(" deferrable"),
1383 Deferrable::InitiallyDeferred => fk_str.push_str(" initially_deferred"),
1384 Deferrable::InitiallyImmediate => fk_str.push_str(" initially_immediate"),
1385 Deferrable::NotDeferrable => {} }
1387 constraints.push(fk_str);
1388 }
1389 if let Some(ref check) = col.check {
1390 constraints.push(format!("check({})", check_expr_str(&check.expr)));
1391 }
1392
1393 let constraint_str = if constraints.is_empty() {
1394 String::new()
1395 } else {
1396 format!(" {}", constraints.join(" "))
1397 };
1398
1399 output.push_str(&format!(
1400 " {} {}{}\n",
1401 col.name,
1402 col.data_type.to_pg_type(),
1403 constraint_str
1404 ));
1405 }
1406 for fk in &table.multi_column_fks {
1408 output.push_str(&format!(
1409 " foreign_key ({}) references {}({})\n",
1410 fk.columns.join(", "),
1411 fk.ref_table,
1412 fk.ref_columns.join(", ")
1413 ));
1414 }
1415 if table.enable_rls {
1417 output.push_str(" enable_rls\n");
1418 }
1419 if table.force_rls {
1420 output.push_str(" force_rls\n");
1421 }
1422 output.push_str("}\n\n");
1423 }
1424
1425 for idx in &schema.indexes {
1426 let unique = if idx.unique { "unique " } else { "" };
1427 let cols = if !idx.expressions.is_empty() {
1428 idx.expressions.join(", ")
1429 } else {
1430 idx.columns.join(", ")
1431 };
1432 output.push_str(&format!(
1433 "{}index {} on {} ({})\n",
1434 unique, idx.name, idx.table, cols
1435 ));
1436 }
1437
1438 for hint in &schema.migrations {
1439 match hint {
1440 MigrationHint::Rename { from, to } => {
1441 output.push_str(&format!("rename {} -> {}\n", from, to));
1442 }
1443 MigrationHint::Transform { expression, target } => {
1444 output.push_str(&format!("transform {} -> {}\n", expression, target));
1445 }
1446 MigrationHint::Drop { target, confirmed } => {
1447 let confirm = if *confirmed { " confirm" } else { "" };
1448 output.push_str(&format!("drop {}{}\n", target, confirm));
1449 }
1450 }
1451 }
1452
1453 for view in &schema.views {
1455 let prefix = if view.materialized {
1456 "materialized view"
1457 } else {
1458 "view"
1459 };
1460 output.push_str(&format!(
1461 "{} {} $$\n{}\n$$\n\n",
1462 prefix, view.name, view.query
1463 ));
1464 }
1465
1466 for func in &schema.functions {
1468 let args = func.args.join(", ");
1469 output.push_str(&format!(
1470 "function {}({}) returns {} language {} $$\n{}\n$$\n\n",
1471 func.name, args, func.returns, func.language, func.body
1472 ));
1473 }
1474
1475 for trigger in &schema.triggers {
1477 let events = trigger.events.join(" or ");
1478 output.push_str(&format!(
1479 "trigger {} on {} {} {} execute {}\n",
1480 trigger.name,
1481 trigger.table,
1482 trigger.timing.to_lowercase(),
1483 events.to_lowercase(),
1484 trigger.execute_function
1485 ));
1486 }
1487 if !schema.triggers.is_empty() {
1488 output.push('\n');
1489 }
1490
1491 for policy in &schema.policies {
1493 let cmd = match policy.target {
1494 PolicyTarget::All => "all",
1495 PolicyTarget::Select => "select",
1496 PolicyTarget::Insert => "insert",
1497 PolicyTarget::Update => "update",
1498 PolicyTarget::Delete => "delete",
1499 };
1500 let perm = match policy.permissiveness {
1501 PolicyPermissiveness::Permissive => "",
1502 PolicyPermissiveness::Restrictive => " restrictive",
1503 };
1504 let role_str = match &policy.role {
1505 Some(r) => format!(" to {}", r),
1506 None => String::new(),
1507 };
1508 output.push_str(&format!(
1509 "policy {} on {} for {}{}{}",
1510 policy.name, policy.table, cmd, role_str, perm
1511 ));
1512 if let Some(ref using) = policy.using {
1513 output.push_str(&format!("\n using $$ {} $$", using));
1514 }
1515 if let Some(ref wc) = policy.with_check {
1516 output.push_str(&format!("\n with_check $$ {} $$", wc));
1517 }
1518 output.push_str("\n\n");
1519 }
1520
1521 for grant in &schema.grants {
1523 let privs: Vec<String> = grant
1524 .privileges
1525 .iter()
1526 .map(|p| p.to_string().to_lowercase())
1527 .collect();
1528 match grant.action {
1529 GrantAction::Grant => {
1530 output.push_str(&format!(
1531 "grant {} on {} to {}\n",
1532 privs.join(", "),
1533 grant.on_object,
1534 grant.to_role
1535 ));
1536 }
1537 GrantAction::Revoke => {
1538 output.push_str(&format!(
1539 "revoke {} on {} from {}\n",
1540 privs.join(", "),
1541 grant.on_object,
1542 grant.to_role
1543 ));
1544 }
1545 }
1546 }
1547 if !schema.grants.is_empty() {
1548 output.push('\n');
1549 }
1550
1551 for comment in &schema.comments {
1553 match &comment.target {
1554 CommentTarget::Table(t) => {
1555 output.push_str(&format!("comment on {} \"{}\"\n", t, comment.text));
1556 }
1557 CommentTarget::Column { table, column } => {
1558 output.push_str(&format!(
1559 "comment on {}.{} \"{}\"\n",
1560 table, column, comment.text
1561 ));
1562 }
1563 }
1564 }
1565
1566 output
1567}
1568
1569pub fn schema_to_commands(schema: &Schema) -> Vec<crate::ast::Qail> {
1572 use crate::ast::{Action, Constraint, Expr, IndexDef, Qail};
1573
1574 let mut cmds = Vec::new();
1575
1576 let mut table_order: Vec<&Table> = schema.tables.values().collect();
1578 table_order.sort_by(|a, b| {
1579 let a_has_fk = a.columns.iter().any(|c| c.foreign_key.is_some());
1580 let b_has_fk = b.columns.iter().any(|c| c.foreign_key.is_some());
1581 a_has_fk.cmp(&b_has_fk)
1582 });
1583
1584 for table in table_order {
1585 let columns: Vec<Expr> = table
1587 .columns
1588 .iter()
1589 .map(|col| {
1590 let mut constraints = Vec::new();
1591
1592 if col.primary_key {
1593 constraints.push(Constraint::PrimaryKey);
1594 }
1595 if col.nullable {
1596 constraints.push(Constraint::Nullable);
1597 }
1598 if col.unique {
1599 constraints.push(Constraint::Unique);
1600 }
1601 if let Some(def) = &col.default {
1602 constraints.push(Constraint::Default(def.clone()));
1603 }
1604 if let Some(ref fk) = col.foreign_key {
1605 constraints.push(Constraint::References(format!(
1606 "{}({})",
1607 fk.table, fk.column
1608 )));
1609 }
1610
1611 Expr::Def {
1612 name: col.name.clone(),
1613 data_type: col.data_type.to_pg_type(),
1614 constraints,
1615 }
1616 })
1617 .collect();
1618
1619 cmds.push(Qail {
1620 action: Action::Make,
1621 table: table.name.clone(),
1622 columns,
1623 ..Default::default()
1624 });
1625 }
1626
1627 for idx in &schema.indexes {
1629 cmds.push(Qail {
1630 action: Action::Index,
1631 table: String::new(),
1632 index_def: Some(IndexDef {
1633 name: idx.name.clone(),
1634 table: idx.table.clone(),
1635 columns: idx.columns.clone(),
1636 unique: idx.unique,
1637 index_type: None,
1638 }),
1639 ..Default::default()
1640 });
1641 }
1642
1643 cmds
1644}
1645
1646#[cfg(test)]
1647mod tests {
1648 use super::*;
1649
1650 #[test]
1651 fn test_schema_builder() {
1652 let mut schema = Schema::new();
1653
1654 let users = Table::new("users")
1655 .column(Column::new("id", ColumnType::Serial).primary_key())
1656 .column(Column::new("name", ColumnType::Text).not_null())
1657 .column(Column::new("email", ColumnType::Text).unique());
1658
1659 schema.add_table(users);
1660 schema.add_index(Index::new("idx_users_email", "users", vec!["email".into()]).unique());
1661
1662 let output = to_qail_string(&schema);
1663 assert!(output.contains("table users"));
1664 assert!(output.contains("id SERIAL primary_key"));
1665 assert!(output.contains("unique index idx_users_email"));
1666 }
1667
1668 #[test]
1669 fn test_migration_hints() {
1670 let mut schema = Schema::new();
1671 schema.add_hint(MigrationHint::Rename {
1672 from: "users.username".into(),
1673 to: "users.name".into(),
1674 });
1675
1676 let output = to_qail_string(&schema);
1677 assert!(output.contains("rename users.username -> users.name"));
1678 }
1679
1680 #[test]
1681 fn test_invalid_primary_key_type_strict() {
1682 let err = Column::new("data", ColumnType::Jsonb)
1683 .try_primary_key()
1684 .expect_err("JSONB should be rejected by strict PK policy");
1685 assert!(err.contains("cannot be a primary key"));
1686 }
1687
1688 #[test]
1689 fn test_invalid_primary_key_type_fail_soft() {
1690 let col = Column::new("data", ColumnType::Jsonb).primary_key();
1691 assert!(col.primary_key);
1692 assert!(!col.nullable);
1693 }
1694
1695 #[test]
1696 fn test_invalid_unique_type_strict() {
1697 let err = Column::new("data", ColumnType::Jsonb)
1698 .try_unique()
1699 .expect_err("JSONB should be rejected by strict UNIQUE policy");
1700 assert!(err.contains("cannot have UNIQUE"));
1701 }
1702
1703 #[test]
1704 fn test_invalid_unique_type_fail_soft() {
1705 let col = Column::new("data", ColumnType::Jsonb).unique();
1706 assert!(col.unique);
1707 }
1708
1709 #[test]
1710 fn test_foreign_key_valid() {
1711 let mut schema = Schema::new();
1712
1713 schema.add_table(
1714 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1715 );
1716
1717 schema.add_table(
1718 Table::new("posts")
1719 .column(Column::new("id", ColumnType::Uuid).primary_key())
1720 .column(
1721 Column::new("user_id", ColumnType::Uuid)
1722 .references("users", "id")
1723 .on_delete(FkAction::Cascade),
1724 ),
1725 );
1726
1727 assert!(schema.validate().is_ok());
1729 }
1730
1731 #[test]
1732 fn test_foreign_key_invalid_table() {
1733 let mut schema = Schema::new();
1734
1735 schema.add_table(
1736 Table::new("posts")
1737 .column(Column::new("id", ColumnType::Uuid).primary_key())
1738 .column(Column::new("user_id", ColumnType::Uuid).references("nonexistent", "id")),
1739 );
1740
1741 let result = schema.validate();
1743 assert!(result.is_err());
1744 assert!(result.unwrap_err()[0].contains("non-existent table"));
1745 }
1746
1747 #[test]
1748 fn test_foreign_key_invalid_column() {
1749 let mut schema = Schema::new();
1750
1751 schema.add_table(
1752 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1753 );
1754
1755 schema.add_table(
1756 Table::new("posts")
1757 .column(Column::new("id", ColumnType::Uuid).primary_key())
1758 .column(
1759 Column::new("user_id", ColumnType::Uuid).references("users", "wrong_column"),
1760 ),
1761 );
1762
1763 let result = schema.validate();
1765 assert!(result.is_err());
1766 assert!(result.unwrap_err()[0].contains("non-existent column"));
1767 }
1768}