Skip to main content

pg2sqlite_core/transform/
planner.rs

1/// Planner: merge ALTER TABLE constraints and resolve SERIAL/IDENTITY/sequences.
2use crate::diagnostics::warning::{self, Severity, Warning};
3use crate::ir::{Expr, PgType, SchemaModel, TableConstraint};
4
5/// Plan and merge ALTER TABLE constraints into CREATE TABLE, resolve SERIAL/sequences.
6pub 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
13/// Merge ALTER TABLE ADD CONSTRAINT statements into the corresponding CREATE TABLE.
14fn 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
44/// Resolve identity columns: if a column has both IDENTITY and single-column PK,
45/// convert to INTEGER PRIMARY KEY AUTOINCREMENT.
46fn 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        // Find the table-level PK columns
73        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        // Find the column
83        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        // Check if this column is the sole PK
106        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            // preserve original NOT NULL (implicit in SQLite INTEGER PRIMARY KEY)
121            col.default = None;
122
123            // Remove the table-level PK constraint if it was there
124            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
149/// Resolve SERIAL/BIGSERIAL/SMALLSERIAL columns:
150/// - If column is single-column integer PK → mark as INTEGER PRIMARY KEY (rowid alias)
151/// - Otherwise → map type to INTEGER, drop the DEFAULT, warn
152fn resolve_serials(model: &mut SchemaModel, warnings: &mut Vec<Warning>) {
153    // Collect sequence names for reference
154    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        // Find if there's a table-level PK
162        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            // Also check for nextval default (SERIAL sugar)
184            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            // Is this column the sole PK?
193            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                // Remove nextval default if present
202                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    // Warn about standalone sequences
227    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
242/// Resolve enum columns: replace PgType::Other with PgType::Enum where a matching enum exists.
243fn 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); // preserved from original PG DDL
397        assert_eq!(col.pg_type, PgType::Integer);
398        assert!(model.tables[0].constraints.is_empty()); // PK removed from table-level
399        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}