1use super::types::ColumnType;
18use super::policy::{RlsPolicy, PolicyTarget, PolicyPermissiveness};
19use std::collections::HashMap;
20
21#[derive(Debug, Clone, Default)]
23pub struct Schema {
24 pub tables: HashMap<String, Table>,
25 pub indexes: Vec<Index>,
26 pub migrations: Vec<MigrationHint>,
27 pub extensions: Vec<Extension>,
29 pub comments: Vec<Comment>,
31 pub sequences: Vec<Sequence>,
33 pub enums: Vec<EnumType>,
35 pub views: Vec<ViewDef>,
37 pub functions: Vec<SchemaFunctionDef>,
39 pub triggers: Vec<SchemaTriggerDef>,
41 pub grants: Vec<Grant>,
43 pub policies: Vec<RlsPolicy>,
45}
46
47#[derive(Debug, Clone)]
48pub struct Table {
49 pub name: String,
50 pub columns: Vec<Column>,
51 pub multi_column_fks: Vec<MultiColumnForeignKey>,
53 pub enable_rls: bool,
55 pub force_rls: bool,
57}
58
59#[derive(Debug, Clone)]
61pub struct Column {
62 pub name: String,
63 pub data_type: ColumnType,
64 pub nullable: bool,
65 pub primary_key: bool,
66 pub unique: bool,
67 pub default: Option<String>,
68 pub foreign_key: Option<ForeignKey>,
69 pub check: Option<CheckConstraint>,
71 pub generated: Option<Generated>,
73}
74
75#[derive(Debug, Clone)]
77pub struct ForeignKey {
78 pub table: String,
79 pub column: String,
80 pub on_delete: FkAction,
81 pub on_update: FkAction,
82 pub deferrable: Deferrable,
84}
85
86#[derive(Debug, Clone, Default, PartialEq)]
88pub enum FkAction {
89 #[default]
90 NoAction,
91 Cascade,
92 SetNull,
93 SetDefault,
94 Restrict,
95}
96
97#[derive(Debug, Clone)]
98pub struct Index {
99 pub name: String,
100 pub table: String,
101 pub columns: Vec<String>,
102 pub unique: bool,
103 pub method: IndexMethod,
105 pub where_clause: Option<CheckExpr>,
107 pub include: Vec<String>,
109 pub concurrently: bool,
111 pub expressions: Vec<String>,
113}
114
115#[derive(Debug, Clone)]
116pub enum MigrationHint {
117 Rename { from: String, to: String },
119 Transform { expression: String, target: String },
121 Drop { target: String, confirmed: bool },
123}
124
125#[derive(Debug, Clone)]
131pub enum CheckExpr {
132 GreaterThan { column: String, value: i64 },
134 GreaterOrEqual { column: String, value: i64 },
136 LessThan { column: String, value: i64 },
138 LessOrEqual { column: String, value: i64 },
140 Between { column: String, low: i64, high: i64 },
141 In { column: String, values: Vec<String> },
142 Regex { column: String, pattern: String },
144 MaxLength { column: String, max: usize },
146 MinLength { column: String, min: usize },
148 NotNull { column: String },
149 And(Box<CheckExpr>, Box<CheckExpr>),
150 Or(Box<CheckExpr>, Box<CheckExpr>),
151 Not(Box<CheckExpr>),
152}
153
154#[derive(Debug, Clone)]
156pub struct CheckConstraint {
157 pub expr: CheckExpr,
158 pub name: Option<String>,
159}
160
161#[derive(Debug, Clone, Default, PartialEq)]
167pub enum Deferrable {
168 #[default]
169 NotDeferrable,
170 Deferrable,
171 InitiallyDeferred,
172 InitiallyImmediate,
173}
174
175#[derive(Debug, Clone)]
181pub enum Generated {
182 AlwaysStored(String),
184 AlwaysIdentity,
186 ByDefaultIdentity,
188}
189
190#[derive(Debug, Clone, Default, PartialEq)]
196pub enum IndexMethod {
197 #[default]
198 BTree,
199 Hash,
200 Gin,
201 Gist,
202 Brin,
203 SpGist,
204}
205
206#[derive(Debug, Clone, PartialEq)]
212pub struct Extension {
213 pub name: String,
214 pub schema: Option<String>,
215 pub version: Option<String>,
216}
217
218impl Extension {
219 pub fn new(name: impl Into<String>) -> Self {
220 Self {
221 name: name.into(),
222 schema: None,
223 version: None,
224 }
225 }
226
227 pub fn schema(mut self, schema: impl Into<String>) -> Self {
228 self.schema = Some(schema.into());
229 self
230 }
231
232 pub fn version(mut self, version: impl Into<String>) -> Self {
233 self.version = Some(version.into());
234 self
235 }
236}
237
238#[derive(Debug, Clone, PartialEq)]
240pub struct Comment {
241 pub target: CommentTarget,
242 pub text: String,
243}
244
245#[derive(Debug, Clone, PartialEq)]
246pub enum CommentTarget {
247 Table(String),
248 Column { table: String, column: String },
249}
250
251impl Comment {
252 pub fn on_table(table: impl Into<String>, text: impl Into<String>) -> Self {
253 Self {
254 target: CommentTarget::Table(table.into()),
255 text: text.into(),
256 }
257 }
258
259 pub fn on_column(
260 table: impl Into<String>,
261 column: impl Into<String>,
262 text: impl Into<String>,
263 ) -> Self {
264 Self {
265 target: CommentTarget::Column {
266 table: table.into(),
267 column: column.into(),
268 },
269 text: text.into(),
270 }
271 }
272}
273
274#[derive(Debug, Clone, PartialEq)]
276pub struct Sequence {
277 pub name: String,
278 pub data_type: Option<String>,
279 pub start: Option<i64>,
280 pub increment: Option<i64>,
281 pub min_value: Option<i64>,
282 pub max_value: Option<i64>,
283 pub cache: Option<i64>,
284 pub cycle: bool,
285 pub owned_by: Option<String>,
286}
287
288impl Sequence {
289 pub fn new(name: impl Into<String>) -> Self {
290 Self {
291 name: name.into(),
292 data_type: None,
293 start: None,
294 increment: None,
295 min_value: None,
296 max_value: None,
297 cache: None,
298 cycle: false,
299 owned_by: None,
300 }
301 }
302
303 pub fn start(mut self, v: i64) -> Self {
304 self.start = Some(v);
305 self
306 }
307
308 pub fn increment(mut self, v: i64) -> Self {
309 self.increment = Some(v);
310 self
311 }
312
313 pub fn min_value(mut self, v: i64) -> Self {
314 self.min_value = Some(v);
315 self
316 }
317
318 pub fn max_value(mut self, v: i64) -> Self {
319 self.max_value = Some(v);
320 self
321 }
322
323 pub fn cache(mut self, v: i64) -> Self {
324 self.cache = Some(v);
325 self
326 }
327
328 pub fn cycle(mut self) -> Self {
329 self.cycle = true;
330 self
331 }
332
333 pub fn owned_by(mut self, col: impl Into<String>) -> Self {
334 self.owned_by = Some(col.into());
335 self
336 }
337}
338
339#[derive(Debug, Clone, PartialEq)]
345pub struct EnumType {
346 pub name: String,
347 pub values: Vec<String>,
348}
349
350impl EnumType {
351 pub fn new(name: impl Into<String>, values: Vec<String>) -> Self {
352 Self {
353 name: name.into(),
354 values,
355 }
356 }
357
358 pub fn add_value(mut self, value: impl Into<String>) -> Self {
360 self.values.push(value.into());
361 self
362 }
363}
364
365#[derive(Debug, Clone, PartialEq)]
367pub struct MultiColumnForeignKey {
368 pub columns: Vec<String>,
369 pub ref_table: String,
370 pub ref_columns: Vec<String>,
371 pub on_delete: FkAction,
372 pub on_update: FkAction,
373 pub deferrable: Deferrable,
374 pub name: Option<String>,
375}
376
377impl MultiColumnForeignKey {
378 pub fn new(
379 columns: Vec<String>,
380 ref_table: impl Into<String>,
381 ref_columns: Vec<String>,
382 ) -> Self {
383 Self {
384 columns,
385 ref_table: ref_table.into(),
386 ref_columns,
387 on_delete: FkAction::default(),
388 on_update: FkAction::default(),
389 deferrable: Deferrable::default(),
390 name: None,
391 }
392 }
393
394 pub fn on_delete(mut self, action: FkAction) -> Self {
395 self.on_delete = action;
396 self
397 }
398
399 pub fn on_update(mut self, action: FkAction) -> Self {
400 self.on_update = action;
401 self
402 }
403
404 pub fn named(mut self, name: impl Into<String>) -> Self {
405 self.name = Some(name.into());
406 self
407 }
408}
409
410#[derive(Debug, Clone, PartialEq)]
416pub struct ViewDef {
417 pub name: String,
418 pub query: String,
419 pub materialized: bool,
420}
421
422impl ViewDef {
423 pub fn new(name: impl Into<String>, query: impl Into<String>) -> Self {
424 Self {
425 name: name.into(),
426 query: query.into(),
427 materialized: false,
428 }
429 }
430
431 pub fn materialized(mut self) -> Self {
432 self.materialized = true;
433 self
434 }
435}
436
437#[derive(Debug, Clone, PartialEq)]
439pub struct SchemaFunctionDef {
440 pub name: String,
441 pub args: Vec<String>,
442 pub returns: String,
443 pub body: String,
444 pub language: String,
445 pub volatility: Option<String>,
446}
447
448impl SchemaFunctionDef {
449 pub fn new(
450 name: impl Into<String>,
451 returns: impl Into<String>,
452 body: impl Into<String>,
453 ) -> Self {
454 Self {
455 name: name.into(),
456 args: Vec::new(),
457 returns: returns.into(),
458 body: body.into(),
459 language: "plpgsql".to_string(),
460 volatility: None,
461 }
462 }
463
464 pub fn language(mut self, lang: impl Into<String>) -> Self {
465 self.language = lang.into();
466 self
467 }
468
469 pub fn arg(mut self, arg: impl Into<String>) -> Self {
470 self.args.push(arg.into());
471 self
472 }
473
474 pub fn volatility(mut self, v: impl Into<String>) -> Self {
475 self.volatility = Some(v.into());
476 self
477 }
478}
479
480#[derive(Debug, Clone, PartialEq)]
482pub struct SchemaTriggerDef {
483 pub name: String,
484 pub table: String,
485 pub timing: String,
486 pub events: Vec<String>,
487 pub for_each_row: bool,
488 pub execute_function: String,
489 pub condition: Option<String>,
490}
491
492impl SchemaTriggerDef {
493 pub fn new(
494 name: impl Into<String>,
495 table: impl Into<String>,
496 execute_function: impl Into<String>,
497 ) -> Self {
498 Self {
499 name: name.into(),
500 table: table.into(),
501 timing: "BEFORE".to_string(),
502 events: vec!["INSERT".to_string()],
503 for_each_row: true,
504 execute_function: execute_function.into(),
505 condition: None,
506 }
507 }
508
509 pub fn timing(mut self, t: impl Into<String>) -> Self {
510 self.timing = t.into();
511 self
512 }
513
514 pub fn events(mut self, evts: Vec<String>) -> Self {
515 self.events = evts;
516 self
517 }
518
519 pub fn for_each_statement(mut self) -> Self {
520 self.for_each_row = false;
521 self
522 }
523
524 pub fn condition(mut self, cond: impl Into<String>) -> Self {
525 self.condition = Some(cond.into());
526 self
527 }
528}
529
530#[derive(Debug, Clone, PartialEq)]
532pub struct Grant {
533 pub action: GrantAction,
534 pub privileges: Vec<Privilege>,
535 pub on_object: String,
536 pub to_role: String,
537}
538
539#[derive(Debug, Clone, PartialEq, Default)]
540pub enum GrantAction {
541 #[default]
542 Grant,
543 Revoke,
544}
545
546#[derive(Debug, Clone, PartialEq)]
547pub enum Privilege {
548 All,
549 Select,
550 Insert,
551 Update,
552 Delete,
553 Usage,
554 Execute,
555}
556
557impl std::fmt::Display for Privilege {
558 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
559 match self {
560 Privilege::All => write!(f, "ALL"),
561 Privilege::Select => write!(f, "SELECT"),
562 Privilege::Insert => write!(f, "INSERT"),
563 Privilege::Update => write!(f, "UPDATE"),
564 Privilege::Delete => write!(f, "DELETE"),
565 Privilege::Usage => write!(f, "USAGE"),
566 Privilege::Execute => write!(f, "EXECUTE"),
567 }
568 }
569}
570
571impl Grant {
572 pub fn new(
573 privileges: Vec<Privilege>,
574 on_object: impl Into<String>,
575 to_role: impl Into<String>,
576 ) -> Self {
577 Self {
578 action: GrantAction::Grant,
579 privileges,
580 on_object: on_object.into(),
581 to_role: to_role.into(),
582 }
583 }
584
585 pub fn revoke(
586 privileges: Vec<Privilege>,
587 on_object: impl Into<String>,
588 from_role: impl Into<String>,
589 ) -> Self {
590 Self {
591 action: GrantAction::Revoke,
592 privileges,
593 on_object: on_object.into(),
594 to_role: from_role.into(),
595 }
596 }
597}
598
599impl Schema {
600 pub fn new() -> Self {
601 Self::default()
602 }
603
604 pub fn add_table(&mut self, table: Table) {
605 self.tables.insert(table.name.clone(), table);
606 }
607
608 pub fn add_index(&mut self, index: Index) {
609 self.indexes.push(index);
610 }
611
612 pub fn add_hint(&mut self, hint: MigrationHint) {
613 self.migrations.push(hint);
614 }
615
616 pub fn add_extension(&mut self, ext: Extension) {
617 self.extensions.push(ext);
618 }
619
620 pub fn add_comment(&mut self, comment: Comment) {
621 self.comments.push(comment);
622 }
623
624 pub fn add_sequence(&mut self, seq: Sequence) {
625 self.sequences.push(seq);
626 }
627
628 pub fn add_enum(&mut self, enum_type: EnumType) {
629 self.enums.push(enum_type);
630 }
631
632 pub fn add_view(&mut self, view: ViewDef) {
633 self.views.push(view);
634 }
635
636 pub fn add_function(&mut self, func: SchemaFunctionDef) {
637 self.functions.push(func);
638 }
639
640 pub fn add_trigger(&mut self, trigger: SchemaTriggerDef) {
641 self.triggers.push(trigger);
642 }
643
644 pub fn add_grant(&mut self, grant: Grant) {
645 self.grants.push(grant);
646 }
647
648 pub fn validate(&self) -> Result<(), Vec<String>> {
650 let mut errors = Vec::new();
651
652 for table in self.tables.values() {
653 for col in &table.columns {
654 if let Some(ref fk) = col.foreign_key {
655 if !self.tables.contains_key(&fk.table) {
656 errors.push(format!(
657 "FK error: {}.{} references non-existent table '{}'",
658 table.name, col.name, fk.table
659 ));
660 } else {
661 let ref_table = &self.tables[&fk.table];
662 if !ref_table.columns.iter().any(|c| c.name == fk.column) {
663 errors.push(format!(
664 "FK error: {}.{} references non-existent column '{}.{}'",
665 table.name, col.name, fk.table, fk.column
666 ));
667 }
668 }
669 }
670 }
671 }
672
673 if errors.is_empty() {
674 Ok(())
675 } else {
676 Err(errors)
677 }
678 }
679}
680
681impl Table {
682 pub fn new(name: impl Into<String>) -> Self {
683 Self {
684 name: name.into(),
685 columns: Vec::new(),
686 multi_column_fks: Vec::new(),
687 enable_rls: false,
688 force_rls: false,
689 }
690 }
691
692 pub fn column(mut self, col: Column) -> Self {
693 self.columns.push(col);
694 self
695 }
696
697 pub fn foreign_key(mut self, fk: MultiColumnForeignKey) -> Self {
699 self.multi_column_fks.push(fk);
700 self
701 }
702}
703
704impl Column {
705 pub fn new(name: impl Into<String>, data_type: ColumnType) -> Self {
707 Self {
708 name: name.into(),
709 data_type,
710 nullable: true,
711 primary_key: false,
712 unique: false,
713 default: None,
714 foreign_key: None,
715 check: None,
716 generated: None,
717 }
718 }
719
720 pub fn not_null(mut self) -> Self {
721 self.nullable = false;
722 self
723 }
724
725 pub fn primary_key(mut self) -> Self {
729 if !self.data_type.can_be_primary_key() {
730 panic!(
731 "Column '{}' of type {} cannot be a primary key. \
732 Valid PK types: UUID, SERIAL, BIGSERIAL, INT, BIGINT",
733 self.name,
734 self.data_type.name()
735 );
736 }
737 self.primary_key = true;
738 self.nullable = false;
739 self
740 }
741
742 pub fn unique(mut self) -> Self {
745 if !self.data_type.supports_indexing() {
746 panic!(
747 "Column '{}' of type {} cannot have UNIQUE constraint. \
748 JSONB and BYTEA types do not support standard indexing.",
749 self.name,
750 self.data_type.name()
751 );
752 }
753 self.unique = true;
754 self
755 }
756
757 pub fn default(mut self, val: impl Into<String>) -> Self {
758 self.default = Some(val.into());
759 self
760 }
761
762 pub fn references(mut self, table: &str, column: &str) -> Self {
770 self.foreign_key = Some(ForeignKey {
771 table: table.to_string(),
772 column: column.to_string(),
773 on_delete: FkAction::default(),
774 on_update: FkAction::default(),
775 deferrable: Deferrable::default(),
776 });
777 self
778 }
779
780 pub fn on_delete(mut self, action: FkAction) -> Self {
782 if let Some(ref mut fk) = self.foreign_key {
783 fk.on_delete = action;
784 }
785 self
786 }
787
788 pub fn on_update(mut self, action: FkAction) -> Self {
790 if let Some(ref mut fk) = self.foreign_key {
791 fk.on_update = action;
792 }
793 self
794 }
795
796 pub fn check(mut self, expr: CheckExpr) -> Self {
800 self.check = Some(CheckConstraint { expr, name: None });
801 self
802 }
803
804 pub fn check_named(mut self, name: impl Into<String>, expr: CheckExpr) -> Self {
806 self.check = Some(CheckConstraint {
807 expr,
808 name: Some(name.into()),
809 });
810 self
811 }
812
813 pub fn deferrable(mut self) -> Self {
817 if let Some(ref mut fk) = self.foreign_key {
818 fk.deferrable = Deferrable::Deferrable;
819 }
820 self
821 }
822
823 pub fn initially_deferred(mut self) -> Self {
825 if let Some(ref mut fk) = self.foreign_key {
826 fk.deferrable = Deferrable::InitiallyDeferred;
827 }
828 self
829 }
830
831 pub fn initially_immediate(mut self) -> Self {
833 if let Some(ref mut fk) = self.foreign_key {
834 fk.deferrable = Deferrable::InitiallyImmediate;
835 }
836 self
837 }
838
839 pub fn generated_stored(mut self, expr: impl Into<String>) -> Self {
843 self.generated = Some(Generated::AlwaysStored(expr.into()));
844 self
845 }
846
847 pub fn generated_identity(mut self) -> Self {
849 self.generated = Some(Generated::AlwaysIdentity);
850 self
851 }
852
853 pub fn generated_by_default(mut self) -> Self {
855 self.generated = Some(Generated::ByDefaultIdentity);
856 self
857 }
858}
859
860impl Index {
861 pub fn new(name: impl Into<String>, table: impl Into<String>, columns: Vec<String>) -> Self {
862 Self {
863 name: name.into(),
864 table: table.into(),
865 columns,
866 unique: false,
867 method: IndexMethod::default(),
868 where_clause: None,
869 include: Vec::new(),
870 concurrently: false,
871 expressions: Vec::new(),
872 }
873 }
874
875 pub fn expression(
877 name: impl Into<String>,
878 table: impl Into<String>,
879 expressions: Vec<String>,
880 ) -> Self {
881 Self {
882 name: name.into(),
883 table: table.into(),
884 columns: Vec::new(),
885 unique: false,
886 method: IndexMethod::default(),
887 where_clause: None,
888 include: Vec::new(),
889 concurrently: false,
890 expressions,
891 }
892 }
893
894 pub fn unique(mut self) -> Self {
895 self.unique = true;
896 self
897 }
898
899 pub fn using(mut self, method: IndexMethod) -> Self {
903 self.method = method;
904 self
905 }
906
907 pub fn partial(mut self, expr: CheckExpr) -> Self {
909 self.where_clause = Some(expr);
910 self
911 }
912
913 pub fn include(mut self, cols: Vec<String>) -> Self {
915 self.include = cols;
916 self
917 }
918
919 pub fn concurrently(mut self) -> Self {
921 self.concurrently = true;
922 self
923 }
924}
925
926fn fk_action_str(action: &FkAction) -> &'static str {
929 match action {
930 FkAction::NoAction => "no_action",
931 FkAction::Cascade => "cascade",
932 FkAction::SetNull => "set_null",
933 FkAction::SetDefault => "set_default",
934 FkAction::Restrict => "restrict",
935 }
936}
937
938fn check_expr_str(expr: &CheckExpr) -> String {
940 match expr {
941 CheckExpr::GreaterThan { column, value } => format!("{} > {}", column, value),
942 CheckExpr::GreaterOrEqual { column, value } => format!("{} >= {}", column, value),
943 CheckExpr::LessThan { column, value } => format!("{} < {}", column, value),
944 CheckExpr::LessOrEqual { column, value } => format!("{} <= {}", column, value),
945 CheckExpr::Between { column, low, high } => format!("{} between {} {}", column, low, high),
946 CheckExpr::In { column, values } => format!("{} in [{}]", column, values.join(", ")),
947 CheckExpr::Regex { column, pattern } => format!("{} ~ '{}'", column, pattern),
948 CheckExpr::MaxLength { column, max } => format!("length({}) <= {}", column, max),
949 CheckExpr::MinLength { column, min } => format!("length({}) >= {}", column, min),
950 CheckExpr::NotNull { column } => format!("{} not_null", column),
951 CheckExpr::And(l, r) => format!("{} and {}", check_expr_str(l), check_expr_str(r)),
952 CheckExpr::Or(l, r) => format!("{} or {}", check_expr_str(l), check_expr_str(r)),
953 CheckExpr::Not(e) => format!("not {}", check_expr_str(e)),
954 }
955}
956
957
958pub fn to_qail_string(schema: &Schema) -> String {
959 let mut output = String::new();
960 output.push_str("# QAIL Schema\n\n");
961
962 for ext in &schema.extensions {
964 let mut line = format!("extension \"{}\"", ext.name);
965 if let Some(ref s) = ext.schema {
966 line.push_str(&format!(" schema {}", s));
967 }
968 if let Some(ref v) = ext.version {
969 line.push_str(&format!(" version \"{}\"", v));
970 }
971 output.push_str(&line);
972 output.push('\n');
973 }
974 if !schema.extensions.is_empty() {
975 output.push('\n');
976 }
977
978 for enum_type in &schema.enums {
980 let values = enum_type
981 .values
982 .iter()
983 .map(|v| v.as_str())
984 .collect::<Vec<_>>()
985 .join(", ");
986 output.push_str(&format!("enum {} {{ {} }}\n", enum_type.name, values));
987 }
988 if !schema.enums.is_empty() {
989 output.push('\n');
990 }
991
992 for seq in &schema.sequences {
994 if seq.start.is_some()
995 || seq.increment.is_some()
996 || seq.min_value.is_some()
997 || seq.max_value.is_some()
998 || seq.cache.is_some()
999 || seq.cycle
1000 || seq.owned_by.is_some()
1001 {
1002 let mut opts = Vec::new();
1003 if let Some(v) = seq.start {
1004 opts.push(format!("start {}", v));
1005 }
1006 if let Some(v) = seq.increment {
1007 opts.push(format!("increment {}", v));
1008 }
1009 if let Some(v) = seq.min_value {
1010 opts.push(format!("minvalue {}", v));
1011 }
1012 if let Some(v) = seq.max_value {
1013 opts.push(format!("maxvalue {}", v));
1014 }
1015 if let Some(v) = seq.cache {
1016 opts.push(format!("cache {}", v));
1017 }
1018 if seq.cycle {
1019 opts.push("cycle".to_string());
1020 }
1021 if let Some(ref o) = seq.owned_by {
1022 opts.push(format!("owned_by {}", o));
1023 }
1024 output.push_str(&format!("sequence {} {{ {} }}\n", seq.name, opts.join(" ")));
1025 } else {
1026 output.push_str(&format!("sequence {}\n", seq.name));
1027 }
1028 }
1029 if !schema.sequences.is_empty() {
1030 output.push('\n');
1031 }
1032
1033 for table in schema.tables.values() {
1034 output.push_str(&format!("table {} {{\n", table.name));
1035 for col in &table.columns {
1036 let mut constraints: Vec<String> = Vec::new();
1037 if col.primary_key {
1038 constraints.push("primary_key".to_string());
1039 }
1040 if !col.nullable && !col.primary_key {
1041 constraints.push("not_null".to_string());
1042 }
1043 if col.unique {
1044 constraints.push("unique".to_string());
1045 }
1046 if let Some(def) = &col.default {
1047 constraints.push(format!("default {}", def));
1048 }
1049 if let Some(ref fk) = col.foreign_key {
1050 let mut fk_str = format!("references {}({})", fk.table, fk.column);
1051 if fk.on_delete != FkAction::NoAction {
1052 fk_str.push_str(&format!(" on_delete {}", fk_action_str(&fk.on_delete)));
1053 }
1054 if fk.on_update != FkAction::NoAction {
1055 fk_str.push_str(&format!(" on_update {}", fk_action_str(&fk.on_update)));
1056 }
1057 match &fk.deferrable {
1058 Deferrable::Deferrable => fk_str.push_str(" deferrable"),
1059 Deferrable::InitiallyDeferred => fk_str.push_str(" initially_deferred"),
1060 Deferrable::InitiallyImmediate => fk_str.push_str(" initially_immediate"),
1061 Deferrable::NotDeferrable => {} }
1063 constraints.push(fk_str);
1064 }
1065 if let Some(ref check) = col.check {
1066 constraints.push(format!("check({})", check_expr_str(&check.expr)));
1067 }
1068
1069 let constraint_str = if constraints.is_empty() {
1070 String::new()
1071 } else {
1072 format!(" {}", constraints.join(" "))
1073 };
1074
1075 output.push_str(&format!(
1076 " {} {}{}\n",
1077 col.name,
1078 col.data_type.to_pg_type(),
1079 constraint_str
1080 ));
1081 }
1082 for fk in &table.multi_column_fks {
1084 output.push_str(&format!(
1085 " foreign_key ({}) references {}({})\n",
1086 fk.columns.join(", "),
1087 fk.ref_table,
1088 fk.ref_columns.join(", ")
1089 ));
1090 }
1091 if table.enable_rls {
1093 output.push_str(" enable_rls\n");
1094 }
1095 if table.force_rls {
1096 output.push_str(" force_rls\n");
1097 }
1098 output.push_str("}\n\n");
1099 }
1100
1101 for idx in &schema.indexes {
1102 let unique = if idx.unique { "unique " } else { "" };
1103 let cols = if !idx.expressions.is_empty() {
1104 idx.expressions.join(", ")
1105 } else {
1106 idx.columns.join(", ")
1107 };
1108 output.push_str(&format!(
1109 "{}index {} on {} ({})\n",
1110 unique, idx.name, idx.table, cols
1111 ));
1112 }
1113
1114 for hint in &schema.migrations {
1115 match hint {
1116 MigrationHint::Rename { from, to } => {
1117 output.push_str(&format!("rename {} -> {}\n", from, to));
1118 }
1119 MigrationHint::Transform { expression, target } => {
1120 output.push_str(&format!("transform {} -> {}\n", expression, target));
1121 }
1122 MigrationHint::Drop { target, confirmed } => {
1123 let confirm = if *confirmed { " confirm" } else { "" };
1124 output.push_str(&format!("drop {}{}\n", target, confirm));
1125 }
1126 }
1127 }
1128
1129 for view in &schema.views {
1131 let prefix = if view.materialized {
1132 "materialized view"
1133 } else {
1134 "view"
1135 };
1136 output.push_str(&format!("{} {} $$\n{}\n$$\n\n", prefix, view.name, view.query));
1137 }
1138
1139 for func in &schema.functions {
1141 let args = func.args.join(", ");
1142 output.push_str(&format!(
1143 "function {}({}) returns {} language {} $$\n{}\n$$\n\n",
1144 func.name, args, func.returns, func.language, func.body
1145 ));
1146 }
1147
1148 for trigger in &schema.triggers {
1150 let events = trigger.events.join(" or ");
1151 output.push_str(&format!(
1152 "trigger {} on {} {} {} execute {}\n",
1153 trigger.name, trigger.table, trigger.timing.to_lowercase(),
1154 events.to_lowercase(), trigger.execute_function
1155 ));
1156 }
1157 if !schema.triggers.is_empty() {
1158 output.push('\n');
1159 }
1160
1161 for policy in &schema.policies {
1163 let cmd = match policy.target {
1164 PolicyTarget::All => "all",
1165 PolicyTarget::Select => "select",
1166 PolicyTarget::Insert => "insert",
1167 PolicyTarget::Update => "update",
1168 PolicyTarget::Delete => "delete",
1169 };
1170 let perm = match policy.permissiveness {
1171 PolicyPermissiveness::Permissive => "",
1172 PolicyPermissiveness::Restrictive => " restrictive",
1173 };
1174 let role_str = match &policy.role {
1175 Some(r) => format!(" to {}", r),
1176 None => String::new(),
1177 };
1178 output.push_str(&format!(
1179 "policy {} on {} for {}{}{}",
1180 policy.name, policy.table, cmd, role_str, perm
1181 ));
1182 if let Some(ref using) = policy.using {
1183 output.push_str(&format!("\n using $$ {} $$", using));
1184 }
1185 if let Some(ref wc) = policy.with_check {
1186 output.push_str(&format!("\n with_check $$ {} $$", wc));
1187 }
1188 output.push_str("\n\n");
1189 }
1190
1191 for grant in &schema.grants {
1193 let privs: Vec<String> = grant.privileges.iter().map(|p| p.to_string().to_lowercase()).collect();
1194 match grant.action {
1195 GrantAction::Grant => {
1196 output.push_str(&format!(
1197 "grant {} on {} to {}\n",
1198 privs.join(", "), grant.on_object, grant.to_role
1199 ));
1200 }
1201 GrantAction::Revoke => {
1202 output.push_str(&format!(
1203 "revoke {} on {} from {}\n",
1204 privs.join(", "), grant.on_object, grant.to_role
1205 ));
1206 }
1207 }
1208 }
1209 if !schema.grants.is_empty() {
1210 output.push('\n');
1211 }
1212
1213 for comment in &schema.comments {
1215 match &comment.target {
1216 CommentTarget::Table(t) => {
1217 output.push_str(&format!("comment on {} \"{}\"\n", t, comment.text));
1218 }
1219 CommentTarget::Column { table, column } => {
1220 output.push_str(&format!(
1221 "comment on {}.{} \"{}\"\n",
1222 table, column, comment.text
1223 ));
1224 }
1225 }
1226 }
1227
1228 output
1229}
1230
1231
1232pub fn schema_to_commands(schema: &Schema) -> Vec<crate::ast::Qail> {
1235 use crate::ast::{Action, Constraint, Expr, IndexDef, Qail};
1236
1237 let mut cmds = Vec::new();
1238
1239 let mut table_order: Vec<&Table> = schema.tables.values().collect();
1241 table_order.sort_by(|a, b| {
1242 let a_has_fk = a.columns.iter().any(|c| c.foreign_key.is_some());
1243 let b_has_fk = b.columns.iter().any(|c| c.foreign_key.is_some());
1244 a_has_fk.cmp(&b_has_fk)
1245 });
1246
1247 for table in table_order {
1248 let columns: Vec<Expr> = table.columns.iter().map(|col| {
1250 let mut constraints = Vec::new();
1251
1252 if col.primary_key {
1253 constraints.push(Constraint::PrimaryKey);
1254 }
1255 if col.nullable {
1256 constraints.push(Constraint::Nullable);
1257 }
1258 if col.unique {
1259 constraints.push(Constraint::Unique);
1260 }
1261 if let Some(def) = &col.default {
1262 constraints.push(Constraint::Default(def.clone()));
1263 }
1264 if let Some(ref fk) = col.foreign_key {
1265 constraints.push(Constraint::References(format!(
1266 "{}({})",
1267 fk.table, fk.column
1268 )));
1269 }
1270
1271 Expr::Def {
1272 name: col.name.clone(),
1273 data_type: col.data_type.to_pg_type(),
1274 constraints,
1275 }
1276 }).collect();
1277
1278 cmds.push(Qail {
1279 action: Action::Make,
1280 table: table.name.clone(),
1281 columns,
1282 ..Default::default()
1283 });
1284 }
1285
1286 for idx in &schema.indexes {
1288 cmds.push(Qail {
1289 action: Action::Index,
1290 table: String::new(),
1291 index_def: Some(IndexDef {
1292 name: idx.name.clone(),
1293 table: idx.table.clone(),
1294 columns: idx.columns.clone(),
1295 unique: idx.unique,
1296 index_type: None,
1297 }),
1298 ..Default::default()
1299 });
1300 }
1301
1302 cmds
1303}
1304
1305#[cfg(test)]
1306mod tests {
1307 use super::*;
1308
1309 #[test]
1310 fn test_schema_builder() {
1311 let mut schema = Schema::new();
1312
1313 let users = Table::new("users")
1314 .column(Column::new("id", ColumnType::Serial).primary_key())
1315 .column(Column::new("name", ColumnType::Text).not_null())
1316 .column(Column::new("email", ColumnType::Text).unique());
1317
1318 schema.add_table(users);
1319 schema.add_index(Index::new("idx_users_email", "users", vec!["email".into()]).unique());
1320
1321 let output = to_qail_string(&schema);
1322 assert!(output.contains("table users"));
1323 assert!(output.contains("id SERIAL primary_key"));
1324 assert!(output.contains("unique index idx_users_email"));
1325 }
1326
1327 #[test]
1328 fn test_migration_hints() {
1329 let mut schema = Schema::new();
1330 schema.add_hint(MigrationHint::Rename {
1331 from: "users.username".into(),
1332 to: "users.name".into(),
1333 });
1334
1335 let output = to_qail_string(&schema);
1336 assert!(output.contains("rename users.username -> users.name"));
1337 }
1338
1339 #[test]
1340 #[should_panic(expected = "cannot be a primary key")]
1341 fn test_invalid_primary_key_type() {
1342 Column::new("data", ColumnType::Text).primary_key();
1344 }
1345
1346 #[test]
1347 #[should_panic(expected = "cannot have UNIQUE")]
1348 fn test_invalid_unique_type() {
1349 Column::new("data", ColumnType::Jsonb).unique();
1351 }
1352
1353 #[test]
1354 fn test_foreign_key_valid() {
1355 let mut schema = Schema::new();
1356
1357 schema.add_table(
1358 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1359 );
1360
1361 schema.add_table(
1362 Table::new("posts")
1363 .column(Column::new("id", ColumnType::Uuid).primary_key())
1364 .column(
1365 Column::new("user_id", ColumnType::Uuid)
1366 .references("users", "id")
1367 .on_delete(FkAction::Cascade),
1368 ),
1369 );
1370
1371 assert!(schema.validate().is_ok());
1373 }
1374
1375 #[test]
1376 fn test_foreign_key_invalid_table() {
1377 let mut schema = Schema::new();
1378
1379 schema.add_table(
1380 Table::new("posts")
1381 .column(Column::new("id", ColumnType::Uuid).primary_key())
1382 .column(Column::new("user_id", ColumnType::Uuid).references("nonexistent", "id")),
1383 );
1384
1385 let result = schema.validate();
1387 assert!(result.is_err());
1388 assert!(result.unwrap_err()[0].contains("non-existent table"));
1389 }
1390
1391 #[test]
1392 fn test_foreign_key_invalid_column() {
1393 let mut schema = Schema::new();
1394
1395 schema.add_table(
1396 Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1397 );
1398
1399 schema.add_table(
1400 Table::new("posts")
1401 .column(Column::new("id", ColumnType::Uuid).primary_key())
1402 .column(
1403 Column::new("user_id", ColumnType::Uuid).references("users", "wrong_column"),
1404 ),
1405 );
1406
1407 let result = schema.validate();
1409 assert!(result.is_err());
1410 assert!(result.unwrap_err()[0].contains("non-existent column"));
1411 }
1412}