1pub 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#[derive(Debug, Clone)]
16pub struct ConvertOptions {
17 pub schema: Option<String>,
19 pub include_all_schemas: bool,
21 pub enable_foreign_keys: bool,
23 pub strict: bool,
25 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#[derive(Debug)]
43pub struct ConvertResult {
44 pub sqlite_sql: String,
46 pub warnings: Vec<Warning>,
48}
49
50#[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
60pub fn convert_pg_ddl_to_sqlite(
64 input: &str,
65 opts: &ConvertOptions,
66) -> Result<ConvertResult, ConvertError> {
67 let mut warnings = Vec::new();
68
69 let (mut model, parse_warnings) = pg::parser::parse(input);
71 warnings.extend(parse_warnings);
72
73 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 transform::planner::plan(&mut model, &mut warnings);
82
83 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 if let Some(default) = &col.default {
95 col.default = transform::expr_map::map_expr(default, &obj, &mut warnings);
96 }
97 }
98 }
99
100 transform::constraint::transform_constraints(
102 &mut model,
103 opts.enable_foreign_keys,
104 &mut warnings,
105 );
106
107 transform::index::transform_indexes(&mut model, &mut warnings);
109
110 transform::name_resolve::resolve_names(&mut model, opts.include_all_schemas, &mut warnings);
112
113 if opts.enable_foreign_keys {
115 transform::topo::topological_sort(&mut model.tables, &mut warnings);
116 } else {
117 model
119 .tables
120 .sort_by(|a, b| a.name.name.normalized.cmp(&b.name.name.normalized));
121 }
122
123 let sqlite_sql = sqlite::render::render(&model, opts.enable_foreign_keys);
125
126 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 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}