pg2sqlite_core/transform/
planner.rs1use crate::diagnostics::warning::{self, Severity, Warning};
3use crate::ir::{Expr, PgType, SchemaModel, TableConstraint};
4
5pub fn plan(model: &mut SchemaModel, warnings: &mut Vec<Warning>) {
7 merge_alter_constraints(model, warnings);
8 resolve_identity(model, warnings);
9 resolve_serials(model, warnings);
10 resolve_enums(model, warnings);
11}
12
13fn merge_alter_constraints(model: &mut SchemaModel, warnings: &mut Vec<Warning>) {
15 let alters = std::mem::take(&mut model.alter_constraints);
16
17 for alter in alters {
18 let target_table = model
19 .tables
20 .iter_mut()
21 .find(|t| t.name.name_eq(&alter.table));
22
23 match target_table {
24 Some(table) => {
25 table.constraints.push(alter.constraint);
26 }
27 None => {
28 warnings.push(
29 Warning::new(
30 warning::ALTER_TARGET_MISSING,
31 Severity::Unsupported,
32 format!(
33 "ALTER TABLE target '{}' not found; constraint skipped",
34 alter.table.name.normalized
35 ),
36 )
37 .with_object(&alter.table.name.normalized),
38 );
39 }
40 }
41 }
42}
43
44fn resolve_identity(model: &mut SchemaModel, warnings: &mut Vec<Warning>) {
47 let identities = std::mem::take(&mut model.identity_columns);
48
49 for identity in identities {
50 let target_table = model
51 .tables
52 .iter_mut()
53 .find(|t| t.name.name_eq(&identity.table));
54
55 let Some(table) = target_table else {
56 warnings.push(
57 Warning::new(
58 warning::ALTER_TARGET_MISSING,
59 Severity::Unsupported,
60 format!(
61 "ALTER TABLE target '{}' not found; identity skipped",
62 identity.table.name.normalized
63 ),
64 )
65 .with_object(&identity.table.name.normalized),
66 );
67 continue;
68 };
69
70 let table_name = table.name.name.normalized.clone();
71
72 let pk_info: Option<(usize, Vec<String>)> =
74 table.constraints.iter().enumerate().find_map(|(i, c)| {
75 if let TableConstraint::PrimaryKey { columns, .. } = c {
76 Some((i, columns.iter().map(|c| c.normalized.clone()).collect()))
77 } else {
78 None
79 }
80 });
81
82 let col = table
84 .columns
85 .iter_mut()
86 .find(|c| c.name.normalized == identity.column.normalized);
87
88 let Some(col) = col else {
89 warnings.push(
90 Warning::new(
91 warning::ALTER_TARGET_MISSING,
92 Severity::Unsupported,
93 format!(
94 "identity column '{}.{}' not found; skipped",
95 table_name, identity.column.normalized
96 ),
97 )
98 .with_object(format!("{}.{}", table_name, identity.column.normalized)),
99 );
100 continue;
101 };
102
103 let obj = format!("{}.{}", table_name, col.name.normalized);
104
105 let is_sole_pk = col.is_primary_key
107 || pk_info
108 .as_ref()
109 .is_some_and(|(_, cols)| cols.len() == 1 && cols[0] == col.name.normalized);
110
111 let is_integer = matches!(
112 col.pg_type,
113 PgType::Integer | PgType::BigInt | PgType::SmallInt
114 );
115
116 if is_sole_pk && is_integer {
117 col.pg_type = PgType::Integer;
118 col.is_primary_key = true;
119 col.autoincrement = true;
120 col.default = None;
122
123 if let Some((pk_idx, _)) = pk_info {
125 table.constraints.remove(pk_idx);
126 }
127
128 warnings.push(
129 Warning::new(
130 warning::IDENTITY_TO_AUTOINCREMENT,
131 Severity::Lossy,
132 "IDENTITY + PRIMARY KEY mapped to INTEGER PRIMARY KEY AUTOINCREMENT",
133 )
134 .with_object(&obj),
135 );
136 } else if !is_sole_pk {
137 warnings.push(
138 Warning::new(
139 warning::IDENTITY_NO_PK,
140 Severity::Unsupported,
141 "IDENTITY column has no single-column primary key; identity ignored",
142 )
143 .with_object(&obj),
144 );
145 }
146 }
147}
148
149fn resolve_serials(model: &mut SchemaModel, warnings: &mut Vec<Warning>) {
153 let _sequence_names: Vec<String> = model
155 .sequences
156 .iter()
157 .map(|s| s.name.name.normalized.clone())
158 .collect();
159
160 for table in &mut model.tables {
161 let table_pk_columns: Vec<String> = table
163 .constraints
164 .iter()
165 .filter_map(|c| match c {
166 TableConstraint::PrimaryKey { columns, .. } => Some(
167 columns
168 .iter()
169 .map(|c| c.normalized.clone())
170 .collect::<Vec<_>>(),
171 ),
172 _ => None,
173 })
174 .flatten()
175 .collect();
176
177 for col in &mut table.columns {
178 let is_serial = matches!(
179 col.pg_type,
180 PgType::Serial | PgType::BigSerial | PgType::SmallSerial
181 );
182
183 let has_nextval = matches!(&col.default, Some(Expr::NextVal(_)));
185
186 if !is_serial && !has_nextval {
187 continue;
188 }
189
190 let obj = format!("{}.{}", table.name.name.normalized, col.name.normalized);
191
192 let is_sole_pk = col.is_primary_key
194 || (table_pk_columns.len() == 1 && table_pk_columns[0] == col.name.normalized);
195
196 if is_sole_pk {
197 col.pg_type = PgType::Integer;
198 col.is_primary_key = true;
199 col.default = None;
200
201 warnings.push(
203 Warning::new(
204 warning::SERIAL_TO_ROWID,
205 Severity::Lossy,
206 "SERIAL column mapped to INTEGER PRIMARY KEY (rowid alias)",
207 )
208 .with_object(&obj),
209 );
210 } else {
211 col.pg_type = PgType::Integer;
212 col.default = None;
213
214 warnings.push(
215 Warning::new(
216 warning::SERIAL_NOT_PRIMARY_KEY,
217 Severity::Lossy,
218 "SERIAL column is not the sole primary key; mapped to INTEGER without auto-increment",
219 )
220 .with_object(&obj),
221 );
222 }
223 }
224 }
225
226 for seq in &model.sequences {
228 warnings.push(
229 Warning::new(
230 warning::SEQUENCE_IGNORED,
231 Severity::Info,
232 format!(
233 "sequence '{}' ignored (absorbed into SERIAL handling or unused)",
234 seq.name.name.normalized
235 ),
236 )
237 .with_object(&seq.name.name.normalized),
238 );
239 }
240}
241
242fn resolve_enums(model: &mut SchemaModel, _warnings: &mut [Warning]) {
244 let enum_names: std::collections::HashSet<String> = model
245 .enums
246 .iter()
247 .map(|e| e.name.name.normalized.clone())
248 .collect();
249
250 for table in &mut model.tables {
251 for col in &mut table.columns {
252 if let PgType::Other { name } = &col.pg_type
253 && enum_names.contains(name)
254 {
255 col.pg_type = PgType::Enum { name: name.clone() };
256 }
257 }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::ir::{AlterConstraint, Column, FkAction, Ident, QualifiedName, Table};
265
266 fn make_table(name: &str, columns: Vec<Column>, constraints: Vec<TableConstraint>) -> Table {
267 Table {
268 name: QualifiedName::new(Ident::new(name)),
269 columns,
270 constraints,
271 }
272 }
273
274 fn make_column(name: &str, pg_type: PgType) -> Column {
275 Column {
276 name: Ident::new(name),
277 pg_type,
278 sqlite_type: None,
279 not_null: false,
280 default: None,
281 is_primary_key: false,
282 is_unique: false,
283 autoincrement: false,
284 references: None,
285 check: None,
286 }
287 }
288
289 #[test]
290 fn test_merge_alter_constraints() {
291 let mut model = SchemaModel {
292 tables: vec![make_table(
293 "orders",
294 vec![
295 make_column("id", PgType::Integer),
296 make_column("user_id", PgType::Integer),
297 ],
298 vec![],
299 )],
300 alter_constraints: vec![AlterConstraint {
301 table: QualifiedName::new(Ident::new("orders")),
302 constraint: TableConstraint::ForeignKey {
303 name: Some(Ident::new("fk_user")),
304 columns: vec![Ident::new("user_id")],
305 ref_table: QualifiedName::new(Ident::new("users")),
306 ref_columns: vec![Ident::new("id")],
307 on_delete: Some(FkAction::Cascade),
308 on_update: None,
309 deferrable: false,
310 },
311 }],
312 ..Default::default()
313 };
314 let mut w = Vec::new();
315 plan(&mut model, &mut w);
316 assert_eq!(model.tables[0].constraints.len(), 1);
317 }
318
319 #[test]
320 fn test_alter_target_missing() {
321 let mut model = SchemaModel {
322 tables: vec![],
323 alter_constraints: vec![AlterConstraint {
324 table: QualifiedName::new(Ident::new("nonexistent")),
325 constraint: TableConstraint::Check {
326 name: None,
327 expr: Expr::Raw("true".to_string()),
328 },
329 }],
330 ..Default::default()
331 };
332 let mut w = Vec::new();
333 plan(&mut model, &mut w);
334 assert!(w.iter().any(|w| w.code == warning::ALTER_TARGET_MISSING));
335 }
336
337 #[test]
338 fn test_serial_sole_pk() {
339 let mut col = make_column("id", PgType::Serial);
340 col.is_primary_key = true;
341 let mut model = SchemaModel {
342 tables: vec![make_table("users", vec![col], vec![])],
343 ..Default::default()
344 };
345 let mut w = Vec::new();
346 plan(&mut model, &mut w);
347 assert_eq!(model.tables[0].columns[0].pg_type, PgType::Integer);
348 assert!(model.tables[0].columns[0].is_primary_key);
349 assert!(w.iter().any(|w| w.code == warning::SERIAL_TO_ROWID));
350 }
351
352 #[test]
353 fn test_serial_not_pk() {
354 let col = make_column("counter", PgType::Serial);
355 let mut model = SchemaModel {
356 tables: vec![make_table("t", vec![col], vec![])],
357 ..Default::default()
358 };
359 let mut w = Vec::new();
360 plan(&mut model, &mut w);
361 assert_eq!(model.tables[0].columns[0].pg_type, PgType::Integer);
362 assert!(w.iter().any(|w| w.code == warning::SERIAL_NOT_PRIMARY_KEY));
363 }
364
365 #[test]
366 fn test_identity_with_pk_autoincrement() {
367 use crate::ir::AlterIdentity;
368
369 let mut col = make_column("id", PgType::BigInt);
370 col.not_null = true;
371 let mut model = SchemaModel {
372 tables: vec![make_table(
373 "seed",
374 vec![col, make_column("name", PgType::Text)],
375 vec![],
376 )],
377 alter_constraints: vec![AlterConstraint {
378 table: QualifiedName::new(Ident::new("seed")),
379 constraint: TableConstraint::PrimaryKey {
380 name: Some(Ident::new("seed_pkey")),
381 columns: vec![Ident::new("id")],
382 },
383 }],
384 identity_columns: vec![AlterIdentity {
385 table: QualifiedName::new(Ident::new("seed")),
386 column: Ident::new("id"),
387 }],
388 ..Default::default()
389 };
390 let mut w = Vec::new();
391 plan(&mut model, &mut w);
392
393 let col = &model.tables[0].columns[0];
394 assert!(col.autoincrement);
395 assert!(col.is_primary_key);
396 assert!(col.not_null); assert_eq!(col.pg_type, PgType::Integer);
398 assert!(model.tables[0].constraints.is_empty()); assert!(
400 w.iter()
401 .any(|w| w.code == warning::IDENTITY_TO_AUTOINCREMENT)
402 );
403 }
404
405 #[test]
406 fn test_identity_without_pk() {
407 use crate::ir::AlterIdentity;
408
409 let mut col = make_column("id", PgType::BigInt);
410 col.not_null = true;
411 let mut model = SchemaModel {
412 tables: vec![make_table("t", vec![col], vec![])],
413 identity_columns: vec![AlterIdentity {
414 table: QualifiedName::new(Ident::new("t")),
415 column: Ident::new("id"),
416 }],
417 ..Default::default()
418 };
419 let mut w = Vec::new();
420 plan(&mut model, &mut w);
421
422 let col = &model.tables[0].columns[0];
423 assert!(!col.autoincrement);
424 assert!(!col.is_primary_key);
425 assert!(w.iter().any(|w| w.code == warning::IDENTITY_NO_PK));
426 }
427}