Skip to main content

pg2sqlite_core/
lib.rs

1//! pg2sqlite-core: PostgreSQL 16 DDL to SQLite3 DDL converter library.
2
3pub mod diagnostics;
4pub mod ir;
5pub mod pg;
6pub mod sqlite;
7pub mod transform;
8
9use std::path::PathBuf;
10
11use diagnostics::warning::Warning;
12use diagnostics::{StrictViolation, check_strict};
13
14/// Options for the DDL conversion.
15#[derive(Debug, Clone)]
16pub struct ConvertOptions {
17    /// Schema to filter by (default: "public").
18    pub schema: Option<String>,
19    /// If true, include all schemas (bypass schema filtering).
20    pub include_all_schemas: bool,
21    /// If true, emit `PRAGMA foreign_keys = ON;` and include FK constraints.
22    pub enable_foreign_keys: bool,
23    /// If true, fail on lossy conversions.
24    pub strict: bool,
25    /// Path for warning output (None = stderr).
26    pub emit_warnings: Option<PathBuf>,
27}
28
29impl Default for ConvertOptions {
30    fn default() -> Self {
31        Self {
32            schema: Some("public".to_string()),
33            include_all_schemas: false,
34            enable_foreign_keys: false,
35            strict: false,
36            emit_warnings: None,
37        }
38    }
39}
40
41/// Result of a successful conversion.
42#[derive(Debug)]
43pub struct ConvertResult {
44    /// The generated SQLite DDL text.
45    pub sqlite_sql: String,
46    /// Warnings emitted during conversion.
47    pub warnings: Vec<Warning>,
48}
49
50/// Errors that can occur during conversion.
51#[derive(Debug, thiserror::Error)]
52pub enum ConvertError {
53    #[error("Strict mode violation:\n{0}")]
54    StrictViolation(#[from] StrictViolation),
55
56    #[error("I/O error: {0}")]
57    Io(#[from] std::io::Error),
58}
59
60/// Convert PostgreSQL DDL text to SQLite DDL.
61///
62/// This is the primary public API for the library.
63pub fn convert_pg_ddl_to_sqlite(
64    input: &str,
65    opts: &ConvertOptions,
66) -> Result<ConvertResult, ConvertError> {
67    let mut warnings = Vec::new();
68
69    // 1. Parse PG DDL → IR
70    let (mut model, parse_warnings) = pg::parser::parse(input);
71    warnings.extend(parse_warnings);
72
73    // 2. Normalize (schema filtering)
74    let normalize_opts = pg::normalize::NormalizeOptions {
75        schema: opts.schema.clone(),
76        include_all_schemas: opts.include_all_schemas,
77    };
78    pg::normalize::normalize(&mut model, &normalize_opts);
79
80    // 3. Plan (merge ALTERs, resolve SERIAL/sequences)
81    transform::planner::plan(&mut model, &mut warnings);
82
83    // 4. Transform types
84    for table in &mut model.tables {
85        for col in &mut table.columns {
86            let obj = format!("{}.{}", table.name.name.normalized, col.name.normalized);
87            col.sqlite_type = Some(transform::type_map::map_type(
88                &col.pg_type,
89                &obj,
90                &mut warnings,
91            ));
92
93            // Transform default expressions
94            if let Some(default) = &col.default {
95                col.default = transform::expr_map::map_expr(default, &obj, &mut warnings);
96            }
97        }
98    }
99
100    // 5. Transform constraints
101    transform::constraint::transform_constraints(
102        &mut model,
103        opts.enable_foreign_keys,
104        &mut warnings,
105    );
106
107    // 6. Transform indexes
108    transform::index::transform_indexes(&mut model, &mut warnings);
109
110    // 7. Resolve names (schema stripping, collision handling)
111    transform::name_resolve::resolve_names(&mut model, opts.include_all_schemas, &mut warnings);
112
113    // 8. Topological sort (if FK enabled)
114    if opts.enable_foreign_keys {
115        transform::topo::topological_sort(&mut model.tables, &mut warnings);
116    } else {
117        // Alphabetical order when FKs disabled
118        model
119            .tables
120            .sort_by(|a, b| a.name.name.normalized.cmp(&b.name.name.normalized));
121    }
122
123    // 9. Render SQLite DDL
124    let sqlite_sql = sqlite::render::render(&model, opts.enable_foreign_keys);
125
126    // 10. Check strict mode
127    if opts.strict {
128        check_strict(&warnings)?;
129    }
130
131    Ok(ConvertResult {
132        sqlite_sql,
133        warnings,
134    })
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_basic_conversion() {
143        let input = "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);";
144        let result = convert_pg_ddl_to_sqlite(input, &ConvertOptions::default()).unwrap();
145        assert!(result.sqlite_sql.contains("CREATE TABLE users"));
146        assert!(result.sqlite_sql.contains("id INTEGER PRIMARY KEY"));
147        assert!(result.sqlite_sql.contains("name TEXT NOT NULL"));
148    }
149
150    #[test]
151    fn test_schema_filtering() {
152        let input = r#"
153            CREATE TABLE public.users (id INTEGER);
154            CREATE TABLE other.accounts (id INTEGER);
155        "#;
156        let result = convert_pg_ddl_to_sqlite(input, &ConvertOptions::default()).unwrap();
157        assert!(result.sqlite_sql.contains("users"));
158        assert!(!result.sqlite_sql.contains("accounts"));
159    }
160
161    #[test]
162    fn test_fk_with_pragma() {
163        let input = r#"
164            CREATE TABLE users (id INTEGER PRIMARY KEY);
165            CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER REFERENCES users(id));
166        "#;
167        let opts = ConvertOptions {
168            enable_foreign_keys: true,
169            ..Default::default()
170        };
171        let result = convert_pg_ddl_to_sqlite(input, &opts).unwrap();
172        assert!(result.sqlite_sql.contains("PRAGMA foreign_keys = ON;"));
173        assert!(result.sqlite_sql.contains("REFERENCES users(id)"));
174    }
175
176    #[test]
177    fn test_fk_ordering() {
178        let input = r#"
179            CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER REFERENCES users(id));
180            CREATE TABLE users (id INTEGER PRIMARY KEY);
181        "#;
182        let opts = ConvertOptions {
183            enable_foreign_keys: true,
184            ..Default::default()
185        };
186        let result = convert_pg_ddl_to_sqlite(input, &opts).unwrap();
187        let users_pos = result.sqlite_sql.find("CREATE TABLE users").unwrap();
188        let orders_pos = result.sqlite_sql.find("CREATE TABLE orders").unwrap();
189        assert!(
190            users_pos < orders_pos,
191            "users should come before orders due to FK dependency"
192        );
193    }
194
195    #[test]
196    fn test_strict_mode_fails_on_lossy() {
197        let input = "CREATE TABLE t (active BOOLEAN DEFAULT true);";
198        let opts = ConvertOptions {
199            strict: true,
200            ..Default::default()
201        };
202        let result = convert_pg_ddl_to_sqlite(input, &opts);
203        assert!(result.is_err());
204    }
205
206    #[test]
207    fn test_boolean_conversion() {
208        let input = "CREATE TABLE t (active BOOLEAN DEFAULT true);";
209        let result = convert_pg_ddl_to_sqlite(input, &ConvertOptions::default()).unwrap();
210        assert!(result.sqlite_sql.contains("INTEGER"));
211        assert!(result.sqlite_sql.contains("DEFAULT 1"));
212    }
213
214    #[test]
215    fn test_timestamp_default_now() {
216        let input = "CREATE TABLE t (created_at TIMESTAMP DEFAULT now());";
217        let result = convert_pg_ddl_to_sqlite(input, &ConvertOptions::default()).unwrap();
218        assert!(result.sqlite_sql.contains("TEXT"));
219        assert!(result.sqlite_sql.contains("CURRENT_TIMESTAMP"));
220    }
221
222    #[test]
223    fn test_varchar_length_dropped() {
224        let input = "CREATE TABLE t (name VARCHAR(255) NOT NULL);";
225        let result = convert_pg_ddl_to_sqlite(input, &ConvertOptions::default()).unwrap();
226        assert!(result.sqlite_sql.contains("TEXT NOT NULL"));
227        assert!(
228            result
229                .warnings
230                .iter()
231                .any(|w| w.code == "VARCHAR_LENGTH_IGNORED")
232        );
233    }
234
235    #[test]
236    fn test_alter_table_constraint_merged() {
237        let input = r#"
238            CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER);
239            ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
240        "#;
241        let opts = ConvertOptions {
242            enable_foreign_keys: true,
243            ..Default::default()
244        };
245        let result = convert_pg_ddl_to_sqlite(input, &opts).unwrap();
246        assert!(
247            result
248                .sqlite_sql
249                .contains("FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE")
250        );
251    }
252
253    #[test]
254    fn test_identity_pk_autoincrement() {
255        let input = r#"
256            CREATE TABLE seed (
257                id smallint NOT NULL,
258                name text NOT NULL
259            );
260
261            ALTER TABLE public.seed ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
262                SEQUENCE NAME public.seed_id_seq
263                START WITH 1
264                INCREMENT BY 1
265                NO MINVALUE
266                NO MAXVALUE
267                CACHE 1
268            );
269
270            ALTER TABLE ONLY public.seed
271                ADD CONSTRAINT seed_pkey PRIMARY KEY (id);
272        "#;
273        let result = convert_pg_ddl_to_sqlite(input, &ConvertOptions::default()).unwrap();
274        assert!(
275            result
276                .sqlite_sql
277                .contains("INTEGER PRIMARY KEY AUTOINCREMENT")
278        );
279        // The id column should NOT have NOT NULL (implicit in SQLite PK)
280        assert!(!result.sqlite_sql.contains("id INTEGER NOT NULL"));
281    }
282
283    #[test]
284    fn test_include_all_schemas() {
285        let input = r#"
286            CREATE TABLE public.users (id INTEGER);
287            CREATE TABLE other.accounts (id INTEGER);
288        "#;
289        let opts = ConvertOptions {
290            include_all_schemas: true,
291            ..Default::default()
292        };
293        let result = convert_pg_ddl_to_sqlite(input, &opts).unwrap();
294        assert!(result.sqlite_sql.contains("users"));
295        assert!(result.sqlite_sql.contains("accounts"));
296    }
297}