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 super::policy::{RlsPolicy, PolicyTarget, PolicyPermissiveness};
19use std::collections::HashMap;
20
21/// A complete database schema.
22#[derive(Debug, Clone, Default)]
23pub struct Schema {
24    pub tables: HashMap<String, Table>,
25    pub indexes: Vec<Index>,
26    pub migrations: Vec<MigrationHint>,
27    /// PostgreSQL extensions (e.g. uuid-ossp, pgcrypto, PostGIS)
28    pub extensions: Vec<Extension>,
29    /// Schema-level comments on tables/columns
30    pub comments: Vec<Comment>,
31    /// Standalone sequences
32    pub sequences: Vec<Sequence>,
33    /// Standalone ENUM types
34    pub enums: Vec<EnumType>,
35    /// Views
36    pub views: Vec<ViewDef>,
37    /// PL/pgSQL functions
38    pub functions: Vec<SchemaFunctionDef>,
39    /// Triggers
40    pub triggers: Vec<SchemaTriggerDef>,
41    /// GRANT/REVOKE permissions
42    pub grants: Vec<Grant>,
43    /// RLS policies
44    pub policies: Vec<RlsPolicy>,
45}
46
47#[derive(Debug, Clone)]
48pub struct Table {
49    pub name: String,
50    pub columns: Vec<Column>,
51    /// Table-level multi-column foreign keys
52    pub multi_column_fks: Vec<MultiColumnForeignKey>,
53    /// ENABLE ROW LEVEL SECURITY
54    pub enable_rls: bool,
55    /// FORCE ROW LEVEL SECURITY
56    pub force_rls: bool,
57}
58
59/// A column definition with compile-time type safety.
60#[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    /// CHECK constraint (Phase 1)
70    pub check: Option<CheckConstraint>,
71    /// GENERATED column (Phase 3)
72    pub generated: Option<Generated>,
73}
74
75/// Foreign key reference definition.
76#[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    /// DEFERRABLE clause (Phase 2)
83    pub deferrable: Deferrable,
84}
85
86/// Foreign key action on DELETE/UPDATE.
87#[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    /// Index method (Phase 4): btree, hash, gin, gist, brin
104    pub method: IndexMethod,
105    /// Partial index WHERE clause
106    pub where_clause: Option<CheckExpr>,
107    /// INCLUDE columns (covering index)
108    pub include: Vec<String>,
109    /// CREATE CONCURRENTLY
110    pub concurrently: bool,
111    /// Expression columns (e.g. `(lower(email))`) — if set, these replace `columns`
112    pub expressions: Vec<String>,
113}
114
115#[derive(Debug, Clone)]
116pub enum MigrationHint {
117    /// Rename a column (not delete + add)
118    Rename { from: String, to: String },
119    /// Transform data with expression
120    Transform { expression: String, target: String },
121    /// Drop with confirmation
122    Drop { target: String, confirmed: bool },
123}
124
125// ============================================================================
126// Phase 1: CHECK Constraints (AST-native)
127// ============================================================================
128
129/// CHECK constraint expression (AST-native, no raw SQL)
130#[derive(Debug, Clone)]
131pub enum CheckExpr {
132    /// column > value
133    GreaterThan { column: String, value: i64 },
134    /// column >= value
135    GreaterOrEqual { column: String, value: i64 },
136    /// column < value
137    LessThan { column: String, value: i64 },
138    /// column <= value
139    LessOrEqual { column: String, value: i64 },
140    Between { column: String, low: i64, high: i64 },
141    In { column: String, values: Vec<String> },
142    /// column ~ pattern (regex)
143    Regex { column: String, pattern: String },
144    /// LENGTH(column) <= max
145    MaxLength { column: String, max: usize },
146    /// LENGTH(column) >= min
147    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/// CHECK constraint with optional name
155#[derive(Debug, Clone)]
156pub struct CheckConstraint {
157    pub expr: CheckExpr,
158    pub name: Option<String>,
159}
160
161// ============================================================================
162// Phase 2: DEFERRABLE Constraints
163// ============================================================================
164
165/// Constraint deferral mode
166#[derive(Debug, Clone, Default, PartialEq)]
167pub enum Deferrable {
168    #[default]
169    NotDeferrable,
170    Deferrable,
171    InitiallyDeferred,
172    InitiallyImmediate,
173}
174
175// ============================================================================
176// Phase 3: GENERATED Columns
177// ============================================================================
178
179/// GENERATED column type
180#[derive(Debug, Clone)]
181pub enum Generated {
182    /// GENERATED ALWAYS AS (expr) STORED
183    AlwaysStored(String),
184    /// GENERATED ALWAYS AS IDENTITY
185    AlwaysIdentity,
186    /// GENERATED BY DEFAULT AS IDENTITY
187    ByDefaultIdentity,
188}
189
190// ============================================================================
191// Phase 4: Advanced Index Types
192// ============================================================================
193
194/// Index method (USING clause)
195#[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// ============================================================================
207// Phase 7: Extensions, Comments, Sequences
208// ============================================================================
209
210/// PostgreSQL extension (e.g. `CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`)
211#[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/// COMMENT ON TABLE/COLUMN
239#[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/// Standalone sequence (CREATE SEQUENCE)
275#[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// ============================================================================
340// Phase 8: Standalone Enums, Multi-Column FK
341// ============================================================================
342
343/// Standalone ENUM type (CREATE TYPE ... AS ENUM)
344#[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    /// Add a new value (for ALTER TYPE ADD VALUE)
359    pub fn add_value(mut self, value: impl Into<String>) -> Self {
360        self.values.push(value.into());
361        self
362    }
363}
364
365/// Table-level multi-column foreign key
366#[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// ============================================================================
411// Phase 9: Views, Functions, Triggers, Grants
412// ============================================================================
413
414/// A SQL view definition.
415#[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/// A PL/pgSQL function definition for the schema model.
438#[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/// A trigger definition for the schema model.
481#[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/// GRANT or REVOKE permission.
531#[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    /// Validate all foreign key references in the schema.
649    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    /// Add a table-level multi-column foreign key
698    pub fn foreign_key(mut self, fk: MultiColumnForeignKey) -> Self {
699        self.multi_column_fks.push(fk);
700        self
701    }
702}
703
704impl Column {
705    /// Create a new column with compile-time type validation.
706    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    /// Set as primary key with compile-time validation.
726    /// Validates that the column type can be a primary key.
727    /// Panics at runtime if type doesn't support PK (caught in tests).
728    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    /// Set as unique with compile-time validation.
743    /// Validates that the column type supports indexing.
744    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    /// Add a foreign key reference to another table.
763    /// # Example
764    /// ```ignore
765    /// Column::new("user_id", ColumnType::Uuid)
766    ///     .references("users", "id")
767    ///     .on_delete(FkAction::Cascade)
768    /// ```
769    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    /// Set the ON DELETE action for the foreign key.
781    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    /// Set the ON UPDATE action for the foreign key.
789    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    // ==================== Phase 1: CHECK ====================
797
798    /// Add a CHECK constraint (AST-native)
799    pub fn check(mut self, expr: CheckExpr) -> Self {
800        self.check = Some(CheckConstraint { expr, name: None });
801        self
802    }
803
804    /// Add a named CHECK constraint
805    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    // ==================== Phase 2: DEFERRABLE ====================
814
815    /// Make foreign key DEFERRABLE
816    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    /// Make foreign key DEFERRABLE INITIALLY DEFERRED
824    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    /// Make foreign key DEFERRABLE INITIALLY IMMEDIATE
832    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    // ==================== Phase 3: GENERATED ====================
840
841    /// GENERATED ALWAYS AS (expr) STORED
842    pub fn generated_stored(mut self, expr: impl Into<String>) -> Self {
843        self.generated = Some(Generated::AlwaysStored(expr.into()));
844        self
845    }
846
847    /// GENERATED ALWAYS AS IDENTITY
848    pub fn generated_identity(mut self) -> Self {
849        self.generated = Some(Generated::AlwaysIdentity);
850        self
851    }
852
853    /// GENERATED BY DEFAULT AS IDENTITY
854    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    /// Create an expression index (e.g. `CREATE INDEX ON t ((lower(email)))`)
876    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    // ==================== Phase 4: Advanced Index Options ====================
900
901    /// Set index method (USING clause)
902    pub fn using(mut self, method: IndexMethod) -> Self {
903        self.method = method;
904        self
905    }
906
907    /// Create a partial index with WHERE clause
908    pub fn partial(mut self, expr: CheckExpr) -> Self {
909        self.where_clause = Some(expr);
910        self
911    }
912
913    /// Add INCLUDE columns (covering index)
914    pub fn include(mut self, cols: Vec<String>) -> Self {
915        self.include = cols;
916        self
917    }
918
919    /// Create index CONCURRENTLY
920    pub fn concurrently(mut self) -> Self {
921        self.concurrently = true;
922        self
923    }
924}
925
926/// Format a Schema to .qail format string.
927/// Convert FkAction to its QAIL string representation
928fn 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
938/// Serialize CheckExpr to QAIL check syntax
939fn 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    // Extensions first (must be created before any DDL)
963    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    // Enums (CREATE TYPE ... AS ENUM, must precede tables)
979    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    // Sequences (before tables, since columns may reference them)
993    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 => {} // default, omit
1062                }
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        // Multi-column foreign keys
1083        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        // RLS directives
1092        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    // Views
1130    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    // Functions
1140    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    // Triggers
1149    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    // Policies
1162    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    // Grants
1192    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    // Comments last (tables must exist first)
1214    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
1232/// Convert a Schema to a list of Qail commands (CREATE TABLE, CREATE INDEX).
1233/// Used by shadow migration to apply the base schema before applying diffs.
1234pub 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    // Sort tables to handle dependencies (tables with FK refs should come after their targets)
1240    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        // Build columns using Expr::Def exactly like diff.rs does
1249        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    // Add indexes using IndexDef like diff.rs
1287    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        // TEXT cannot be a primary key
1343        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        // JSONB cannot have standard unique index
1350        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        // Should pass validation
1372        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        // Should fail validation
1386        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        // Should fail validation
1408        let result = schema.validate();
1409        assert!(result.is_err());
1410        assert!(result.unwrap_err()[0].contains("non-existent column"));
1411    }
1412}