pg2sqlite_core/transform/
constraint.rs1use crate::diagnostics::warning::{self, Severity, Warning};
3use crate::ir::{PgType, SchemaModel, SqliteType, Table, TableConstraint};
4use crate::transform::expr_map;
5
6pub 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_integer_pk(table);
26
27 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 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 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 if !enable_foreign_keys {
92 col.references = None;
93 }
94 }
95}
96
97fn 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 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 assert!(model.tables[0].constraints.is_empty());
225 }
226}