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_serials(model, warnings);
9    resolve_enums(model, warnings);
10}
11
12/// Merge ALTER TABLE ADD CONSTRAINT statements into the corresponding CREATE TABLE.
13fn merge_alter_constraints(model: &mut SchemaModel, warnings: &mut Vec<Warning>) {
14    let alters = std::mem::take(&mut model.alter_constraints);
15
16    for alter in alters {
17        let target_table = model.tables.iter_mut().find(|t| t.name == alter.table);
18
19        match target_table {
20            Some(table) => {
21                table.constraints.push(alter.constraint);
22            }
23            None => {
24                warnings.push(
25                    Warning::new(
26                        warning::ALTER_TARGET_MISSING,
27                        Severity::Unsupported,
28                        format!(
29                            "ALTER TABLE target '{}' not found; constraint skipped",
30                            alter.table.name.normalized
31                        ),
32                    )
33                    .with_object(&alter.table.name.normalized),
34                );
35            }
36        }
37    }
38}
39
40/// Resolve SERIAL/BIGSERIAL/SMALLSERIAL columns:
41/// - If column is single-column integer PK → mark as INTEGER PRIMARY KEY (rowid alias)
42/// - Otherwise → map type to INTEGER, drop the DEFAULT, warn
43fn resolve_serials(model: &mut SchemaModel, warnings: &mut Vec<Warning>) {
44    // Collect sequence names for reference
45    let _sequence_names: Vec<String> = model
46        .sequences
47        .iter()
48        .map(|s| s.name.name.normalized.clone())
49        .collect();
50
51    for table in &mut model.tables {
52        // Find if there's a table-level PK
53        let table_pk_columns: Vec<String> = table
54            .constraints
55            .iter()
56            .filter_map(|c| match c {
57                TableConstraint::PrimaryKey { columns, .. } => Some(
58                    columns
59                        .iter()
60                        .map(|c| c.normalized.clone())
61                        .collect::<Vec<_>>(),
62                ),
63                _ => None,
64            })
65            .flatten()
66            .collect();
67
68        for col in &mut table.columns {
69            let is_serial = matches!(
70                col.pg_type,
71                PgType::Serial | PgType::BigSerial | PgType::SmallSerial
72            );
73
74            // Also check for nextval default (SERIAL sugar)
75            let has_nextval = matches!(&col.default, Some(Expr::NextVal(_)));
76
77            if !is_serial && !has_nextval {
78                continue;
79            }
80
81            let obj = format!("{}.{}", table.name.name.normalized, col.name.normalized);
82
83            // Is this column the sole PK?
84            let is_sole_pk = col.is_primary_key
85                || (table_pk_columns.len() == 1 && table_pk_columns[0] == col.name.normalized);
86
87            if is_sole_pk {
88                col.pg_type = PgType::Integer;
89                col.is_primary_key = true;
90                col.default = None;
91
92                // Remove nextval default if present
93                warnings.push(
94                    Warning::new(
95                        warning::SERIAL_TO_ROWID,
96                        Severity::Lossy,
97                        "SERIAL column mapped to INTEGER PRIMARY KEY (rowid alias)",
98                    )
99                    .with_object(&obj),
100                );
101            } else {
102                col.pg_type = PgType::Integer;
103                col.default = None;
104
105                warnings.push(
106                    Warning::new(
107                        warning::SERIAL_NOT_PRIMARY_KEY,
108                        Severity::Lossy,
109                        "SERIAL column is not the sole primary key; mapped to INTEGER without auto-increment",
110                    )
111                    .with_object(&obj),
112                );
113            }
114        }
115    }
116
117    // Warn about standalone sequences
118    for seq in &model.sequences {
119        warnings.push(
120            Warning::new(
121                warning::SEQUENCE_IGNORED,
122                Severity::Info,
123                format!(
124                    "sequence '{}' ignored (absorbed into SERIAL handling or unused)",
125                    seq.name.name.normalized
126                ),
127            )
128            .with_object(&seq.name.name.normalized),
129        );
130    }
131}
132
133/// Resolve enum columns: replace PgType::Other with PgType::Enum where a matching enum exists.
134fn resolve_enums(model: &mut SchemaModel, _warnings: &mut [Warning]) {
135    let enum_names: std::collections::HashSet<String> = model
136        .enums
137        .iter()
138        .map(|e| e.name.name.normalized.clone())
139        .collect();
140
141    for table in &mut model.tables {
142        for col in &mut table.columns {
143            if let PgType::Other { name } = &col.pg_type
144                && enum_names.contains(name)
145            {
146                col.pg_type = PgType::Enum { name: name.clone() };
147            }
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::ir::{AlterConstraint, Column, FkAction, Ident, QualifiedName, Table};
156
157    fn make_table(name: &str, columns: Vec<Column>, constraints: Vec<TableConstraint>) -> Table {
158        Table {
159            name: QualifiedName::new(Ident::new(name)),
160            columns,
161            constraints,
162        }
163    }
164
165    fn make_column(name: &str, pg_type: PgType) -> Column {
166        Column {
167            name: Ident::new(name),
168            pg_type,
169            sqlite_type: None,
170            not_null: false,
171            default: None,
172            is_primary_key: false,
173            is_unique: false,
174            references: None,
175            check: None,
176        }
177    }
178
179    #[test]
180    fn test_merge_alter_constraints() {
181        let mut model = SchemaModel {
182            tables: vec![make_table(
183                "orders",
184                vec![
185                    make_column("id", PgType::Integer),
186                    make_column("user_id", PgType::Integer),
187                ],
188                vec![],
189            )],
190            alter_constraints: vec![AlterConstraint {
191                table: QualifiedName::new(Ident::new("orders")),
192                constraint: TableConstraint::ForeignKey {
193                    name: Some(Ident::new("fk_user")),
194                    columns: vec![Ident::new("user_id")],
195                    ref_table: QualifiedName::new(Ident::new("users")),
196                    ref_columns: vec![Ident::new("id")],
197                    on_delete: Some(FkAction::Cascade),
198                    on_update: None,
199                    deferrable: false,
200                },
201            }],
202            ..Default::default()
203        };
204        let mut w = Vec::new();
205        plan(&mut model, &mut w);
206        assert_eq!(model.tables[0].constraints.len(), 1);
207    }
208
209    #[test]
210    fn test_alter_target_missing() {
211        let mut model = SchemaModel {
212            tables: vec![],
213            alter_constraints: vec![AlterConstraint {
214                table: QualifiedName::new(Ident::new("nonexistent")),
215                constraint: TableConstraint::Check {
216                    name: None,
217                    expr: Expr::Raw("true".to_string()),
218                },
219            }],
220            ..Default::default()
221        };
222        let mut w = Vec::new();
223        plan(&mut model, &mut w);
224        assert!(w.iter().any(|w| w.code == warning::ALTER_TARGET_MISSING));
225    }
226
227    #[test]
228    fn test_serial_sole_pk() {
229        let mut col = make_column("id", PgType::Serial);
230        col.is_primary_key = true;
231        let mut model = SchemaModel {
232            tables: vec![make_table("users", vec![col], vec![])],
233            ..Default::default()
234        };
235        let mut w = Vec::new();
236        plan(&mut model, &mut w);
237        assert_eq!(model.tables[0].columns[0].pg_type, PgType::Integer);
238        assert!(model.tables[0].columns[0].is_primary_key);
239        assert!(w.iter().any(|w| w.code == warning::SERIAL_TO_ROWID));
240    }
241
242    #[test]
243    fn test_serial_not_pk() {
244        let col = make_column("counter", PgType::Serial);
245        let mut model = SchemaModel {
246            tables: vec![make_table("t", vec![col], vec![])],
247            ..Default::default()
248        };
249        let mut w = Vec::new();
250        plan(&mut model, &mut w);
251        assert_eq!(model.tables[0].columns[0].pg_type, PgType::Integer);
252        assert!(w.iter().any(|w| w.code == warning::SERIAL_NOT_PRIMARY_KEY));
253    }
254}