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 if col.autoincrement {
117 return;
118 }
119
120 let is_integer = matches!(col.sqlite_type, Some(SqliteType::Integer))
121 || matches!(
122 col.pg_type,
123 PgType::Integer
124 | PgType::BigInt
125 | PgType::SmallInt
126 | PgType::Serial
127 | PgType::BigSerial
128 | PgType::SmallSerial
129 );
130
131 if is_integer {
132 col.is_primary_key = true;
133 table.constraints.remove(pk_idx);
134 }
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::ir::{Column, FkAction, Ident, QualifiedName};
143
144 fn make_column(name: &str, pg_type: PgType) -> Column {
145 Column {
146 name: Ident::new(name),
147 pg_type,
148 sqlite_type: None,
149 not_null: false,
150 default: None,
151 is_primary_key: false,
152 is_unique: false,
153 autoincrement: false,
154 references: None,
155 check: None,
156 }
157 }
158
159 fn make_table(name: &str, columns: Vec<Column>, constraints: Vec<TableConstraint>) -> Table {
160 Table {
161 name: QualifiedName::new(Ident::new(name)),
162 columns,
163 constraints,
164 }
165 }
166
167 #[test]
168 fn test_fk_dropped_when_disabled() {
169 let mut model = SchemaModel {
170 tables: vec![make_table(
171 "orders",
172 vec![make_column("id", PgType::Integer)],
173 vec![TableConstraint::ForeignKey {
174 name: None,
175 columns: vec![Ident::new("user_id")],
176 ref_table: QualifiedName::new(Ident::new("users")),
177 ref_columns: vec![Ident::new("id")],
178 on_delete: Some(FkAction::Cascade),
179 on_update: None,
180 deferrable: false,
181 }],
182 )],
183 ..Default::default()
184 };
185 let mut w = Vec::new();
186 transform_constraints(&mut model, false, &mut w);
187 assert!(model.tables[0].constraints.is_empty());
188 }
189
190 #[test]
191 fn test_fk_kept_when_enabled() {
192 let mut model = SchemaModel {
193 tables: vec![make_table(
194 "orders",
195 vec![make_column("id", PgType::Integer)],
196 vec![TableConstraint::ForeignKey {
197 name: None,
198 columns: vec![Ident::new("user_id")],
199 ref_table: QualifiedName::new(Ident::new("users")),
200 ref_columns: vec![Ident::new("id")],
201 on_delete: Some(FkAction::Cascade),
202 on_update: None,
203 deferrable: false,
204 }],
205 )],
206 ..Default::default()
207 };
208 let mut w = Vec::new();
209 transform_constraints(&mut model, true, &mut w);
210 assert_eq!(model.tables[0].constraints.len(), 1);
211 }
212
213 #[test]
214 fn test_single_integer_pk_promoted() {
215 let mut model = SchemaModel {
216 tables: vec![make_table(
217 "t",
218 vec![make_column("id", PgType::Integer)],
219 vec![TableConstraint::PrimaryKey {
220 name: None,
221 columns: vec![Ident::new("id")],
222 }],
223 )],
224 ..Default::default()
225 };
226 let mut w = Vec::new();
227 transform_constraints(&mut model, false, &mut w);
228 assert!(model.tables[0].columns[0].is_primary_key);
229 assert!(model.tables[0].constraints.is_empty());
231 }
232}