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::policy::{PolicyPermissiveness, PolicyTarget, RlsPolicy};
18use super::types::ColumnType;
19use std::collections::HashMap;
20
21/// A complete database schema.
22#[derive(Debug, Clone, Default)]
23pub struct Schema {
24    /// Declared tables.
25    pub tables: HashMap<String, Table>,
26    /// Declared indexes.
27    pub indexes: Vec<Index>,
28    /// Migration hints (renames, transforms, drops).
29    pub migrations: Vec<MigrationHint>,
30    /// PostgreSQL extensions (e.g. uuid-ossp, pgcrypto, PostGIS)
31    pub extensions: Vec<Extension>,
32    /// Schema-level comments on tables/columns
33    pub comments: Vec<Comment>,
34    /// Standalone sequences
35    pub sequences: Vec<Sequence>,
36    /// Standalone ENUM types
37    pub enums: Vec<EnumType>,
38    /// SQL views (CREATE VIEW / CREATE MATERIALIZED VIEW).
39    pub views: Vec<ViewDef>,
40    /// PL/pgSQL functions
41    pub functions: Vec<SchemaFunctionDef>,
42    /// Database triggers (CREATE TRIGGER).
43    pub triggers: Vec<SchemaTriggerDef>,
44    /// GRANT/REVOKE permissions
45    pub grants: Vec<Grant>,
46    /// RLS policies
47    pub policies: Vec<RlsPolicy>,
48    /// Infrastructure resources (buckets, queues, topics)
49    pub resources: Vec<ResourceDef>,
50}
51
52// ============================================================================
53// Infrastructure Resources
54// ============================================================================
55
56/// Kind of infrastructure resource declared in schema.qail.
57#[derive(Debug, Clone, PartialEq)]
58pub enum ResourceKind {
59    /// Object storage bucket.
60    Bucket,
61    /// Message queue.
62    Queue,
63    /// Pub/sub topic.
64    Topic,
65}
66
67impl std::fmt::Display for ResourceKind {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            Self::Bucket => write!(f, "bucket"),
71            Self::Queue => write!(f, "queue"),
72            Self::Topic => write!(f, "topic"),
73        }
74    }
75}
76
77/// An infrastructure resource declaration.
78///
79/// ```qail
80/// bucket avatars {
81///     provider s3
82///     region "ap-southeast-1"
83/// }
84/// ```
85#[derive(Debug, Clone)]
86pub struct ResourceDef {
87    /// Resource name (e.g. `"avatars"`).
88    pub name: String,
89    /// Kind of resource.
90    pub kind: ResourceKind,
91    /// Cloud provider (e.g. `"s3"`, `"gcs"`).
92    pub provider: Option<String>,
93    /// Arbitrary key-value properties.
94    pub properties: HashMap<String, String>,
95}
96
97/// A table definition in the schema.
98#[derive(Debug, Clone)]
99pub struct Table {
100    /// Table name.
101    pub name: String,
102    /// Column definitions.
103    pub columns: Vec<Column>,
104    /// Table-level multi-column foreign keys
105    pub multi_column_fks: Vec<MultiColumnForeignKey>,
106    /// ENABLE ROW LEVEL SECURITY
107    pub enable_rls: bool,
108    /// FORCE ROW LEVEL SECURITY
109    pub force_rls: bool,
110}
111
112/// A column definition with compile-time type safety.
113#[derive(Debug, Clone)]
114pub struct Column {
115    /// Column name.
116    pub name: String,
117    /// Compile-time validated data type.
118    pub data_type: ColumnType,
119    /// Whether the column accepts NULL.
120    pub nullable: bool,
121    /// Whether this column is a primary key.
122    pub primary_key: bool,
123    /// Whether this column has a UNIQUE constraint.
124    pub unique: bool,
125    /// Default value expression.
126    pub default: Option<String>,
127    /// Foreign key reference.
128    pub foreign_key: Option<ForeignKey>,
129    /// CHECK constraint (Phase 1)
130    pub check: Option<CheckConstraint>,
131    /// GENERATED column (Phase 3)
132    pub generated: Option<Generated>,
133}
134
135/// Foreign key reference definition.
136#[derive(Debug, Clone)]
137pub struct ForeignKey {
138    /// Referenced table name.
139    pub table: String,
140    /// Referenced column name.
141    pub column: String,
142    /// Action taken when the referenced row is deleted.
143    pub on_delete: FkAction,
144    /// Action taken when the referenced row is updated.
145    pub on_update: FkAction,
146    /// DEFERRABLE clause (Phase 2)
147    pub deferrable: Deferrable,
148}
149
150/// Foreign key action on DELETE/UPDATE.
151#[derive(Debug, Clone, Default, PartialEq)]
152pub enum FkAction {
153    #[default]
154    /// No action on referenced row change.
155    NoAction,
156    /// Cascade the delete/update to referencing rows.
157    Cascade,
158    /// Set referencing column to NULL.
159    SetNull,
160    /// Set referencing column to its DEFAULT.
161    SetDefault,
162    /// Prevent the action (raises error).
163    Restrict,
164}
165
166/// An index definition.
167#[derive(Debug, Clone)]
168pub struct Index {
169    /// Index name.
170    pub name: String,
171    /// Table the index belongs to.
172    pub table: String,
173    /// Columns covered by the index.
174    pub columns: Vec<String>,
175    /// Whether the index enforces uniqueness.
176    pub unique: bool,
177    /// Index method (Phase 4): btree, hash, gin, gist, brin
178    pub method: IndexMethod,
179    /// Partial index WHERE clause
180    pub where_clause: Option<CheckExpr>,
181    /// INCLUDE columns (covering index)
182    pub include: Vec<String>,
183    /// CREATE CONCURRENTLY
184    pub concurrently: bool,
185    /// Expression columns (e.g. `(lower(email))`) — if set, these replace `columns`
186    pub expressions: Vec<String>,
187}
188
189/// Hints for the migration diff engine to improve migration quality.
190#[derive(Debug, Clone)]
191pub enum MigrationHint {
192    /// Rename a column (not delete + add)
193    Rename {
194        /// Original column name.
195        from: String,
196        /// New column name.
197        to: String,
198    },
199    /// Transform data with expression
200    Transform {
201        /// SQL expression for data transformation.
202        expression: String,
203        /// Target column name.
204        target: String,
205    },
206    /// Drop with confirmation
207    Drop {
208        /// Target name to drop.
209        target: String,
210        /// Whether the drop has been confirmed.
211        confirmed: bool,
212    },
213}
214
215// ============================================================================
216// Phase 1: CHECK Constraints (AST-native)
217// ============================================================================
218
219/// CHECK constraint expression (AST-native, no raw SQL)
220#[derive(Debug, Clone)]
221pub enum CheckExpr {
222    /// column > value
223    GreaterThan {
224        /// Column name.
225        column: String,
226        /// Comparison value.
227        value: i64,
228    },
229    /// column >= value
230    GreaterOrEqual {
231        /// Column name.
232        column: String,
233        /// Comparison value.
234        value: i64,
235    },
236    /// column < value
237    LessThan {
238        /// Column name.
239        column: String,
240        /// Comparison value.
241        value: i64,
242    },
243    /// column <= value
244    LessOrEqual {
245        /// Column name.
246        column: String,
247        /// Comparison value.
248        value: i64,
249    },
250    /// value BETWEEN low AND high
251    Between {
252        /// Column name.
253        column: String,
254        /// Lower bound.
255        low: i64,
256        /// Upper bound.
257        high: i64,
258    },
259    /// column IN (values)
260    In {
261        /// Column name.
262        column: String,
263        /// Allowed values.
264        values: Vec<String>,
265    },
266    /// column ~ pattern (regex)
267    Regex {
268        /// Column name.
269        column: String,
270        /// Regex pattern.
271        pattern: String,
272    },
273    /// LENGTH(column) <= max
274    MaxLength {
275        /// Column name.
276        column: String,
277        /// Maximum allowed length.
278        max: usize,
279    },
280    /// LENGTH(column) >= min
281    MinLength {
282        /// Column name.
283        column: String,
284        /// Minimum required length.
285        min: usize,
286    },
287    /// column IS NOT NULL
288    NotNull {
289        /// Column name.
290        column: String,
291    },
292    /// Logical AND of two expressions.
293    And(Box<CheckExpr>, Box<CheckExpr>),
294    /// Logical OR of two expressions.
295    Or(Box<CheckExpr>, Box<CheckExpr>),
296    /// Logical NOT of an expression.
297    Not(Box<CheckExpr>),
298}
299
300/// CHECK constraint with optional name
301#[derive(Debug, Clone)]
302pub struct CheckConstraint {
303    /// The constraint expression.
304    pub expr: CheckExpr,
305    /// Optional constraint name.
306    pub name: Option<String>,
307}
308
309// ============================================================================
310// Phase 2: DEFERRABLE Constraints
311// ============================================================================
312
313/// Constraint deferral mode
314#[derive(Debug, Clone, Default, PartialEq)]
315pub enum Deferrable {
316    #[default]
317    /// Not deferrable (default).
318    NotDeferrable,
319    /// DEFERRABLE (initially immediate).
320    Deferrable,
321    /// DEFERRABLE INITIALLY DEFERRED.
322    InitiallyDeferred,
323    /// DEFERRABLE INITIALLY IMMEDIATE.
324    InitiallyImmediate,
325}
326
327// ============================================================================
328// Phase 3: GENERATED Columns
329// ============================================================================
330
331/// GENERATED column type
332#[derive(Debug, Clone)]
333pub enum Generated {
334    /// GENERATED ALWAYS AS (expr) STORED
335    AlwaysStored(String),
336    /// GENERATED ALWAYS AS IDENTITY
337    AlwaysIdentity,
338    /// GENERATED BY DEFAULT AS IDENTITY
339    ByDefaultIdentity,
340}
341
342// ============================================================================
343// Phase 4: Advanced Index Types
344// ============================================================================
345
346/// Index method (USING clause)
347#[derive(Debug, Clone, Default, PartialEq)]
348pub enum IndexMethod {
349    #[default]
350    /// B-tree (default for most columns).
351    BTree,
352    /// Hash (equality-only lookups).
353    Hash,
354    /// GIN (full-text search, JSONB).
355    Gin,
356    /// GiST (geometric, range types).
357    Gist,
358    /// BRIN (large, naturally-ordered tables).
359    Brin,
360    /// SP-GiST (space-partitioned).
361    SpGist,
362}
363
364// ============================================================================
365// Phase 7: Extensions, Comments, Sequences
366// ============================================================================
367
368/// PostgreSQL extension (e.g. `CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`)
369#[derive(Debug, Clone, PartialEq)]
370pub struct Extension {
371    /// Extension name (e.g. `"uuid-ossp"`).
372    pub name: String,
373    /// Target schema.
374    pub schema: Option<String>,
375    /// Pinned version.
376    pub version: Option<String>,
377}
378
379impl Extension {
380    /// Create a new extension declaration.
381    pub fn new(name: impl Into<String>) -> Self {
382        Self {
383            name: name.into(),
384            schema: None,
385            version: None,
386        }
387    }
388
389    /// Set the target schema.
390    pub fn schema(mut self, schema: impl Into<String>) -> Self {
391        self.schema = Some(schema.into());
392        self
393    }
394
395    /// Pin to a specific version.
396    pub fn version(mut self, version: impl Into<String>) -> Self {
397        self.version = Some(version.into());
398        self
399    }
400}
401
402/// COMMENT ON TABLE/COLUMN
403#[derive(Debug, Clone, PartialEq)]
404pub struct Comment {
405    /// What the comment is attached to.
406    pub target: CommentTarget,
407    /// Comment text.
408    pub text: String,
409}
410
411/// Target of a COMMENT ON statement.
412#[derive(Debug, Clone, PartialEq)]
413pub enum CommentTarget {
414    /// COMMENT ON TABLE.
415    Table(String),
416    /// COMMENT ON COLUMN.
417    Column {
418        /// Table name.
419        table: String,
420        /// Column name.
421        column: String,
422    },
423}
424
425impl Comment {
426    /// Create a comment on a table.
427    pub fn on_table(table: impl Into<String>, text: impl Into<String>) -> Self {
428        Self {
429            target: CommentTarget::Table(table.into()),
430            text: text.into(),
431        }
432    }
433
434    /// Create a comment on a column.
435    pub fn on_column(
436        table: impl Into<String>,
437        column: impl Into<String>,
438        text: impl Into<String>,
439    ) -> Self {
440        Self {
441            target: CommentTarget::Column {
442                table: table.into(),
443                column: column.into(),
444            },
445            text: text.into(),
446        }
447    }
448}
449
450/// Standalone sequence (CREATE SEQUENCE)
451#[derive(Debug, Clone, PartialEq)]
452pub struct Sequence {
453    /// Sequence name.
454    pub name: String,
455    /// Data type (e.g. `"bigint"`).
456    pub data_type: Option<String>,
457    /// START WITH value.
458    pub start: Option<i64>,
459    /// INCREMENT BY value.
460    pub increment: Option<i64>,
461    /// Minimum value for the sequence (MINVALUE clause).
462    pub min_value: Option<i64>,
463    /// Maximum value for the sequence (MAXVALUE clause).
464    pub max_value: Option<i64>,
465    /// CACHE size.
466    pub cache: Option<i64>,
467    /// Whether the sequence wraps around.
468    pub cycle: bool,
469    /// OWNED BY column reference.
470    pub owned_by: Option<String>,
471}
472
473impl Sequence {
474    /// Create a new sequence.
475    pub fn new(name: impl Into<String>) -> Self {
476        Self {
477            name: name.into(),
478            data_type: None,
479            start: None,
480            increment: None,
481            min_value: None,
482            max_value: None,
483            cache: None,
484            cycle: false,
485            owned_by: None,
486        }
487    }
488
489    /// Set the START WITH value.
490    pub fn start(mut self, v: i64) -> Self {
491        self.start = Some(v);
492        self
493    }
494
495    /// Set the INCREMENT BY value.
496    pub fn increment(mut self, v: i64) -> Self {
497        self.increment = Some(v);
498        self
499    }
500
501    /// Set the MINVALUE.
502    pub fn min_value(mut self, v: i64) -> Self {
503        self.min_value = Some(v);
504        self
505    }
506
507    /// Set the MAXVALUE.
508    pub fn max_value(mut self, v: i64) -> Self {
509        self.max_value = Some(v);
510        self
511    }
512
513    /// Set the CACHE size.
514    pub fn cache(mut self, v: i64) -> Self {
515        self.cache = Some(v);
516        self
517    }
518
519    /// Enable CYCLE (wrap around at limit).
520    pub fn cycle(mut self) -> Self {
521        self.cycle = true;
522        self
523    }
524
525    /// Set the OWNED BY column reference.
526    pub fn owned_by(mut self, col: impl Into<String>) -> Self {
527        self.owned_by = Some(col.into());
528        self
529    }
530}
531
532// ============================================================================
533// Phase 8: Standalone Enums, Multi-Column FK
534// ============================================================================
535
536/// Standalone ENUM type (CREATE TYPE ... AS ENUM)
537#[derive(Debug, Clone, PartialEq)]
538pub struct EnumType {
539    /// Type name.
540    pub name: String,
541    /// Allowed values.
542    pub values: Vec<String>,
543}
544
545impl EnumType {
546    /// Create a new enum type.
547    pub fn new(name: impl Into<String>, values: Vec<String>) -> Self {
548        Self {
549            name: name.into(),
550            values,
551        }
552    }
553
554    /// Add a new value (for ALTER TYPE ADD VALUE)
555    pub fn add_value(mut self, value: impl Into<String>) -> Self {
556        self.values.push(value.into());
557        self
558    }
559}
560
561/// Table-level multi-column foreign key
562#[derive(Debug, Clone, PartialEq)]
563pub struct MultiColumnForeignKey {
564    /// Source columns.
565    pub columns: Vec<String>,
566    /// Referenced table.
567    pub ref_table: String,
568    /// Referenced columns.
569    pub ref_columns: Vec<String>,
570    /// ON DELETE action.
571    pub on_delete: FkAction,
572    /// ON UPDATE action.
573    pub on_update: FkAction,
574    /// Deferral mode.
575    pub deferrable: Deferrable,
576    /// Optional constraint name.
577    pub name: Option<String>,
578}
579
580impl MultiColumnForeignKey {
581    /// Create a new multi-column foreign key.
582    pub fn new(
583        columns: Vec<String>,
584        ref_table: impl Into<String>,
585        ref_columns: Vec<String>,
586    ) -> Self {
587        Self {
588            columns,
589            ref_table: ref_table.into(),
590            ref_columns,
591            on_delete: FkAction::default(),
592            on_update: FkAction::default(),
593            deferrable: Deferrable::default(),
594            name: None,
595        }
596    }
597
598    /// Set the ON DELETE action.
599    pub fn on_delete(mut self, action: FkAction) -> Self {
600        self.on_delete = action;
601        self
602    }
603
604    /// Set the ON UPDATE action.
605    pub fn on_update(mut self, action: FkAction) -> Self {
606        self.on_update = action;
607        self
608    }
609
610    /// Set an explicit constraint name.
611    pub fn named(mut self, name: impl Into<String>) -> Self {
612        self.name = Some(name.into());
613        self
614    }
615}
616
617// ============================================================================
618// Phase 9: Views, Functions, Triggers, Grants
619// ============================================================================
620
621/// A SQL view definition.
622#[derive(Debug, Clone, PartialEq)]
623pub struct ViewDef {
624    /// View name.
625    pub name: String,
626    /// Underlying SQL query.
627    pub query: String,
628    /// Whether this is a MATERIALIZED VIEW.
629    pub materialized: bool,
630}
631
632impl ViewDef {
633    /// Create a standard (non-materialized) view.
634    pub fn new(name: impl Into<String>, query: impl Into<String>) -> Self {
635        Self {
636            name: name.into(),
637            query: query.into(),
638            materialized: false,
639        }
640    }
641
642    /// Mark as MATERIALIZED VIEW.
643    pub fn materialized(mut self) -> Self {
644        self.materialized = true;
645        self
646    }
647}
648
649/// A PL/pgSQL function definition for the schema model.
650#[derive(Debug, Clone, PartialEq)]
651pub struct SchemaFunctionDef {
652    /// Function name.
653    pub name: String,
654    /// Function arguments (e.g. `"p_id uuid"`).
655    pub args: Vec<String>,
656    /// Return type.
657    pub returns: String,
658    /// Function body.
659    pub body: String,
660    /// Language (default `"plpgsql"`).
661    pub language: String,
662    /// Volatility category (VOLATILE, STABLE, IMMUTABLE).
663    pub volatility: Option<String>,
664}
665
666impl SchemaFunctionDef {
667    /// Create a new function definition.
668    pub fn new(
669        name: impl Into<String>,
670        returns: impl Into<String>,
671        body: impl Into<String>,
672    ) -> Self {
673        Self {
674            name: name.into(),
675            args: Vec::new(),
676            returns: returns.into(),
677            body: body.into(),
678            language: "plpgsql".to_string(),
679            volatility: None,
680        }
681    }
682
683    /// Set the function language.
684    pub fn language(mut self, lang: impl Into<String>) -> Self {
685        self.language = lang.into();
686        self
687    }
688
689    /// Add a function argument.
690    pub fn arg(mut self, arg: impl Into<String>) -> Self {
691        self.args.push(arg.into());
692        self
693    }
694
695    /// Set the volatility category.
696    pub fn volatility(mut self, v: impl Into<String>) -> Self {
697        self.volatility = Some(v.into());
698        self
699    }
700}
701
702/// A trigger definition for the schema model.
703#[derive(Debug, Clone, PartialEq)]
704pub struct SchemaTriggerDef {
705    /// Trigger name.
706    pub name: String,
707    /// Target table.
708    pub table: String,
709    /// Timing (BEFORE, AFTER, INSTEAD OF).
710    pub timing: String,
711    /// Events that fire the trigger (INSERT, UPDATE, DELETE).
712    pub events: Vec<String>,
713    /// Whether the trigger fires FOR EACH ROW (vs. FOR EACH STATEMENT).
714    pub for_each_row: bool,
715    /// Function to execute.
716    pub execute_function: String,
717    /// Optional WHEN condition.
718    pub condition: Option<String>,
719}
720
721impl SchemaTriggerDef {
722    /// Create a new trigger definition.
723    pub fn new(
724        name: impl Into<String>,
725        table: impl Into<String>,
726        execute_function: impl Into<String>,
727    ) -> Self {
728        Self {
729            name: name.into(),
730            table: table.into(),
731            timing: "BEFORE".to_string(),
732            events: vec!["INSERT".to_string()],
733            for_each_row: true,
734            execute_function: execute_function.into(),
735            condition: None,
736        }
737    }
738
739    /// Set the trigger timing.
740    pub fn timing(mut self, t: impl Into<String>) -> Self {
741        self.timing = t.into();
742        self
743    }
744
745    /// Set the trigger events.
746    pub fn events(mut self, evts: Vec<String>) -> Self {
747        self.events = evts;
748        self
749    }
750
751    /// Fire FOR EACH STATEMENT instead of FOR EACH ROW.
752    pub fn for_each_statement(mut self) -> Self {
753        self.for_each_row = false;
754        self
755    }
756
757    /// Set an optional WHEN condition.
758    pub fn condition(mut self, cond: impl Into<String>) -> Self {
759        self.condition = Some(cond.into());
760        self
761    }
762}
763
764/// GRANT or REVOKE permission.
765#[derive(Debug, Clone, PartialEq)]
766pub struct Grant {
767    /// GRANT or REVOKE.
768    pub action: GrantAction,
769    /// Privileges being granted/revoked.
770    pub privileges: Vec<Privilege>,
771    /// Target object (table, schema, sequence).
772    pub on_object: String,
773    /// Role receiving (or losing) the privileges.
774    pub to_role: String,
775}
776
777/// Whether a permission statement is a GRANT or REVOKE.
778#[derive(Debug, Clone, PartialEq, Default)]
779pub enum GrantAction {
780    #[default]
781    /// Grant privileges.
782    Grant,
783    /// Revoke privileges.
784    Revoke,
785}
786
787/// SQL privilege type.
788#[derive(Debug, Clone, PartialEq)]
789pub enum Privilege {
790    /// ALL PRIVILEGES.
791    All,
792    /// SELECT.
793    Select,
794    /// INSERT.
795    Insert,
796    /// UPDATE.
797    Update,
798    /// DELETE.
799    Delete,
800    /// USAGE (on schemas, sequences).
801    Usage,
802    /// EXECUTE (on functions).
803    Execute,
804}
805
806impl std::fmt::Display for Privilege {
807    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808        match self {
809            Privilege::All => write!(f, "ALL"),
810            Privilege::Select => write!(f, "SELECT"),
811            Privilege::Insert => write!(f, "INSERT"),
812            Privilege::Update => write!(f, "UPDATE"),
813            Privilege::Delete => write!(f, "DELETE"),
814            Privilege::Usage => write!(f, "USAGE"),
815            Privilege::Execute => write!(f, "EXECUTE"),
816        }
817    }
818}
819
820impl Grant {
821    /// Create a GRANT statement.
822    pub fn new(
823        privileges: Vec<Privilege>,
824        on_object: impl Into<String>,
825        to_role: impl Into<String>,
826    ) -> Self {
827        Self {
828            action: GrantAction::Grant,
829            privileges,
830            on_object: on_object.into(),
831            to_role: to_role.into(),
832        }
833    }
834
835    /// Create a REVOKE statement.
836    pub fn revoke(
837        privileges: Vec<Privilege>,
838        on_object: impl Into<String>,
839        from_role: impl Into<String>,
840    ) -> Self {
841        Self {
842            action: GrantAction::Revoke,
843            privileges,
844            on_object: on_object.into(),
845            to_role: from_role.into(),
846        }
847    }
848}
849
850impl Schema {
851    /// Create an empty schema.
852    pub fn new() -> Self {
853        Self::default()
854    }
855
856    /// Add a table definition.
857    pub fn add_table(&mut self, table: Table) {
858        self.tables.insert(table.name.clone(), table);
859    }
860
861    /// Add an index definition.
862    pub fn add_index(&mut self, index: Index) {
863        self.indexes.push(index);
864    }
865
866    /// Add a migration hint.
867    pub fn add_hint(&mut self, hint: MigrationHint) {
868        self.migrations.push(hint);
869    }
870
871    /// Add a PostgreSQL extension.
872    pub fn add_extension(&mut self, ext: Extension) {
873        self.extensions.push(ext);
874    }
875
876    /// Add a schema comment.
877    pub fn add_comment(&mut self, comment: Comment) {
878        self.comments.push(comment);
879    }
880
881    /// Add a standalone sequence.
882    pub fn add_sequence(&mut self, seq: Sequence) {
883        self.sequences.push(seq);
884    }
885
886    /// Add a standalone ENUM type.
887    pub fn add_enum(&mut self, enum_type: EnumType) {
888        self.enums.push(enum_type);
889    }
890
891    /// Add a view definition.
892    pub fn add_view(&mut self, view: ViewDef) {
893        self.views.push(view);
894    }
895
896    /// Add a function definition.
897    pub fn add_function(&mut self, func: SchemaFunctionDef) {
898        self.functions.push(func);
899    }
900
901    /// Add a trigger definition.
902    pub fn add_trigger(&mut self, trigger: SchemaTriggerDef) {
903        self.triggers.push(trigger);
904    }
905
906    /// Add a GRANT or REVOKE.
907    pub fn add_grant(&mut self, grant: Grant) {
908        self.grants.push(grant);
909    }
910
911    /// Add an infrastructure resource declaration.
912    pub fn add_resource(&mut self, resource: ResourceDef) {
913        self.resources.push(resource);
914    }
915
916    /// Add an RLS policy definition.
917    pub fn add_policy(&mut self, policy: RlsPolicy) {
918        self.policies.push(policy);
919    }
920
921    /// Validate all foreign key references in the schema.
922    pub fn validate(&self) -> Result<(), Vec<String>> {
923        let mut errors = Vec::new();
924
925        for table in self.tables.values() {
926            for col in &table.columns {
927                if let Some(ref fk) = col.foreign_key {
928                    if !self.tables.contains_key(&fk.table) {
929                        errors.push(format!(
930                            "FK error: {}.{} references non-existent table '{}'",
931                            table.name, col.name, fk.table
932                        ));
933                    } else {
934                        let ref_table = &self.tables[&fk.table];
935                        if !ref_table.columns.iter().any(|c| c.name == fk.column) {
936                            errors.push(format!(
937                                "FK error: {}.{} references non-existent column '{}.{}'",
938                                table.name, col.name, fk.table, fk.column
939                            ));
940                        }
941                    }
942                }
943            }
944        }
945
946        if errors.is_empty() {
947            Ok(())
948        } else {
949            Err(errors)
950        }
951    }
952}
953
954impl Table {
955    /// Create a new empty table.
956    pub fn new(name: impl Into<String>) -> Self {
957        Self {
958            name: name.into(),
959            columns: Vec::new(),
960            multi_column_fks: Vec::new(),
961            enable_rls: false,
962            force_rls: false,
963        }
964    }
965
966    /// Add a column (builder pattern).
967    pub fn column(mut self, col: Column) -> Self {
968        self.columns.push(col);
969        self
970    }
971
972    /// Add a table-level multi-column foreign key
973    pub fn foreign_key(mut self, fk: MultiColumnForeignKey) -> Self {
974        self.multi_column_fks.push(fk);
975        self
976    }
977}
978
979impl Column {
980    fn primary_key_type_error(&self) -> String {
981        format!(
982            "Column '{}' of type {} cannot be a primary key. \
983             Valid PK types: scalar/indexable types \
984             (UUID, TEXT, VARCHAR, INT, BIGINT, SERIAL, BIGSERIAL, BOOLEAN, FLOAT, DECIMAL, \
985             TIMESTAMP, TIMESTAMPTZ, DATE, TIME, ENUM, INET, CIDR, MACADDR)",
986            self.name,
987            self.data_type.name()
988        )
989    }
990
991    fn unique_type_error(&self) -> String {
992        format!(
993            "Column '{}' of type {} cannot have UNIQUE constraint. \
994             JSONB and BYTEA types do not support standard indexing.",
995            self.name,
996            self.data_type.name()
997        )
998    }
999
1000    /// Create a new column with compile-time type validation.
1001    pub fn new(name: impl Into<String>, data_type: ColumnType) -> Self {
1002        Self {
1003            name: name.into(),
1004            data_type,
1005            nullable: true,
1006            primary_key: false,
1007            unique: false,
1008            default: None,
1009            foreign_key: None,
1010            check: None,
1011            generated: None,
1012        }
1013    }
1014
1015    /// Mark as NOT NULL.
1016    pub fn not_null(mut self) -> Self {
1017        self.nullable = false;
1018        self
1019    }
1020
1021    /// Set as primary key with compile-time validation.
1022    /// Validates that the column type can be a primary key.
1023    ///
1024    /// This method is fail-soft: invalid type combinations are allowed to
1025    /// continue without panicking so production callers cannot crash on
1026    /// dynamic schema input. Use [`Column::try_primary_key`] for strict mode.
1027    pub fn primary_key(mut self) -> Self {
1028        if !self.data_type.can_be_primary_key() {
1029            #[cfg(debug_assertions)]
1030            eprintln!("QAIL: {}", self.primary_key_type_error());
1031        }
1032        self.primary_key = true;
1033        self.nullable = false;
1034        self
1035    }
1036
1037    /// Strict variant of [`Column::primary_key`].
1038    ///
1039    /// Returns an error instead of panicking when type policy disallows PK.
1040    pub fn try_primary_key(mut self) -> Result<Self, String> {
1041        if !self.data_type.can_be_primary_key() {
1042            return Err(self.primary_key_type_error());
1043        }
1044        self.primary_key = true;
1045        self.nullable = false;
1046        Ok(self)
1047    }
1048
1049    /// Set as unique with compile-time validation.
1050    /// Validates that the column type supports indexing.
1051    ///
1052    /// This method is fail-soft: invalid type combinations are allowed to
1053    /// continue without panicking so production callers cannot crash on
1054    /// dynamic schema input. Use [`Column::try_unique`] for strict mode.
1055    pub fn unique(mut self) -> Self {
1056        if !self.data_type.supports_indexing() {
1057            #[cfg(debug_assertions)]
1058            eprintln!("QAIL: {}", self.unique_type_error());
1059        }
1060        self.unique = true;
1061        self
1062    }
1063
1064    /// Strict variant of [`Column::unique`].
1065    ///
1066    /// Returns an error instead of panicking when type policy disallows UNIQUE.
1067    pub fn try_unique(mut self) -> Result<Self, String> {
1068        if !self.data_type.supports_indexing() {
1069            return Err(self.unique_type_error());
1070        }
1071        self.unique = true;
1072        Ok(self)
1073    }
1074
1075    /// Set a DEFAULT value expression.
1076    pub fn default(mut self, val: impl Into<String>) -> Self {
1077        self.default = Some(val.into());
1078        self
1079    }
1080
1081    /// Add a foreign key reference to another table.
1082    /// # Example
1083    /// ```ignore
1084    /// Column::new("user_id", ColumnType::Uuid)
1085    ///     .references("users", "id")
1086    ///     .on_delete(FkAction::Cascade)
1087    /// ```
1088    pub fn references(mut self, table: &str, column: &str) -> Self {
1089        self.foreign_key = Some(ForeignKey {
1090            table: table.to_string(),
1091            column: column.to_string(),
1092            on_delete: FkAction::default(),
1093            on_update: FkAction::default(),
1094            deferrable: Deferrable::default(),
1095        });
1096        self
1097    }
1098
1099    /// Set the ON DELETE action for the foreign key.
1100    pub fn on_delete(mut self, action: FkAction) -> Self {
1101        if let Some(ref mut fk) = self.foreign_key {
1102            fk.on_delete = action;
1103        }
1104        self
1105    }
1106
1107    /// Set the ON UPDATE action for the foreign key.
1108    pub fn on_update(mut self, action: FkAction) -> Self {
1109        if let Some(ref mut fk) = self.foreign_key {
1110            fk.on_update = action;
1111        }
1112        self
1113    }
1114
1115    // ==================== Phase 1: CHECK ====================
1116
1117    /// Add a CHECK constraint (AST-native)
1118    pub fn check(mut self, expr: CheckExpr) -> Self {
1119        self.check = Some(CheckConstraint { expr, name: None });
1120        self
1121    }
1122
1123    /// Add a named CHECK constraint
1124    pub fn check_named(mut self, name: impl Into<String>, expr: CheckExpr) -> Self {
1125        self.check = Some(CheckConstraint {
1126            expr,
1127            name: Some(name.into()),
1128        });
1129        self
1130    }
1131
1132    // ==================== Phase 2: DEFERRABLE ====================
1133
1134    /// Make foreign key DEFERRABLE
1135    pub fn deferrable(mut self) -> Self {
1136        if let Some(ref mut fk) = self.foreign_key {
1137            fk.deferrable = Deferrable::Deferrable;
1138        }
1139        self
1140    }
1141
1142    /// Make foreign key DEFERRABLE INITIALLY DEFERRED
1143    pub fn initially_deferred(mut self) -> Self {
1144        if let Some(ref mut fk) = self.foreign_key {
1145            fk.deferrable = Deferrable::InitiallyDeferred;
1146        }
1147        self
1148    }
1149
1150    /// Make foreign key DEFERRABLE INITIALLY IMMEDIATE
1151    pub fn initially_immediate(mut self) -> Self {
1152        if let Some(ref mut fk) = self.foreign_key {
1153            fk.deferrable = Deferrable::InitiallyImmediate;
1154        }
1155        self
1156    }
1157
1158    // ==================== Phase 3: GENERATED ====================
1159
1160    /// GENERATED ALWAYS AS (expr) STORED
1161    pub fn generated_stored(mut self, expr: impl Into<String>) -> Self {
1162        self.generated = Some(Generated::AlwaysStored(expr.into()));
1163        self
1164    }
1165
1166    /// GENERATED ALWAYS AS IDENTITY
1167    pub fn generated_identity(mut self) -> Self {
1168        self.generated = Some(Generated::AlwaysIdentity);
1169        self
1170    }
1171
1172    /// GENERATED BY DEFAULT AS IDENTITY
1173    pub fn generated_by_default(mut self) -> Self {
1174        self.generated = Some(Generated::ByDefaultIdentity);
1175        self
1176    }
1177}
1178
1179impl Index {
1180    /// Create a new index on the given columns.
1181    pub fn new(name: impl Into<String>, table: impl Into<String>, columns: Vec<String>) -> Self {
1182        Self {
1183            name: name.into(),
1184            table: table.into(),
1185            columns,
1186            unique: false,
1187            method: IndexMethod::default(),
1188            where_clause: None,
1189            include: Vec::new(),
1190            concurrently: false,
1191            expressions: Vec::new(),
1192        }
1193    }
1194
1195    /// Create an expression index (e.g. `CREATE INDEX ON t ((lower(email)))`)
1196    pub fn expression(
1197        name: impl Into<String>,
1198        table: impl Into<String>,
1199        expressions: Vec<String>,
1200    ) -> Self {
1201        Self {
1202            name: name.into(),
1203            table: table.into(),
1204            columns: Vec::new(),
1205            unique: false,
1206            method: IndexMethod::default(),
1207            where_clause: None,
1208            include: Vec::new(),
1209            concurrently: false,
1210            expressions,
1211        }
1212    }
1213
1214    /// Mark this index as UNIQUE.
1215    pub fn unique(mut self) -> Self {
1216        self.unique = true;
1217        self
1218    }
1219
1220    // ==================== Phase 4: Advanced Index Options ====================
1221
1222    /// Set index method (USING clause)
1223    pub fn using(mut self, method: IndexMethod) -> Self {
1224        self.method = method;
1225        self
1226    }
1227
1228    /// Create a partial index with WHERE clause
1229    pub fn partial(mut self, expr: CheckExpr) -> Self {
1230        self.where_clause = Some(expr);
1231        self
1232    }
1233
1234    /// Add INCLUDE columns (covering index)
1235    pub fn include(mut self, cols: Vec<String>) -> Self {
1236        self.include = cols;
1237        self
1238    }
1239
1240    /// Create index CONCURRENTLY
1241    pub fn concurrently(mut self) -> Self {
1242        self.concurrently = true;
1243        self
1244    }
1245}
1246
1247/// Format a Schema to .qail format string.
1248/// Convert FkAction to its QAIL string representation
1249fn fk_action_str(action: &FkAction) -> &'static str {
1250    match action {
1251        FkAction::NoAction => "no_action",
1252        FkAction::Cascade => "cascade",
1253        FkAction::SetNull => "set_null",
1254        FkAction::SetDefault => "set_default",
1255        FkAction::Restrict => "restrict",
1256    }
1257}
1258
1259/// Serialize CheckExpr to QAIL check syntax
1260fn check_expr_str(expr: &CheckExpr) -> String {
1261    match expr {
1262        CheckExpr::GreaterThan { column, value } => format!("{} > {}", column, value),
1263        CheckExpr::GreaterOrEqual { column, value } => format!("{} >= {}", column, value),
1264        CheckExpr::LessThan { column, value } => format!("{} < {}", column, value),
1265        CheckExpr::LessOrEqual { column, value } => format!("{} <= {}", column, value),
1266        CheckExpr::Between { column, low, high } => format!("{} between {} {}", column, low, high),
1267        CheckExpr::In { column, values } => format!("{} in [{}]", column, values.join(", ")),
1268        CheckExpr::Regex { column, pattern } => format!("{} ~ '{}'", column, pattern),
1269        CheckExpr::MaxLength { column, max } => format!("length({}) <= {}", column, max),
1270        CheckExpr::MinLength { column, min } => format!("length({}) >= {}", column, min),
1271        CheckExpr::NotNull { column } => format!("{} not_null", column),
1272        CheckExpr::And(l, r) => format!("{} and {}", check_expr_str(l), check_expr_str(r)),
1273        CheckExpr::Or(l, r) => format!("{} or {}", check_expr_str(l), check_expr_str(r)),
1274        CheckExpr::Not(e) => format!("not {}", check_expr_str(e)),
1275    }
1276}
1277
1278/// Serialize a `Schema` back to a QAIL-format string.
1279pub fn to_qail_string(schema: &Schema) -> String {
1280    let mut output = String::new();
1281    output.push_str("# QAIL Schema\n\n");
1282
1283    // Extensions first (must be created before any DDL)
1284    for ext in &schema.extensions {
1285        let mut line = format!("extension \"{}\"", ext.name);
1286        if let Some(ref s) = ext.schema {
1287            line.push_str(&format!(" schema {}", s));
1288        }
1289        if let Some(ref v) = ext.version {
1290            line.push_str(&format!(" version \"{}\"", v));
1291        }
1292        output.push_str(&line);
1293        output.push('\n');
1294    }
1295    if !schema.extensions.is_empty() {
1296        output.push('\n');
1297    }
1298
1299    // Enums (CREATE TYPE ... AS ENUM, must precede tables)
1300    for enum_type in &schema.enums {
1301        let values = enum_type
1302            .values
1303            .iter()
1304            .map(|v| v.as_str())
1305            .collect::<Vec<_>>()
1306            .join(", ");
1307        output.push_str(&format!("enum {} {{ {} }}\n", enum_type.name, values));
1308    }
1309    if !schema.enums.is_empty() {
1310        output.push('\n');
1311    }
1312
1313    // Sequences (before tables, since columns may reference them)
1314    for seq in &schema.sequences {
1315        if seq.start.is_some()
1316            || seq.increment.is_some()
1317            || seq.min_value.is_some()
1318            || seq.max_value.is_some()
1319            || seq.cache.is_some()
1320            || seq.cycle
1321            || seq.owned_by.is_some()
1322        {
1323            let mut opts = Vec::new();
1324            if let Some(v) = seq.start {
1325                opts.push(format!("start {}", v));
1326            }
1327            if let Some(v) = seq.increment {
1328                opts.push(format!("increment {}", v));
1329            }
1330            if let Some(v) = seq.min_value {
1331                opts.push(format!("minvalue {}", v));
1332            }
1333            if let Some(v) = seq.max_value {
1334                opts.push(format!("maxvalue {}", v));
1335            }
1336            if let Some(v) = seq.cache {
1337                opts.push(format!("cache {}", v));
1338            }
1339            if seq.cycle {
1340                opts.push("cycle".to_string());
1341            }
1342            if let Some(ref o) = seq.owned_by {
1343                opts.push(format!("owned_by {}", o));
1344            }
1345            output.push_str(&format!("sequence {} {{ {} }}\n", seq.name, opts.join(" ")));
1346        } else {
1347            output.push_str(&format!("sequence {}\n", seq.name));
1348        }
1349    }
1350    if !schema.sequences.is_empty() {
1351        output.push('\n');
1352    }
1353
1354    let mut table_names: Vec<&String> = schema.tables.keys().collect();
1355    table_names.sort();
1356    for table_name in table_names {
1357        let table = &schema.tables[table_name];
1358        output.push_str(&format!("table {} {{\n", table.name));
1359        for col in &table.columns {
1360            let mut constraints: Vec<String> = Vec::new();
1361            if col.primary_key {
1362                constraints.push("primary_key".to_string());
1363            }
1364            if !col.nullable && !col.primary_key {
1365                constraints.push("not_null".to_string());
1366            }
1367            if col.unique {
1368                constraints.push("unique".to_string());
1369            }
1370            if let Some(def) = &col.default {
1371                constraints.push(format!("default {}", def));
1372            }
1373            if let Some(ref fk) = col.foreign_key {
1374                let mut fk_str = format!("references {}({})", fk.table, fk.column);
1375                if fk.on_delete != FkAction::NoAction {
1376                    fk_str.push_str(&format!(" on_delete {}", fk_action_str(&fk.on_delete)));
1377                }
1378                if fk.on_update != FkAction::NoAction {
1379                    fk_str.push_str(&format!(" on_update {}", fk_action_str(&fk.on_update)));
1380                }
1381                match &fk.deferrable {
1382                    Deferrable::Deferrable => fk_str.push_str(" deferrable"),
1383                    Deferrable::InitiallyDeferred => fk_str.push_str(" initially_deferred"),
1384                    Deferrable::InitiallyImmediate => fk_str.push_str(" initially_immediate"),
1385                    Deferrable::NotDeferrable => {} // default, omit
1386                }
1387                constraints.push(fk_str);
1388            }
1389            if let Some(ref check) = col.check {
1390                constraints.push(format!("check({})", check_expr_str(&check.expr)));
1391            }
1392
1393            let constraint_str = if constraints.is_empty() {
1394                String::new()
1395            } else {
1396                format!(" {}", constraints.join(" "))
1397            };
1398
1399            output.push_str(&format!(
1400                "  {} {}{}\n",
1401                col.name,
1402                col.data_type.to_pg_type(),
1403                constraint_str
1404            ));
1405        }
1406        // Multi-column foreign keys
1407        for fk in &table.multi_column_fks {
1408            output.push_str(&format!(
1409                "  foreign_key ({}) references {}({})\n",
1410                fk.columns.join(", "),
1411                fk.ref_table,
1412                fk.ref_columns.join(", ")
1413            ));
1414        }
1415        // RLS directives
1416        if table.enable_rls {
1417            output.push_str("  enable_rls\n");
1418        }
1419        if table.force_rls {
1420            output.push_str("  force_rls\n");
1421        }
1422        output.push_str("}\n\n");
1423    }
1424
1425    for idx in &schema.indexes {
1426        let unique = if idx.unique { "unique " } else { "" };
1427        let cols = if !idx.expressions.is_empty() {
1428            idx.expressions.join(", ")
1429        } else {
1430            idx.columns.join(", ")
1431        };
1432        output.push_str(&format!(
1433            "{}index {} on {} ({})\n",
1434            unique, idx.name, idx.table, cols
1435        ));
1436    }
1437
1438    for hint in &schema.migrations {
1439        match hint {
1440            MigrationHint::Rename { from, to } => {
1441                output.push_str(&format!("rename {} -> {}\n", from, to));
1442            }
1443            MigrationHint::Transform { expression, target } => {
1444                output.push_str(&format!("transform {} -> {}\n", expression, target));
1445            }
1446            MigrationHint::Drop { target, confirmed } => {
1447                let confirm = if *confirmed { " confirm" } else { "" };
1448                output.push_str(&format!("drop {}{}\n", target, confirm));
1449            }
1450        }
1451    }
1452
1453    // Views
1454    for view in &schema.views {
1455        let prefix = if view.materialized {
1456            "materialized view"
1457        } else {
1458            "view"
1459        };
1460        output.push_str(&format!(
1461            "{} {} $$\n{}\n$$\n\n",
1462            prefix, view.name, view.query
1463        ));
1464    }
1465
1466    // Functions
1467    for func in &schema.functions {
1468        let args = func.args.join(", ");
1469        output.push_str(&format!(
1470            "function {}({}) returns {} language {} $$\n{}\n$$\n\n",
1471            func.name, args, func.returns, func.language, func.body
1472        ));
1473    }
1474
1475    // Triggers
1476    for trigger in &schema.triggers {
1477        let events = trigger.events.join(" or ");
1478        output.push_str(&format!(
1479            "trigger {} on {} {} {} execute {}\n",
1480            trigger.name,
1481            trigger.table,
1482            trigger.timing.to_lowercase(),
1483            events.to_lowercase(),
1484            trigger.execute_function
1485        ));
1486    }
1487    if !schema.triggers.is_empty() {
1488        output.push('\n');
1489    }
1490
1491    // Policies
1492    for policy in &schema.policies {
1493        let cmd = match policy.target {
1494            PolicyTarget::All => "all",
1495            PolicyTarget::Select => "select",
1496            PolicyTarget::Insert => "insert",
1497            PolicyTarget::Update => "update",
1498            PolicyTarget::Delete => "delete",
1499        };
1500        let perm = match policy.permissiveness {
1501            PolicyPermissiveness::Permissive => "",
1502            PolicyPermissiveness::Restrictive => " restrictive",
1503        };
1504        let role_str = match &policy.role {
1505            Some(r) => format!(" to {}", r),
1506            None => String::new(),
1507        };
1508        output.push_str(&format!(
1509            "policy {} on {} for {}{}{}",
1510            policy.name, policy.table, cmd, role_str, perm
1511        ));
1512        if let Some(ref using) = policy.using {
1513            output.push_str(&format!("\n  using $$ {} $$", using));
1514        }
1515        if let Some(ref wc) = policy.with_check {
1516            output.push_str(&format!("\n  with_check $$ {} $$", wc));
1517        }
1518        output.push_str("\n\n");
1519    }
1520
1521    // Grants
1522    for grant in &schema.grants {
1523        let privs: Vec<String> = grant
1524            .privileges
1525            .iter()
1526            .map(|p| p.to_string().to_lowercase())
1527            .collect();
1528        match grant.action {
1529            GrantAction::Grant => {
1530                output.push_str(&format!(
1531                    "grant {} on {} to {}\n",
1532                    privs.join(", "),
1533                    grant.on_object,
1534                    grant.to_role
1535                ));
1536            }
1537            GrantAction::Revoke => {
1538                output.push_str(&format!(
1539                    "revoke {} on {} from {}\n",
1540                    privs.join(", "),
1541                    grant.on_object,
1542                    grant.to_role
1543                ));
1544            }
1545        }
1546    }
1547    if !schema.grants.is_empty() {
1548        output.push('\n');
1549    }
1550
1551    // Comments last (tables must exist first)
1552    for comment in &schema.comments {
1553        match &comment.target {
1554            CommentTarget::Table(t) => {
1555                output.push_str(&format!("comment on {} \"{}\"\n", t, comment.text));
1556            }
1557            CommentTarget::Column { table, column } => {
1558                output.push_str(&format!(
1559                    "comment on {}.{} \"{}\"\n",
1560                    table, column, comment.text
1561                ));
1562            }
1563        }
1564    }
1565
1566    output
1567}
1568
1569/// Convert a Schema to a list of Qail commands (CREATE TABLE, CREATE INDEX).
1570/// Used by shadow migration to apply the base schema before applying diffs.
1571pub fn schema_to_commands(schema: &Schema) -> Vec<crate::ast::Qail> {
1572    use crate::ast::{Action, Constraint, Expr, IndexDef, Qail};
1573
1574    let mut cmds = Vec::new();
1575
1576    // Sort tables to handle dependencies (tables with FK refs should come after their targets)
1577    let mut table_order: Vec<&Table> = schema.tables.values().collect();
1578    table_order.sort_by(|a, b| {
1579        let a_has_fk = a.columns.iter().any(|c| c.foreign_key.is_some());
1580        let b_has_fk = b.columns.iter().any(|c| c.foreign_key.is_some());
1581        a_has_fk.cmp(&b_has_fk)
1582    });
1583
1584    for table in table_order {
1585        // Build columns using Expr::Def exactly like diff.rs does
1586        let columns: Vec<Expr> = table
1587            .columns
1588            .iter()
1589            .map(|col| {
1590                let mut constraints = Vec::new();
1591
1592                if col.primary_key {
1593                    constraints.push(Constraint::PrimaryKey);
1594                }
1595                if col.nullable {
1596                    constraints.push(Constraint::Nullable);
1597                }
1598                if col.unique {
1599                    constraints.push(Constraint::Unique);
1600                }
1601                if let Some(def) = &col.default {
1602                    constraints.push(Constraint::Default(def.clone()));
1603                }
1604                if let Some(ref fk) = col.foreign_key {
1605                    constraints.push(Constraint::References(format!(
1606                        "{}({})",
1607                        fk.table, fk.column
1608                    )));
1609                }
1610
1611                Expr::Def {
1612                    name: col.name.clone(),
1613                    data_type: col.data_type.to_pg_type(),
1614                    constraints,
1615                }
1616            })
1617            .collect();
1618
1619        cmds.push(Qail {
1620            action: Action::Make,
1621            table: table.name.clone(),
1622            columns,
1623            ..Default::default()
1624        });
1625    }
1626
1627    // Add indexes using IndexDef like diff.rs
1628    for idx in &schema.indexes {
1629        cmds.push(Qail {
1630            action: Action::Index,
1631            table: String::new(),
1632            index_def: Some(IndexDef {
1633                name: idx.name.clone(),
1634                table: idx.table.clone(),
1635                columns: idx.columns.clone(),
1636                unique: idx.unique,
1637                index_type: None,
1638            }),
1639            ..Default::default()
1640        });
1641    }
1642
1643    cmds
1644}
1645
1646#[cfg(test)]
1647mod tests {
1648    use super::*;
1649
1650    #[test]
1651    fn test_schema_builder() {
1652        let mut schema = Schema::new();
1653
1654        let users = Table::new("users")
1655            .column(Column::new("id", ColumnType::Serial).primary_key())
1656            .column(Column::new("name", ColumnType::Text).not_null())
1657            .column(Column::new("email", ColumnType::Text).unique());
1658
1659        schema.add_table(users);
1660        schema.add_index(Index::new("idx_users_email", "users", vec!["email".into()]).unique());
1661
1662        let output = to_qail_string(&schema);
1663        assert!(output.contains("table users"));
1664        assert!(output.contains("id SERIAL primary_key"));
1665        assert!(output.contains("unique index idx_users_email"));
1666    }
1667
1668    #[test]
1669    fn test_migration_hints() {
1670        let mut schema = Schema::new();
1671        schema.add_hint(MigrationHint::Rename {
1672            from: "users.username".into(),
1673            to: "users.name".into(),
1674        });
1675
1676        let output = to_qail_string(&schema);
1677        assert!(output.contains("rename users.username -> users.name"));
1678    }
1679
1680    #[test]
1681    fn test_invalid_primary_key_type_strict() {
1682        let err = Column::new("data", ColumnType::Jsonb)
1683            .try_primary_key()
1684            .expect_err("JSONB should be rejected by strict PK policy");
1685        assert!(err.contains("cannot be a primary key"));
1686    }
1687
1688    #[test]
1689    fn test_invalid_primary_key_type_fail_soft() {
1690        let col = Column::new("data", ColumnType::Jsonb).primary_key();
1691        assert!(col.primary_key);
1692        assert!(!col.nullable);
1693    }
1694
1695    #[test]
1696    fn test_invalid_unique_type_strict() {
1697        let err = Column::new("data", ColumnType::Jsonb)
1698            .try_unique()
1699            .expect_err("JSONB should be rejected by strict UNIQUE policy");
1700        assert!(err.contains("cannot have UNIQUE"));
1701    }
1702
1703    #[test]
1704    fn test_invalid_unique_type_fail_soft() {
1705        let col = Column::new("data", ColumnType::Jsonb).unique();
1706        assert!(col.unique);
1707    }
1708
1709    #[test]
1710    fn test_foreign_key_valid() {
1711        let mut schema = Schema::new();
1712
1713        schema.add_table(
1714            Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1715        );
1716
1717        schema.add_table(
1718            Table::new("posts")
1719                .column(Column::new("id", ColumnType::Uuid).primary_key())
1720                .column(
1721                    Column::new("user_id", ColumnType::Uuid)
1722                        .references("users", "id")
1723                        .on_delete(FkAction::Cascade),
1724                ),
1725        );
1726
1727        // Should pass validation
1728        assert!(schema.validate().is_ok());
1729    }
1730
1731    #[test]
1732    fn test_foreign_key_invalid_table() {
1733        let mut schema = Schema::new();
1734
1735        schema.add_table(
1736            Table::new("posts")
1737                .column(Column::new("id", ColumnType::Uuid).primary_key())
1738                .column(Column::new("user_id", ColumnType::Uuid).references("nonexistent", "id")),
1739        );
1740
1741        // Should fail validation
1742        let result = schema.validate();
1743        assert!(result.is_err());
1744        assert!(result.unwrap_err()[0].contains("non-existent table"));
1745    }
1746
1747    #[test]
1748    fn test_foreign_key_invalid_column() {
1749        let mut schema = Schema::new();
1750
1751        schema.add_table(
1752            Table::new("users").column(Column::new("id", ColumnType::Uuid).primary_key()),
1753        );
1754
1755        schema.add_table(
1756            Table::new("posts")
1757                .column(Column::new("id", ColumnType::Uuid).primary_key())
1758                .column(
1759                    Column::new("user_id", ColumnType::Uuid).references("users", "wrong_column"),
1760                ),
1761        );
1762
1763        // Should fail validation
1764        let result = schema.validate();
1765        assert!(result.is_err());
1766        assert!(result.unwrap_err()[0].contains("non-existent column"));
1767    }
1768}