Skip to main content

pg2sqlite_core/transform/
name_resolve.rs

1/// Schema stripping and name collision handling.
2use std::collections::HashMap;
3
4use crate::diagnostics::warning::{self, Severity, Warning};
5use crate::ir::{Ident, QualifiedName, SchemaModel, TableConstraint};
6
7/// Strip schema prefixes from all identifiers.
8/// When include_all_schemas is true and names collide, prefix with schema__table.
9pub fn resolve_names(
10    model: &mut SchemaModel,
11    include_all_schemas: bool,
12    warnings: &mut Vec<Warning>,
13) {
14    if !include_all_schemas {
15        // Just strip schema prefixes
16        strip_schemas(model);
17        return;
18    }
19
20    // Detect collisions
21    let mut name_counts: HashMap<String, Vec<(Option<String>, usize)>> = HashMap::new();
22    for (i, table) in model.tables.iter().enumerate() {
23        let schema = table.name.schema.as_ref().map(|s| s.normalized.clone());
24        name_counts
25            .entry(table.name.name.normalized.clone())
26            .or_default()
27            .push((schema, i));
28    }
29
30    // Build rename map for collisions
31    let mut rename_map: HashMap<(Option<String>, String), String> = HashMap::new();
32    for (name, entries) in &name_counts {
33        if entries.len() > 1 {
34            for (schema, _) in entries {
35                if let Some(s) = schema {
36                    let new_name = format!("{s}__{name}");
37                    rename_map.insert((Some(s.clone()), name.clone()), new_name.clone());
38                    warnings.push(
39                        Warning::new(
40                            warning::SCHEMA_PREFIXED,
41                            Severity::Lossy,
42                            format!(
43                                "table '{s}.{name}' renamed to '{new_name}' to avoid collision"
44                            ),
45                        )
46                        .with_object(&new_name),
47                    );
48                }
49            }
50        }
51    }
52
53    // Apply renames
54    for table in &mut model.tables {
55        let key = (
56            table.name.schema.as_ref().map(|s| s.normalized.clone()),
57            table.name.name.normalized.clone(),
58        );
59        if let Some(new_name) = rename_map.get(&key) {
60            table.name = QualifiedName::new(Ident::new(new_name));
61        } else {
62            table.name.schema = None;
63        }
64    }
65
66    // Rename FK references in constraints
67    for table in &mut model.tables {
68        for constraint in &mut table.constraints {
69            if let TableConstraint::ForeignKey { ref_table, .. } = constraint {
70                let key = (
71                    ref_table.schema.as_ref().map(|s| s.normalized.clone()),
72                    ref_table.name.normalized.clone(),
73                );
74                if let Some(new_name) = rename_map.get(&key) {
75                    *ref_table = QualifiedName::new(Ident::new(new_name));
76                } else {
77                    ref_table.schema = None;
78                }
79            }
80        }
81
82        // Also rename column-level FK refs
83        for col in &mut table.columns {
84            if let Some(fk) = &mut col.references {
85                let key = (
86                    fk.table.schema.as_ref().map(|s| s.normalized.clone()),
87                    fk.table.name.normalized.clone(),
88                );
89                if let Some(new_name) = rename_map.get(&key) {
90                    fk.table = QualifiedName::new(Ident::new(new_name));
91                } else {
92                    fk.table.schema = None;
93                }
94            }
95        }
96    }
97
98    // Rename index table references
99    for index in &mut model.indexes {
100        let key = (
101            index.table.schema.as_ref().map(|s| s.normalized.clone()),
102            index.table.name.normalized.clone(),
103        );
104        if let Some(new_name) = rename_map.get(&key) {
105            index.table = QualifiedName::new(Ident::new(new_name));
106        } else {
107            index.table.schema = None;
108        }
109    }
110}
111
112/// Simple schema stripping (no collision handling).
113fn strip_schemas(model: &mut SchemaModel) {
114    for table in &mut model.tables {
115        table.name.schema = None;
116    }
117    for index in &mut model.indexes {
118        index.table.schema = None;
119    }
120    for table in &mut model.tables {
121        for constraint in &mut table.constraints {
122            if let TableConstraint::ForeignKey { ref_table, .. } = constraint {
123                ref_table.schema = None;
124            }
125        }
126        for col in &mut table.columns {
127            if let Some(fk) = &mut col.references {
128                fk.table.schema = None;
129            }
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::ir::{Column, PgType, Table};
138
139    fn make_table(schema: Option<&str>, name: &str) -> Table {
140        Table {
141            name: match schema {
142                Some(s) => QualifiedName::with_schema(Ident::new(s), Ident::new(name)),
143                None => QualifiedName::new(Ident::new(name)),
144            },
145            columns: vec![Column {
146                name: Ident::new("id"),
147                pg_type: PgType::Integer,
148                sqlite_type: None,
149                not_null: false,
150                default: None,
151                is_primary_key: false,
152                is_unique: false,
153                references: None,
154                check: None,
155            }],
156            constraints: vec![],
157        }
158    }
159
160    #[test]
161    fn test_strip_schemas_single() {
162        let mut model = SchemaModel {
163            tables: vec![make_table(Some("public"), "users")],
164            ..Default::default()
165        };
166        let mut w = Vec::new();
167        resolve_names(&mut model, false, &mut w);
168        assert!(model.tables[0].name.schema.is_none());
169    }
170
171    #[test]
172    fn test_collision_prefixing() {
173        let mut model = SchemaModel {
174            tables: vec![
175                make_table(Some("public"), "users"),
176                make_table(Some("other"), "users"),
177            ],
178            ..Default::default()
179        };
180        let mut w = Vec::new();
181        resolve_names(&mut model, true, &mut w);
182
183        let names: Vec<&str> = model
184            .tables
185            .iter()
186            .map(|t| t.name.name.normalized.as_str())
187            .collect();
188        assert!(names.contains(&"public__users"));
189        assert!(names.contains(&"other__users"));
190        assert!(w.iter().any(|w| w.code == warning::SCHEMA_PREFIXED));
191    }
192}