Skip to main content

qail_core/migrate/
schema.rs

1//! QAIL Schema Format (Native AST)
2//!
3//! Replaces JSON with a human-readable, intent-aware schema format.
4//!
5//! ```qail
6//! table users {
7//!   id serial primary_key
8//!   name text not_null
9//!   email text nullable unique
10//! }
11//!
12//! unique index idx_users_email on users (email)
13//!
14//! rename users.username -> users.name
15//! ```
16
17use super::types::ColumnType;
18use std::collections::HashMap;
19
20/// A complete database schema.
21#[derive(Debug, Clone, Default)]
22pub struct Schema {
23    pub tables: HashMap<String, Table>,
24    pub indexes: Vec<Index>,
25    pub migrations: Vec<MigrationHint>,
26    /// PostgreSQL extensions (e.g. uuid-ossp, pgcrypto, PostGIS)
27    pub extensions: Vec<Extension>,
28    /// Schema-level comments on tables/columns
29    pub comments: Vec<Comment>,
30    /// Standalone sequences
31    pub sequences: Vec<Sequence>,
32    /// Standalone ENUM types
33    pub enums: Vec<EnumType>,
34    /// Views
35    pub views: Vec<ViewDef>,
36    /// PL/pgSQL functions
37    pub functions: Vec<SchemaFunctionDef>,
38    /// Triggers
39    pub triggers: Vec<SchemaTriggerDef>,
40    /// GRANT/REVOKE permissions
41    pub grants: Vec<Grant>,
42}
43
44#[derive(Debug, Clone)]
45pub struct Table {
46    pub name: String,
47    pub columns: Vec<Column>,
48    /// Table-level multi-column foreign keys
49    pub multi_column_fks: Vec<MultiColumnForeignKey>,
50}
51
52/// A column definition with compile-time type safety.
53#[derive(Debug, Clone)]
54pub struct Column {
55    pub name: String,
56    pub data_type: ColumnType,
57    pub nullable: bool,
58    pub primary_key: bool,
59    pub unique: bool,
60    pub default: Option<String>,
61    pub foreign_key: Option<ForeignKey>,
62    /// CHECK constraint (Phase 1)
63    pub check: Option<CheckConstraint>,
64    /// GENERATED column (Phase 3)
65    pub generated: Option<Generated>,
66}
67
68/// Foreign key reference definition.
69#[derive(Debug, Clone)]
70pub struct ForeignKey {
71    pub table: String,
72    pub column: String,
73    pub on_delete: FkAction,
74    pub on_update: FkAction,
75    /// DEFERRABLE clause (Phase 2)
76    pub deferrable: Deferrable,
77}
78
79/// Foreign key action on DELETE/UPDATE.
80#[derive(Debug, Clone, Default, PartialEq)]
81pub enum FkAction {
82    #[default]
83    NoAction,
84    Cascade,
85    SetNull,
86    SetDefault,
87    Restrict,
88}
89
90#[derive(Debug, Clone)]
91pub struct Index {
92    pub name: String,
93    pub table: String,
94    pub columns: Vec<String>,
95    pub unique: bool,
96    /// Index method (Phase 4): btree, hash, gin, gist, brin
97    pub method: IndexMethod,
98    /// Partial index WHERE clause
99    pub where_clause: Option<CheckExpr>,
100    /// INCLUDE columns (covering index)
101    pub include: Vec<String>,
102    /// CREATE CONCURRENTLY
103    pub concurrently: bool,
104    /// Expression columns (e.g. `(lower(email))`) — if set, these replace `columns`
105    pub expressions: Vec<String>,
106}
107
108#[derive(Debug, Clone)]
109pub enum MigrationHint {
110    /// Rename a column (not delete + add)
111    Rename { from: String, to: String },
112    /// Transform data with expression
113    Transform { expression: String, target: String },
114    /// Drop with confirmation
115    Drop { target: String, confirmed: bool },
116}
117
118// ============================================================================
119// Phase 1: CHECK Constraints (AST-native)
120// ============================================================================
121
122/// CHECK constraint expression (AST-native, no raw SQL)
123#[derive(Debug, Clone)]
124pub enum CheckExpr {
125    /// column > value
126    GreaterThan { column: String, value: i64 },
127    /// column >= value
128    GreaterOrEqual { column: String, value: i64 },
129    /// column < value
130    LessThan { column: String, value: i64 },
131    /// column <= value
132    LessOrEqual { column: String, value: i64 },
133    Between { column: String, low: i64, high: i64 },
134    In { column: String, values: Vec<String> },
135    /// column ~ pattern (regex)
136    Regex { column: String, pattern: String },
137    /// LENGTH(column) <= max
138    MaxLength { column: String, max: usize },
139    /// LENGTH(column) >= min
140    MinLength { column: String, min: usize },
141    NotNull { column: String },
142    And(Box<CheckExpr>, Box<CheckExpr>),
143    Or(Box<CheckExpr>, Box<CheckExpr>),
144    Not(Box<CheckExpr>),
145}
146
147/// CHECK constraint with optional name
148#[derive(Debug, Clone)]
149pub struct CheckConstraint {
150    pub expr: CheckExpr,
151    pub name: Option<String>,
152}
153
154// ============================================================================
155// Phase 2: DEFERRABLE Constraints
156// ============================================================================
157
158/// Constraint deferral mode
159#[derive(Debug, Clone, Default, PartialEq)]
160pub enum Deferrable {
161    #[default]
162    NotDeferrable,
163    Deferrable,
164    InitiallyDeferred,
165    InitiallyImmediate,
166}
167
168// ============================================================================
169// Phase 3: GENERATED Columns
170// ============================================================================
171
172/// GENERATED column type
173#[derive(Debug, Clone)]
174pub enum Generated {
175    /// GENERATED ALWAYS AS (expr) STORED
176    AlwaysStored(String),
177    /// GENERATED ALWAYS AS IDENTITY
178    AlwaysIdentity,
179    /// GENERATED BY DEFAULT AS IDENTITY
180    ByDefaultIdentity,
181}
182
183// ============================================================================
184// Phase 4: Advanced Index Types
185// ============================================================================
186
187/// Index method (USING clause)
188#[derive(Debug, Clone, Default, PartialEq)]
189pub enum IndexMethod {
190    #[default]
191    BTree,
192    Hash,
193    Gin,
194    Gist,
195    Brin,
196    SpGist,
197}
198
199// ============================================================================
200// Phase 7: Extensions, Comments, Sequences
201// ============================================================================
202
203/// PostgreSQL extension (e.g. `CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`)
204#[derive(Debug, Clone, PartialEq)]
205pub struct Extension {
206    pub name: String,
207    pub schema: Option<String>,
208    pub version: Option<String>,
209}
210
211impl Extension {
212    pub fn new(name: impl Into<String>) -> Self {
213        Self {
214            name: name.into(),
215            schema: None,
216            version: None,
217        }
218    }
219
220    pub fn schema(mut self, schema: impl Into<String>) -> Self {
221        self.schema = Some(schema.into());
222        self
223    }
224
225    pub fn version(mut self, version: impl Into<String>) -> Self {
226        self.version = Some(version.into());
227        self
228    }
229}
230
231/// COMMENT ON TABLE/COLUMN
232#[derive(Debug, Clone, PartialEq)]
233pub struct Comment {
234    pub target: CommentTarget,
235    pub text: String,
236}
237
238#[derive(Debug, Clone, PartialEq)]
239pub enum CommentTarget {
240    Table(String),
241    Column { table: String, column: String },
242}
243
244impl Comment {
245    pub fn on_table(table: impl Into<String>, text: impl Into<String>) -> Self {
246        Self {
247            target: CommentTarget::Table(table.into()),
248            text: text.into(),
249        }
250    }
251
252    pub fn on_column(
253        table: impl Into<String>,
254        column: impl Into<String>,
255        text: impl Into<String>,
256    ) -> Self {
257        Self {
258            target: CommentTarget::Column {
259                table: table.into(),
260                column: column.into(),
261            },
262            text: text.into(),
263        }
264    }
265}
266
267/// Standalone sequence (CREATE SEQUENCE)
268#[derive(Debug, Clone, PartialEq)]
269pub struct Sequence {
270    pub name: String,
271    pub data_type: Option<String>,
272    pub start: Option<i64>,
273    pub increment: Option<i64>,
274    pub min_value: Option<i64>,
275    pub max_value: Option<i64>,
276    pub cache: Option<i64>,
277    pub cycle: bool,
278    pub owned_by: Option<String>,
279}
280
281impl Sequence {
282    pub fn new(name: impl Into<String>) -> Self {
283        Self {
284            name: name.into(),
285            data_type: None,
286            start: None,
287            increment: None,
288            min_value: None,
289            max_value: None,
290            cache: None,
291            cycle: false,
292            owned_by: None,
293        }
294    }
295
296    pub fn start(mut self, v: i64) -> Self {
297        self.start = Some(v);
298        self
299    }
300
301    pub fn increment(mut self, v: i64) -> Self {
302        self.increment = Some(v);
303        self
304    }
305
306    pub fn min_value(mut self, v: i64) -> Self {
307        self.min_value = Some(v);
308        self
309    }
310
311    pub fn max_value(mut self, v: i64) -> Self {
312        self.max_value = Some(v);
313        self
314    }
315
316    pub fn cache(mut self, v: i64) -> Self {
317        self.cache = Some(v);
318        self
319    }
320
321    pub fn cycle(mut self) -> Self {
322        self.cycle = true;
323        self
324    }
325
326    pub fn owned_by(mut self, col: impl Into<String>) -> Self {
327        self.owned_by = Some(col.into());
328        self
329    }
330}
331
332// ============================================================================
333// Phase 8: Standalone Enums, Multi-Column FK
334// ============================================================================
335
336/// Standalone ENUM type (CREATE TYPE ... AS ENUM)
337#[derive(Debug, Clone, PartialEq)]
338pub struct EnumType {
339    pub name: String,
340    pub values: Vec<String>,
341}
342
343impl EnumType {
344    pub fn new(name: impl Into<String>, values: Vec<String>) -> Self {
345        Self {
346            name: name.into(),
347            values,
348        }
349    }
350
351    /// Add a new value (for ALTER TYPE ADD VALUE)
352    pub fn add_value(mut self, value: impl Into<String>) -> Self {
353        self.values.push(value.into());
354        self
355    }
356}
357
358/// Table-level multi-column foreign key
359#[derive(Debug, Clone, PartialEq)]
360pub struct MultiColumnForeignKey {
361    pub columns: Vec<String>,
362    pub ref_table: String,
363    pub ref_columns: Vec<String>,
364    pub on_delete: FkAction,
365    pub on_update: FkAction,
366    pub deferrable: Deferrable,
367    pub name: Option<String>,
368}
369
370impl MultiColumnForeignKey {
371    pub fn new(
372        columns: Vec<String>,
373        ref_table: impl Into<String>,
374        ref_columns: Vec<String>,
375    ) -> Self {
376        Self {
377            columns,
378            ref_table: ref_table.into(),
379            ref_columns,
380            on_delete: FkAction::default(),
381            on_update: FkAction::default(),
382            deferrable: Deferrable::default(),
383            name: None,
384        }
385    }
386
387    pub fn on_delete(mut self, action: FkAction) -> Self {
388        self.on_delete = action;
389        self
390    }
391
392    pub fn on_update(mut self, action: FkAction) -> Self {
393        self.on_update = action;
394        self
395    }
396
397    pub fn named(mut self, name: impl Into<String>) -> Self {
398        self.name = Some(name.into());
399        self
400    }
401}
402
403// ============================================================================
404// Phase 9: Views, Functions, Triggers, Grants
405// ============================================================================
406
407/// A SQL view definition.
408#[derive(Debug, Clone, PartialEq)]
409pub struct ViewDef {
410    pub name: String,
411    pub query: String,
412    pub materialized: bool,
413}
414
415impl ViewDef {
416    pub fn new(name: impl Into<String>, query: impl Into<String>) -> Self {
417        Self {
418            name: name.into(),
419            query: query.into(),
420            materialized: false,
421        }
422    }
423
424    pub fn materialized(mut self) -> Self {
425        self.materialized = true;
426        self
427    }
428}
429
430/// A PL/pgSQL function definition for the schema model.
431#[derive(Debug, Clone, PartialEq)]
432pub struct SchemaFunctionDef {
433    pub name: String,
434    pub args: Vec<String>,
435    pub returns: String,
436    pub body: String,
437    pub language: String,
438    pub volatility: Option<String>,
439}
440
441impl SchemaFunctionDef {
442    pub fn new(
443        name: impl Into<String>,
444        returns: impl Into<String>,
445        body: impl Into<String>,
446    ) -> Self {
447        Self {
448            name: name.into(),
449            args: Vec::new(),
450            returns: returns.into(),
451            body: body.into(),
452            language: "plpgsql".to_string(),
453            volatility: None,
454        }
455    }
456
457    pub fn language(mut self, lang: impl Into<String>) -> Self {
458        self.language = lang.into();
459        self
460    }
461
462    pub fn arg(mut self, arg: impl Into<String>) -> Self {
463        self.args.push(arg.into());
464        self
465    }
466
467    pub fn volatility(mut self, v: impl Into<String>) -> Self {
468        self.volatility = Some(v.into());
469        self
470    }
471}
472
473/// A trigger definition for the schema model.
474#[derive(Debug, Clone, PartialEq)]
475pub struct SchemaTriggerDef {
476    pub name: String,
477    pub table: String,
478    pub timing: String,
479    pub events: Vec<String>,
480    pub for_each_row: bool,
481    pub execute_function: String,
482    pub condition: Option<String>,
483}
484
485impl SchemaTriggerDef {
486    pub fn new(
487        name: impl Into<String>,
488        table: impl Into<String>,
489        execute_function: impl Into<String>,
490    ) -> Self {
491        Self {
492            name: name.into(),
493            table: table.into(),
494            timing: "BEFORE".to_string(),
495            events: vec!["INSERT".to_string()],
496            for_each_row: true,
497            execute_function: execute_function.into(),
498            condition: None,
499        }
500    }
501
502    pub fn timing(mut self, t: impl Into<String>) -> Self {
503        self.timing = t.into();
504        self
505    }
506
507    pub fn events(mut self, evts: Vec<String>) -> Self {
508        self.events = evts;
509        self
510    }
511
512    pub fn for_each_statement(mut self) -> Self {
513        self.for_each_row = false;
514        self
515    }
516
517    pub fn condition(mut self, cond: impl Into<String>) -> Self {
518        self.condition = Some(cond.into());
519        self
520    }
521}
522
523/// GRANT or REVOKE permission.
524#[derive(Debug, Clone, PartialEq)]
525pub struct Grant {
526    pub action: GrantAction,
527    pub privileges: Vec<Privilege>,
528    pub on_object: String,
529    pub to_role: String,
530}
531
532#[derive(Debug, Clone, PartialEq, Default)]
533pub enum GrantAction {
534    #[default]
535    Grant,
536    Revoke,
537}
538
539#[derive(Debug, Clone, PartialEq)]
540pub enum Privilege {
541    All,
542    Select,
543    Insert,
544    Update,
545    Delete,
546    Usage,
547    Execute,
548}
549
550impl std::fmt::Display for Privilege {
551    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552        match self {
553            Privilege::All => write!(f, "ALL"),
554            Privilege::Select => write!(f, "SELECT"),
555            Privilege::Insert => write!(f, "INSERT"),
556            Privilege::Update => write!(f, "UPDATE"),
557            Privilege::Delete => write!(f, "DELETE"),
558            Privilege::Usage => write!(f, "USAGE"),
559            Privilege::Execute => write!(f, "EXECUTE"),
560        }
561    }
562}
563
564impl Grant {
565    pub fn new(
566        privileges: Vec<Privilege>,
567        on_object: impl Into<String>,
568        to_role: impl Into<String>,
569    ) -> Self {
570        Self {
571            action: GrantAction::Grant,
572            privileges,
573            on_object: on_object.into(),
574            to_role: to_role.into(),
575        }
576    }
577
578    pub fn revoke(
579        privileges: Vec<Privilege>,
580        on_object: impl Into<String>,
581        from_role: impl Into<String>,
582    ) -> Self {
583        Self {
584            action: GrantAction::Revoke,
585            privileges,
586            on_object: on_object.into(),
587            to_role: from_role.into(),
588        }
589    }
590}
591
592impl Schema {
593    pub fn new() -> Self {
594        Self::default()
595    }
596
597    pub fn add_table(&mut self, table: Table) {
598        self.tables.insert(table.name.clone(), table);
599    }
600
601    pub fn add_index(&mut self, index: Index) {
602        self.indexes.push(index);
603    }
604
605    pub fn add_hint(&mut self, hint: MigrationHint) {
606        self.migrations.push(hint);
607    }
608
609    pub fn add_extension(&mut self, ext: Extension) {
610        self.extensions.push(ext);
611    }
612
613    pub fn add_comment(&mut self, comment: Comment) {
614        self.comments.push(comment);
615    }
616
617    pub fn add_sequence(&mut self, seq: Sequence) {
618        self.sequences.push(seq);
619    }
620
621    pub fn add_enum(&mut self, enum_type: EnumType) {
622        self.enums.push(enum_type);
623    }
624
625    pub fn add_view(&mut self, view: ViewDef) {
626        self.views.push(view);
627    }
628
629    pub fn add_function(&mut self, func: SchemaFunctionDef) {
630        self.functions.push(func);
631    }
632
633    pub fn add_trigger(&mut self, trigger: SchemaTriggerDef) {
634        self.triggers.push(trigger);
635    }
636
637    pub fn add_grant(&mut self, grant: Grant) {
638        self.grants.push(grant);
639    }
640
641    /// Validate all foreign key references in the schema.
642    pub fn validate(&self) -> Result<(), Vec<String>> {
643        let mut errors = Vec::new();
644
645        for table in self.tables.values() {
646            for col in &table.columns {
647                if let Some(ref fk) = col.foreign_key {
648                    if !self.tables.contains_key(&fk.table) {
649                        errors.push(format!(
650                            "FK error: {}.{} references non-existent table '{}'",
651                            table.name, col.name, fk.table
652                        ));
653                    } else {
654                        let ref_table = &self.tables[&fk.table];
655                        if !ref_table.columns.iter().any(|c| c.name == fk.column) {
656                            errors.push(format!(
657                                "FK error: {}.{} references non-existent column '{}.{}'",
658                                table.name, col.name, fk.table, fk.column
659                            ));
660                        }
661                    }
662                }
663            }
664        }
665
666        if errors.is_empty() {
667            Ok(())
668        } else {
669            Err(errors)
670        }
671    }
672}
673
674impl Table {
675    pub fn new(name: impl Into<String>) -> Self {
676        Self {
677            name: name.into(),
678            columns: Vec::new(),
679            multi_column_fks: Vec::new(),
680        }
681    }
682
683    pub fn column(mut self, col: Column) -> Self {
684        self.columns.push(col);
685        self
686    }
687
688    /// Add a table-level multi-column foreign key
689    pub fn foreign_key(mut self, fk: MultiColumnForeignKey) -> Self {
690        self.multi_column_fks.push(fk);
691        self
692    }
693}
694
695impl Column {
696    /// Create a new column with compile-time type validation.
697    pub fn new(name: impl Into<String>, data_type: ColumnType) -> Self {
698        Self {
699            name: name.into(),
700            data_type,
701            nullable: true,
702            primary_key: false,
703            unique: false,
704            default: None,
705            foreign_key: None,
706            check: None,
707            generated: None,
708        }
709    }
710
711    pub fn not_null(mut self) -> Self {
712        self.nullable = false;
713        self
714    }
715
716    /// Set as primary key with compile-time validation.
717    /// Validates that the column type can be a primary key.
718    /// Panics at runtime if type doesn't support PK (caught in tests).
719    pub fn primary_key(mut self) -> Self {
720        if !self.data_type.can_be_primary_key() {
721            panic!(
722                "Column '{}' of type {} cannot be a primary key. \
723                 Valid PK types: UUID, SERIAL, BIGSERIAL, INT, BIGINT",
724                self.name,
725                self.data_type.name()
726            );
727        }
728        self.primary_key = true;
729        self.nullable = false;
730        self
731    }
732
733    /// Set as unique with compile-time validation.
734    /// Validates that the column type supports indexing.
735    pub fn unique(mut self) -> Self {
736        if !self.data_type.supports_indexing() {
737            panic!(
738                "Column '{}' of type {} cannot have UNIQUE constraint. \
739                 JSONB and BYTEA types do not support standard indexing.",
740                self.name,
741                self.data_type.name()
742            );
743        }
744        self.unique = true;
745        self
746    }
747
748    pub fn default(mut self, val: impl Into<String>) -> Self {
749        self.default = Some(val.into());
750        self
751    }
752
753    /// Add a foreign key reference to another table.
754    /// # Example
755    /// ```ignore
756    /// Column::new("user_id", ColumnType::Uuid)
757    ///     .references("users", "id")
758    ///     .on_delete(FkAction::Cascade)
759    /// ```
760    pub fn references(mut self, table: &str, column: &str) -> Self {
761        self.foreign_key = Some(ForeignKey {
762            table: table.to_string(),
763            column: column.to_string(),
764            on_delete: FkAction::default(),
765            on_update: FkAction::default(),
766            deferrable: Deferrable::default(),
767        });
768        self
769    }
770
771    /// Set the ON DELETE action for the foreign key.
772    pub fn on_delete(mut self, action: FkAction) -> Self {
773        if let Some(ref mut fk) = self.foreign_key {
774            fk.on_delete = action;
775        }
776        self
777    }
778
779    /// Set the ON UPDATE action for the foreign key.
780    pub fn on_update(mut self, action: FkAction) -> Self {
781        if let Some(ref mut fk) = self.foreign_key {
782            fk.on_update = action;
783        }
784        self
785    }
786
787    // ==================== Phase 1: CHECK ====================
788
789    /// Add a CHECK constraint (AST-native)
790    pub fn check(mut self, expr: CheckExpr) -> Self {
791        self.check = Some(CheckConstraint { expr, name: None });
792        self
793    }
794
795    /// Add a named CHECK constraint
796    pub fn check_named(mut self, name: impl Into<String>, expr: CheckExpr) -> Self {
797        self.check = Some(CheckConstraint {
798            expr,
799            name: Some(name.into()),
800        });
801        self
802    }
803
804    // ==================== Phase 2: DEFERRABLE ====================
805
806    /// Make foreign key DEFERRABLE
807    pub fn deferrable(mut self) -> Self {
808        if let Some(ref mut fk) = self.foreign_key {
809            fk.deferrable = Deferrable::Deferrable;
810        }
811        self
812    }
813
814    /// Make foreign key DEFERRABLE INITIALLY DEFERRED
815    pub fn initially_deferred(mut self) -> Self {
816        if let Some(ref mut fk) = self.foreign_key {
817            fk.deferrable = Deferrable::InitiallyDeferred;
818        }
819        self
820    }
821
822    /// Make foreign key DEFERRABLE INITIALLY IMMEDIATE
823    pub fn initially_immediate(mut self) -> Self {
824        if let Some(ref mut fk) = self.foreign_key {
825            fk.deferrable = Deferrable::InitiallyImmediate;
826        }
827        self
828    }
829
830    // ==================== Phase 3: GENERATED ====================
831
832    /// GENERATED ALWAYS AS (expr) STORED
833    pub fn generated_stored(mut self, expr: impl Into<String>) -> Self {
834        self.generated = Some(Generated::AlwaysStored(expr.into()));
835        self
836    }
837
838    /// GENERATED ALWAYS AS IDENTITY
839    pub fn generated_identity(mut self) -> Self {
840        self.generated = Some(Generated::AlwaysIdentity);
841        self
842    }
843
844    /// GENERATED BY DEFAULT AS IDENTITY
845    pub fn generated_by_default(mut self) -> Self {
846        self.generated = Some(Generated::ByDefaultIdentity);
847        self
848    }
849}
850
851impl Index {
852    pub fn new(name: impl Into<String>, table: impl Into<String>, columns: Vec<String>) -> Self {
853        Self {
854            name: name.into(),
855            table: table.into(),
856            columns,
857            unique: false,
858            method: IndexMethod::default(),
859            where_clause: None,
860            include: Vec::new(),
861            concurrently: false,
862            expressions: Vec::new(),
863        }
864    }
865
866    /// Create an expression index (e.g. `CREATE INDEX ON t ((lower(email)))`)
867    pub fn expression(
868        name: impl Into<String>,
869        table: impl Into<String>,
870        expressions: Vec<String>,
871    ) -> Self {
872        Self {
873            name: name.into(),
874            table: table.into(),
875            columns: Vec::new(),
876            unique: false,
877            method: IndexMethod::default(),
878            where_clause: None,
879            include: Vec::new(),
880            concurrently: false,
881            expressions,
882        }
883    }
884
885    pub fn unique(mut self) -> Self {
886        self.unique = true;
887        self
888    }
889
890    // ==================== Phase 4: Advanced Index Options ====================
891
892    /// Set index method (USING clause)
893    pub fn using(mut self, method: IndexMethod) -> Self {
894        self.method = method;
895        self
896    }
897
898    /// Create a partial index with WHERE clause
899    pub fn partial(mut self, expr: CheckExpr) -> Self {
900        self.where_clause = Some(expr);
901        self
902    }
903
904    /// Add INCLUDE columns (covering index)
905    pub fn include(mut self, cols: Vec<String>) -> Self {
906        self.include = cols;
907        self
908    }
909
910    /// Create index CONCURRENTLY
911    pub fn concurrently(mut self) -> Self {
912        self.concurrently = true;
913        self
914    }
915}
916
917/// Format a Schema to .qail format string.
918pub fn to_qail_string(schema: &Schema) -> String {
919    let mut output = String::new();
920    output.push_str("# QAIL Schema\n\n");
921
922    // Extensions first (must be created before any DDL)
923    for ext in &schema.extensions {
924        let mut line = format!("extension \"{}\"", ext.name);
925        if let Some(ref s) = ext.schema {
926            line.push_str(&format!(" schema {}", s));
927        }
928        if let Some(ref v) = ext.version {
929            line.push_str(&format!(" version \"{}\"", v));
930        }
931        output.push_str(&line);
932        output.push('\n');
933    }
934    if !schema.extensions.is_empty() {
935        output.push('\n');
936    }
937
938    // Enums (CREATE TYPE ... AS ENUM, must precede tables)
939    for enum_type in &schema.enums {
940        let values = enum_type
941            .values
942            .iter()
943            .map(|v| v.as_str())
944            .collect::<Vec<_>>()
945            .join(", ");
946        output.push_str(&format!("enum {} {{ {} }}\n", enum_type.name, values));
947    }
948    if !schema.enums.is_empty() {
949        output.push('\n');
950    }
951
952    // Sequences (before tables, since columns may reference them)
953    for seq in &schema.sequences {
954        if seq.start.is_some()
955            || seq.increment.is_some()
956            || seq.min_value.is_some()
957            || seq.max_value.is_some()
958            || seq.cache.is_some()
959            || seq.cycle
960            || seq.owned_by.is_some()
961        {
962            let mut opts = Vec::new();
963            if let Some(v) = seq.start {
964                opts.push(format!("start {}", v));
965            }
966            if let Some(v) = seq.increment {
967                opts.push(format!("increment {}", v));
968            }
969            if let Some(v) = seq.min_value {
970                opts.push(format!("minvalue {}", v));
971            }
972            if let Some(v) = seq.max_value {
973                opts.push(format!("maxvalue {}", v));
974            }
975            if let Some(v) = seq.cache {
976                opts.push(format!("cache {}", v));
977            }
978            if seq.cycle {
979                opts.push("cycle".to_string());
980            }
981            if let Some(ref o) = seq.owned_by {
982                opts.push(format!("owned_by {}", o));
983            }
984            output.push_str(&format!("sequence {} {{ {} }}\n", seq.name, opts.join(" ")));
985        } else {
986            output.push_str(&format!("sequence {}\n", seq.name));
987        }
988    }
989    if !schema.sequences.is_empty() {
990        output.push('\n');
991    }
992
993    for table in schema.tables.values() {
994        output.push_str(&format!("table {} {{\n", table.name));
995        for col in &table.columns {
996            let mut constraints: Vec<String> = Vec::new();
997            if col.primary_key {
998                constraints.push("primary_key".to_string());
999            }
1000            if !col.nullable && !col.primary_key {
1001                constraints.push("not_null".to_string());
1002            }
1003            if col.unique {
1004                constraints.push("unique".to_string());
1005            }
1006            if let Some(def) = &col.default {
1007                constraints.push(format!("default {}", def));
1008            }
1009            if let Some(ref fk) = col.foreign_key {
1010                constraints.push(format!("references {}({})", fk.table, fk.column));
1011            }
1012
1013            let constraint_str = if constraints.is_empty() {
1014                String::new()
1015            } else {
1016                format!(" {}", constraints.join(" "))
1017            };
1018
1019            output.push_str(&format!(
1020                "  {} {}{}\n",
1021                col.name,
1022                col.data_type.to_pg_type(),
1023                constraint_str
1024            ));
1025        }
1026        // Multi-column foreign keys
1027        for fk in &table.multi_column_fks {
1028            output.push_str(&format!(
1029                "  foreign_key ({}) references {}({})\n",
1030                fk.columns.join(", "),
1031                fk.ref_table,
1032                fk.ref_columns.join(", ")
1033            ));
1034        }
1035        output.push_str("}\n\n");
1036    }
1037
1038    for idx in &schema.indexes {
1039        let unique = if idx.unique { "unique " } else { "" };
1040        let cols = if !idx.expressions.is_empty() {
1041            idx.expressions.join(", ")
1042        } else {
1043            idx.columns.join(", ")
1044        };
1045        output.push_str(&format!(
1046            "{}index {} on {} ({})\n",
1047            unique, idx.name, idx.table, cols
1048        ));
1049    }
1050
1051    for hint in &schema.migrations {
1052        match hint {
1053            MigrationHint::Rename { from, to } => {
1054                output.push_str(&format!("rename {} -> {}\n", from, to));
1055            }
1056            MigrationHint::Transform { expression, target } => {
1057                output.push_str(&format!("transform {} -> {}\n", expression, target));
1058            }
1059            MigrationHint::Drop { target, confirmed } => {
1060                let confirm = if *confirmed { " confirm" } else { "" };
1061                output.push_str(&format!("drop {}{}\n", target, confirm));
1062            }
1063        }
1064    }
1065
1066    // Views
1067    for view in &schema.views {
1068        let prefix = if view.materialized {
1069            "materialized view"
1070        } else {
1071            "view"
1072        };
1073        output.push_str(&format!("{} {} $$\n{}\n$$\n\n", prefix, view.name, view.query));
1074    }
1075
1076    // Functions
1077    for func in &schema.functions {
1078        let args = func.args.join(", ");
1079        output.push_str(&format!(
1080            "function {}({}) returns {} language {} $$\n{}\n$$\n\n",
1081            func.name, args, func.returns, func.language, func.body
1082        ));
1083    }
1084
1085    // Triggers
1086    for trigger in &schema.triggers {
1087        let events = trigger.events.join(" or ");
1088        output.push_str(&format!(
1089            "trigger {} on {} {} {} execute {}\n",
1090            trigger.name, trigger.table, trigger.timing.to_lowercase(),
1091            events.to_lowercase(), trigger.execute_function
1092        ));
1093    }
1094    if !schema.triggers.is_empty() {
1095        output.push('\n');
1096    }
1097
1098    // Grants
1099    for grant in &schema.grants {
1100        let privs: Vec<String> = grant.privileges.iter().map(|p| p.to_string().to_lowercase()).collect();
1101        match grant.action {
1102            GrantAction::Grant => {
1103                output.push_str(&format!(
1104                    "grant {} on {} to {}\n",
1105                    privs.join(", "), grant.on_object, grant.to_role
1106                ));
1107            }
1108            GrantAction::Revoke => {
1109                output.push_str(&format!(
1110                    "revoke {} on {} from {}\n",
1111                    privs.join(", "), grant.on_object, grant.to_role
1112                ));
1113            }
1114        }
1115    }
1116    if !schema.grants.is_empty() {
1117        output.push('\n');
1118    }
1119
1120    // Comments last (tables must exist first)
1121    for comment in &schema.comments {
1122        match &comment.target {
1123            CommentTarget::Table(t) => {
1124                output.push_str(&format!("comment on {} \"{}\"\n", t, comment.text));
1125            }
1126            CommentTarget::Column { table, column } => {
1127                output.push_str(&format!(
1128                    "comment on {}.{} \"{}\"\n",
1129                    table, column, comment.text
1130                ));
1131            }
1132        }
1133    }
1134
1135    output
1136}
1137
1138
1139/// Convert a Schema to a list of Qail commands (CREATE TABLE, CREATE INDEX).
1140/// Used by shadow migration to apply the base schema before applying diffs.
1141pub fn schema_to_commands(schema: &Schema) -> Vec<crate::ast::Qail> {
1142    use crate::ast::{Action, Constraint, Expr, IndexDef, Qail};
1143    
1144    let mut cmds = Vec::new();
1145    
1146    // Sort tables to handle dependencies (tables with FK refs should come after their targets)
1147    let mut table_order: Vec<&Table> = schema.tables.values().collect();
1148    table_order.sort_by(|a, b| {
1149        let a_has_fk = a.columns.iter().any(|c| c.foreign_key.is_some());
1150        let b_has_fk = b.columns.iter().any(|c| c.foreign_key.is_some());
1151        a_has_fk.cmp(&b_has_fk)
1152    });
1153    
1154    for table in table_order {
1155        // Build columns using Expr::Def exactly like diff.rs does
1156        let columns: Vec<Expr> = table.columns.iter().map(|col| {
1157            let mut constraints = Vec::new();
1158            
1159            if col.primary_key {
1160                constraints.push(Constraint::PrimaryKey);
1161            }
1162            if col.nullable {
1163                constraints.push(Constraint::Nullable);
1164            }
1165            if col.unique {
1166                constraints.push(Constraint::Unique);
1167            }
1168            if let Some(def) = &col.default {
1169                constraints.push(Constraint::Default(def.clone()));
1170            }
1171            if let Some(ref fk) = col.foreign_key {
1172                constraints.push(Constraint::References(format!(
1173                    "{}({})",
1174                    fk.table, fk.column
1175                )));
1176            }
1177            
1178            Expr::Def {
1179                name: col.name.clone(),
1180                data_type: col.data_type.to_pg_type(),
1181                constraints,
1182            }
1183        }).collect();
1184        
1185        cmds.push(Qail {
1186            action: Action::Make,
1187            table: table.name.clone(),
1188            columns,
1189            ..Default::default()
1190        });
1191    }
1192    
1193    // Add indexes using IndexDef like diff.rs
1194    for idx in &schema.indexes {
1195        cmds.push(Qail {
1196            action: Action::Index,
1197            table: String::new(),
1198            index_def: Some(IndexDef {
1199                name: idx.name.clone(),
1200                table: idx.table.clone(),
1201                columns: idx.columns.clone(),
1202                unique: idx.unique,
1203                index_type: None,
1204            }),
1205            ..Default::default()
1206        });
1207    }
1208    
1209    cmds
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214    use super::*;
1215
1216    #[test]
1217    fn test_schema_builder() {
1218        let mut schema = Schema::new();
1219
1220        let users = Table::new("users")
1221            .column(Column::new("id", ColumnType::Serial).primary_key())
1222            .column(Column::new("name", ColumnType::Text).not_null())
1223            .column(Column::new("email", ColumnType::Text).unique());
1224
1225        schema.add_table(users);
1226        schema.add_index(Index::new("idx_users_email", "users", vec!["email".into()]).unique());
1227
1228        let output = to_qail_string(&schema);
1229        assert!(output.contains("table users"));
1230        assert!(output.contains("id SERIAL primary_key"));
1231        assert!(output.contains("unique index idx_users_email"));
1232    }
1233
1234    #[test]
1235    fn test_migration_hints() {
1236        let mut schema = Schema::new();
1237        schema.add_hint(MigrationHint::Rename {
1238            from: "users.username".into(),
1239            to: "users.name".into(),
1240        });
1241
1242        let output = to_qail_string(&schema);
1243        assert!(output.contains("rename users.username -> users.name"));
1244    }
1245
1246    #[test]
1247    #[should_panic(expected = "cannot be a primary key")]
1248    fn test_invalid_primary_key_type() {
1249        // TEXT cannot be a primary key
1250        Column::new("data", ColumnType::Text).primary_key();
1251    }
1252
1253    #[test]
1254    #[should_panic(expected = "cannot have UNIQUE")]
1255    fn test_invalid_unique_type() {
1256        // JSONB cannot have standard unique index
1257        Column::new("data", ColumnType::Jsonb).unique();
1258    }
1259
1260    #[test]
1261    fn test_foreign_key_valid() {
1262        let mut schema = Schema::new();
1263
1264        schema.add_table(
1265            Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1266        );
1267
1268        schema.add_table(
1269            Table::new("posts")
1270                .column(Column::new("id", ColumnType::Uuid).primary_key())
1271                .column(
1272                    Column::new("user_id", ColumnType::Uuid)
1273                        .references("users", "id")
1274                        .on_delete(FkAction::Cascade),
1275                ),
1276        );
1277
1278        // Should pass validation
1279        assert!(schema.validate().is_ok());
1280    }
1281
1282    #[test]
1283    fn test_foreign_key_invalid_table() {
1284        let mut schema = Schema::new();
1285
1286        schema.add_table(
1287            Table::new("posts")
1288                .column(Column::new("id", ColumnType::Uuid).primary_key())
1289                .column(Column::new("user_id", ColumnType::Uuid).references("nonexistent", "id")),
1290        );
1291
1292        // Should fail validation
1293        let result = schema.validate();
1294        assert!(result.is_err());
1295        assert!(result.unwrap_err()[0].contains("non-existent table"));
1296    }
1297
1298    #[test]
1299    fn test_foreign_key_invalid_column() {
1300        let mut schema = Schema::new();
1301
1302        schema.add_table(
1303            Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1304        );
1305
1306        schema.add_table(
1307            Table::new("posts")
1308                .column(Column::new("id", ColumnType::Uuid).primary_key())
1309                .column(
1310                    Column::new("user_id", ColumnType::Uuid).references("users", "wrong_column"),
1311                ),
1312        );
1313
1314        // Should fail validation
1315        let result = schema.validate();
1316        assert!(result.is_err());
1317        assert!(result.unwrap_err()[0].contains("non-existent column"));
1318    }
1319}