Skip to main content

pg2sqlite_core/transform/
constraint.rs

1/// Constraint transformation: PK, UNIQUE, FK, CHECK.
2use crate::diagnostics::warning::{self, Severity, Warning};
3use crate::ir::{PgType, SchemaModel, SqliteType, Table, TableConstraint};
4use crate::transform::expr_map;
5
6/// Transform constraints on all tables in the schema model.
7pub fn transform_constraints(
8    model: &mut SchemaModel,
9    enable_foreign_keys: bool,
10    warnings: &mut Vec<Warning>,
11) {
12    for table in &mut model.tables {
13        transform_table_constraints(table, enable_foreign_keys, warnings);
14    }
15}
16
17fn transform_table_constraints(
18    table: &mut Table,
19    enable_foreign_keys: bool,
20    warnings: &mut Vec<Warning>,
21) {
22    let table_name = table.name.name.normalized.clone();
23
24    // Handle single-column integer PK: promote to column-level INTEGER PRIMARY KEY
25    handle_integer_pk(table);
26
27    // Transform CHECK constraint expressions
28    let mut kept_constraints = Vec::new();
29    for constraint in &table.constraints {
30        match constraint {
31            TableConstraint::PrimaryKey { .. } | TableConstraint::Unique { .. } => {
32                kept_constraints.push(constraint.clone());
33            }
34            TableConstraint::ForeignKey { deferrable, .. } => {
35                if !enable_foreign_keys {
36                    continue;
37                }
38                if *deferrable {
39                    warnings.push(
40                        Warning::new(
41                            warning::DEFERRABLE_IGNORED,
42                            Severity::Lossy,
43                            "DEFERRABLE modifier dropped from foreign key",
44                        )
45                        .with_object(&table_name),
46                    );
47                }
48                // Clone without deferrable
49                let mut c = constraint.clone();
50                if let TableConstraint::ForeignKey { deferrable, .. } = &mut c {
51                    *deferrable = false;
52                }
53                kept_constraints.push(c);
54            }
55            TableConstraint::Check { name, expr } => {
56                let obj = format!("{table_name}.CHECK");
57                match expr_map::map_expr(expr, &obj, warnings) {
58                    Some(mapped) => {
59                        kept_constraints.push(TableConstraint::Check {
60                            name: name.clone(),
61                            expr: mapped,
62                        });
63                    }
64                    None => {
65                        warnings.push(
66                            Warning::new(
67                                warning::CHECK_EXPRESSION_UNSUPPORTED,
68                                Severity::Unsupported,
69                                "CHECK constraint expression uses unsupported PG features; dropped",
70                            )
71                            .with_object(&table_name),
72                        );
73                    }
74                }
75            }
76        }
77    }
78    table.constraints = kept_constraints;
79
80    // Transform column-level CHECK constraints
81    for col in &mut table.columns {
82        if let Some(check) = &col.check {
83            let obj = format!("{}.{}", table_name, col.name.normalized);
84            match expr_map::map_expr(check, &obj, warnings) {
85                Some(mapped) => col.check = Some(mapped),
86                None => col.check = None,
87            }
88        }
89
90        // Drop column-level FK refs if foreign keys disabled
91        if !enable_foreign_keys {
92            col.references = None;
93        }
94    }
95}
96
97/// If a table has a single-column integer PK as a table-level constraint,
98/// and the column is an integer type, promote it to column-level.
99fn handle_integer_pk(table: &mut Table) {
100    let pk_constraint = table.constraints.iter().position(
101        |c| matches!(c, TableConstraint::PrimaryKey { columns, .. } if columns.len() == 1),
102    );
103
104    if let Some(pk_idx) = pk_constraint
105        && let TableConstraint::PrimaryKey { columns, .. } = &table.constraints[pk_idx]
106    {
107        let pk_col_name = columns[0].normalized.clone();
108
109        // Check if the column is an integer type
110        let col = table
111            .columns
112            .iter_mut()
113            .find(|c| c.name.normalized == pk_col_name);
114        if let Some(col) = col {
115            let is_integer = matches!(col.sqlite_type, Some(SqliteType::Integer))
116                || matches!(
117                    col.pg_type,
118                    PgType::Integer
119                        | PgType::BigInt
120                        | PgType::SmallInt
121                        | PgType::Serial
122                        | PgType::BigSerial
123                        | PgType::SmallSerial
124                );
125
126            if is_integer {
127                col.is_primary_key = true;
128                table.constraints.remove(pk_idx);
129            }
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::ir::{Column, FkAction, Ident, QualifiedName};
138
139    fn make_column(name: &str, pg_type: PgType) -> Column {
140        Column {
141            name: Ident::new(name),
142            pg_type,
143            sqlite_type: None,
144            not_null: false,
145            default: None,
146            is_primary_key: false,
147            is_unique: false,
148            references: None,
149            check: None,
150        }
151    }
152
153    fn make_table(name: &str, columns: Vec<Column>, constraints: Vec<TableConstraint>) -> Table {
154        Table {
155            name: QualifiedName::new(Ident::new(name)),
156            columns,
157            constraints,
158        }
159    }
160
161    #[test]
162    fn test_fk_dropped_when_disabled() {
163        let mut model = SchemaModel {
164            tables: vec![make_table(
165                "orders",
166                vec![make_column("id", PgType::Integer)],
167                vec![TableConstraint::ForeignKey {
168                    name: None,
169                    columns: vec![Ident::new("user_id")],
170                    ref_table: QualifiedName::new(Ident::new("users")),
171                    ref_columns: vec![Ident::new("id")],
172                    on_delete: Some(FkAction::Cascade),
173                    on_update: None,
174                    deferrable: false,
175                }],
176            )],
177            ..Default::default()
178        };
179        let mut w = Vec::new();
180        transform_constraints(&mut model, false, &mut w);
181        assert!(model.tables[0].constraints.is_empty());
182    }
183
184    #[test]
185    fn test_fk_kept_when_enabled() {
186        let mut model = SchemaModel {
187            tables: vec![make_table(
188                "orders",
189                vec![make_column("id", PgType::Integer)],
190                vec![TableConstraint::ForeignKey {
191                    name: None,
192                    columns: vec![Ident::new("user_id")],
193                    ref_table: QualifiedName::new(Ident::new("users")),
194                    ref_columns: vec![Ident::new("id")],
195                    on_delete: Some(FkAction::Cascade),
196                    on_update: None,
197                    deferrable: false,
198                }],
199            )],
200            ..Default::default()
201        };
202        let mut w = Vec::new();
203        transform_constraints(&mut model, true, &mut w);
204        assert_eq!(model.tables[0].constraints.len(), 1);
205    }
206
207    #[test]
208    fn test_single_integer_pk_promoted() {
209        let mut model = SchemaModel {
210            tables: vec![make_table(
211                "t",
212                vec![make_column("id", PgType::Integer)],
213                vec![TableConstraint::PrimaryKey {
214                    name: None,
215                    columns: vec![Ident::new("id")],
216                }],
217            )],
218            ..Default::default()
219        };
220        let mut w = Vec::new();
221        transform_constraints(&mut model, false, &mut w);
222        assert!(model.tables[0].columns[0].is_primary_key);
223        // Table-level PK should be removed
224        assert!(model.tables[0].constraints.is_empty());
225    }
226}