1use std::fmt;
2
3use bon::bon;
4use diff::Diff;
5use migration::Migrate;
6use sqlparser::{
7 ast::Statement,
8 dialect::{self},
9 parser::{self, Parser},
10};
11use thiserror::Error;
12
13mod diff;
14mod migration;
15pub mod name_gen;
16pub mod path_template;
17
18#[derive(Error, Debug)]
19#[error("Oops, we couldn't parse that!")]
20pub struct ParseError(#[from] parser::ParserError);
21
22#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
23#[cfg_attr(feature = "clap", derive(clap::ValueEnum), clap(rename_all = "lower"))]
24#[non_exhaustive]
25pub enum Dialect {
26 Ansi,
27 BigQuery,
28 ClickHouse,
29 Databricks,
30 DuckDb,
31 #[default]
32 Generic,
33 Hive,
34 MsSql,
35 MySql,
36 PostgreSql,
37 RedshiftSql,
38 Snowflake,
39 SQLite,
40}
41
42impl Dialect {
43 fn to_sqlparser_dialect(self) -> Box<dyn dialect::Dialect> {
44 match self {
45 Self::Ansi => Box::new(dialect::AnsiDialect {}),
46 Self::BigQuery => Box::new(dialect::BigQueryDialect {}),
47 Self::ClickHouse => Box::new(dialect::ClickHouseDialect {}),
48 Self::Databricks => Box::new(dialect::DatabricksDialect {}),
49 Self::DuckDb => Box::new(dialect::DuckDbDialect {}),
50 Self::Generic => Box::new(dialect::GenericDialect {}),
51 Self::Hive => Box::new(dialect::HiveDialect {}),
52 Self::MsSql => Box::new(dialect::MsSqlDialect {}),
53 Self::MySql => Box::new(dialect::MySqlDialect {}),
54 Self::PostgreSql => Box::new(dialect::PostgreSqlDialect {}),
55 Self::RedshiftSql => Box::new(dialect::RedshiftSqlDialect {}),
56 Self::Snowflake => Box::new(dialect::SnowflakeDialect {}),
57 Self::SQLite => Box::new(dialect::SQLiteDialect {}),
58 }
59 }
60}
61
62impl fmt::Display for Dialect {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 write!(
66 f,
67 "{}",
68 format!("{self:?}")
69 .to_ascii_lowercase()
70 .split('-')
71 .collect::<String>()
72 )
73 }
74}
75
76#[derive(Debug, Clone)]
77pub struct SyntaxTree(pub(crate) Vec<Statement>);
78
79#[bon]
80impl SyntaxTree {
81 #[builder]
82 pub fn new<'a>(dialect: Option<Dialect>, sql: impl Into<&'a str>) -> Result<Self, ParseError> {
83 let dialect = dialect.unwrap_or_default().to_sqlparser_dialect();
84 let ast = Parser::parse_sql(dialect.as_ref(), sql.into())?;
85 Ok(Self(ast))
86 }
87
88 pub fn empty() -> Self {
89 Self(vec![])
90 }
91}
92
93pub use diff::DiffError;
94pub use migration::MigrateError;
95
96impl SyntaxTree {
97 pub fn diff(&self, other: &SyntaxTree) -> Result<Option<Self>, DiffError> {
98 Ok(Diff::diff(&self.0, &other.0)?.map(Self))
99 }
100
101 pub fn migrate(self, other: &SyntaxTree) -> Result<Option<Self>, MigrateError> {
102 Ok(Migrate::migrate(self.0, &other.0)?.map(Self))
103 }
104}
105
106impl fmt::Display for SyntaxTree {
107 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108 let mut iter = self.0.iter().peekable();
109 while let Some(s) = iter.next() {
110 let formatted = sqlformat::format(
111 format!("{s};").as_str(),
112 &sqlformat::QueryParams::None,
113 &sqlformat::FormatOptions::default(),
114 );
115 write!(f, "{formatted}")?;
116 if iter.peek().is_some() {
117 write!(f, "\n\n")?;
118 }
119 }
120 Ok(())
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[derive(Debug)]
129 struct TestCase {
130 dialect: Dialect,
131 sql_a: &'static str,
132 sql_b: &'static str,
133 expect: &'static str,
134 }
135
136 fn run_test_case<F>(tc: &TestCase, testfn: F)
137 where
138 F: Fn(SyntaxTree, SyntaxTree) -> SyntaxTree,
139 {
140 let ast_a = SyntaxTree::builder()
141 .dialect(tc.dialect)
142 .sql(tc.sql_a)
143 .build()
144 .unwrap();
145 let ast_b = SyntaxTree::builder()
146 .dialect(tc.dialect)
147 .sql(tc.sql_b)
148 .build()
149 .unwrap();
150 SyntaxTree::builder()
151 .dialect(tc.dialect)
152 .sql(tc.expect)
153 .build()
154 .unwrap_or_else(|_| panic!("invalid SQL: {:?}", tc.expect));
155 let actual = testfn(ast_a, ast_b);
156 assert_eq!(actual.to_string(), tc.expect, "{tc:?}");
157 }
158
159 fn run_test_cases<F, E: fmt::Debug>(test_cases: Vec<TestCase>, testfn: F)
160 where
161 F: Fn(SyntaxTree, SyntaxTree) -> Result<Option<SyntaxTree>, E>,
162 {
163 test_cases.into_iter().for_each(|tc| {
164 run_test_case(&tc, |ast_a, ast_b| {
165 testfn(ast_a, ast_b)
166 .inspect_err(|err| eprintln!("Error: {err:?}"))
167 .unwrap()
168 .unwrap()
169 })
170 });
171 }
172
173 #[test]
174 fn diff_create_table() {
175 run_test_cases(
176 vec![TestCase {
177 dialect: Dialect::Generic,
178 sql_a: "CREATE TABLE foo(\
179 id int PRIMARY KEY
180 )",
181 sql_b: "CREATE TABLE foo(\
182 id int PRIMARY KEY
183 );\
184 CREATE TABLE bar (id INT PRIMARY KEY);",
185 expect: "CREATE TABLE bar (id INT PRIMARY KEY);",
186 }],
187 |ast_a, ast_b| ast_a.diff(&ast_b),
188 );
189 }
190
191 #[test]
192 fn diff_drop_table() {
193 run_test_cases(
194 vec![TestCase {
195 dialect: Dialect::Generic,
196 sql_a: "CREATE TABLE foo(\
197 id int PRIMARY KEY
198 );\
199 CREATE TABLE bar (id INT PRIMARY KEY);",
200 sql_b: "CREATE TABLE foo(\
201 id int PRIMARY KEY
202 )",
203 expect: "DROP TABLE bar;",
204 }],
205 |ast_a, ast_b| ast_a.diff(&ast_b),
206 );
207 }
208
209 #[test]
210 fn diff_add_column() {
211 run_test_cases(
212 vec![TestCase {
213 dialect: Dialect::Generic,
214 sql_a: "CREATE TABLE foo(\
215 id int PRIMARY KEY
216 )",
217 sql_b: "CREATE TABLE foo(\
218 id int PRIMARY KEY,
219 bar text
220 )",
221 expect: "ALTER TABLE\n foo\nADD\n COLUMN bar TEXT;",
222 }],
223 |ast_a, ast_b| ast_a.diff(&ast_b),
224 );
225 }
226
227 #[test]
228 fn diff_drop_column() {
229 run_test_cases(
230 vec![TestCase {
231 dialect: Dialect::Generic,
232 sql_a: "CREATE TABLE foo(\
233 id int PRIMARY KEY,
234 bar text
235 )",
236 sql_b: "CREATE TABLE foo(\
237 id int PRIMARY KEY
238 )",
239 expect: "ALTER TABLE\n foo DROP COLUMN bar;",
240 }],
241 |ast_a, ast_b| ast_a.diff(&ast_b),
242 );
243 }
244
245 #[test]
246 fn diff_create_index() {
247 run_test_cases(
248 vec![
249 TestCase {
250 dialect: Dialect::Generic,
251 sql_a: "CREATE UNIQUE INDEX title_idx ON films (title);",
252 sql_b: "CREATE UNIQUE INDEX title_idx ON films ((lower(title)));",
253 expect: "DROP INDEX title_idx;\n\nCREATE UNIQUE INDEX title_idx ON films((lower(title)));",
254 },
255 TestCase {
256 dialect: Dialect::Generic,
257 sql_a: "CREATE UNIQUE INDEX IF NOT EXISTS title_idx ON films (title);",
258 sql_b: "CREATE UNIQUE INDEX IF NOT EXISTS title_idx ON films ((lower(title)));",
259 expect: "DROP INDEX IF EXISTS title_idx;\n\nCREATE UNIQUE INDEX IF NOT EXISTS title_idx ON films((lower(title)));",
260 },
261 ],
262 |ast_a, ast_b| ast_a.diff(&ast_b),
263 );
264 }
265
266 #[test]
267 fn diff_create_type() {
268 run_test_cases(
269 vec![
270 TestCase {
271 dialect: Dialect::Generic,
272 sql_a: "CREATE TYPE bug_status AS ENUM ('new', 'open');",
273 sql_b: "CREATE TYPE foo AS ENUM ('bar');",
274 expect: "DROP TYPE bug_status;\n\nCREATE TYPE foo AS ENUM ('bar');",
275 },
276 TestCase {
277 dialect: Dialect::Generic,
278 sql_a: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'closed');",
279 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'assigned', 'closed');",
280 expect: "ALTER TYPE bug_status\nADD\n VALUE 'assigned'\nAFTER\n 'open';",
281 },
282 TestCase {
283 dialect: Dialect::Generic,
284 sql_a: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
285 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'closed');",
286 expect: "ALTER TYPE bug_status\nADD\n VALUE 'new' BEFORE 'open';",
287 },
288 TestCase {
289 dialect: Dialect::Generic,
290 sql_a: "CREATE TYPE bug_status AS ENUM ('new', 'open');",
291 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'closed');",
292 expect: "ALTER TYPE bug_status\nADD\n VALUE 'closed';",
293 },
294 TestCase {
295 dialect: Dialect::Generic,
296 sql_a: "CREATE TYPE bug_status AS ENUM ('new', 'open');",
297 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'assigned', 'closed');",
298 expect: "ALTER TYPE bug_status\nADD\n VALUE 'assigned';\n\nALTER TYPE bug_status\nADD\n VALUE 'closed';",
299 },
300 TestCase {
301 dialect: Dialect::Generic,
302 sql_a: "CREATE TYPE bug_status AS ENUM ('open', 'critical');",
303 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'assigned', 'closed', 'critical');",
304 expect: "ALTER TYPE bug_status\nADD\n VALUE 'new' BEFORE 'open';\n\nALTER TYPE bug_status\nADD\n VALUE 'assigned'\nAFTER\n 'open';\n\nALTER TYPE bug_status\nADD\n VALUE 'closed'\nAFTER\n 'assigned';",
305 },
306 TestCase {
307 dialect: Dialect::Generic,
308 sql_a: "CREATE TYPE bug_status AS ENUM ('open');",
309 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'closed');",
310 expect: "ALTER TYPE bug_status\nADD\n VALUE 'new' BEFORE 'open';\n\nALTER TYPE bug_status\nADD\n VALUE 'closed';",
311 },
312 ],
313 |ast_a, ast_b| ast_a.diff(&ast_b),
314 );
315 }
316
317 #[test]
318 fn diff_create_extension() {
319 run_test_cases(
320 vec![TestCase {
321 dialect: Dialect::Generic,
322 sql_a: "CREATE EXTENSION hstore;",
323 sql_b: "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";",
324 expect: "DROP EXTENSION hstore;\n\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";",
325 }],
326 |ast_a, ast_b| ast_a.diff(&ast_b),
327 );
328 }
329
330 #[test]
331 fn diff_create_domain() {
332 run_test_cases(
333 vec![TestCase {
334 dialect: Dialect::PostgreSql,
335 sql_a: "",
336 sql_b: "CREATE DOMAIN email AS VARCHAR(255) CHECK (VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$');",
337 expect: "CREATE DOMAIN email AS VARCHAR(255) CHECK (\n VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n);",
338 }],
339 |ast_a, ast_b| ast_a.diff(&ast_b),
340 );
341 }
342
343 #[test]
344 fn diff_edit_domain() {
345 run_test_cases(
346 vec![TestCase {
347 dialect: Dialect::PostgreSql,
348 sql_a: "CREATE DOMAIN positive_int AS INTEGER CHECK (VALUE > 0);",
349 sql_b: "CREATE DOMAIN positive_int AS BIGINT CHECK (VALUE > 0 AND VALUE < 1000000);",
350 expect: "DROP DOMAIN IF EXISTS positive_int;\n\nCREATE DOMAIN positive_int AS BIGINT CHECK (\n VALUE > 0\n AND VALUE < 1000000\n);",
351 }],
352 |ast_a, ast_b| ast_a.diff(&ast_b),
353 );
354 }
355
356 #[test]
357 fn apply_create_table() {
358 run_test_cases(
359 vec![TestCase {
360 dialect: Dialect::Generic,
361 sql_a: "CREATE TABLE bar (id INT PRIMARY KEY);",
362 sql_b: "CREATE TABLE foo (id INT PRIMARY KEY);",
363 expect: "CREATE TABLE bar (id INT PRIMARY KEY);\n\nCREATE TABLE foo (id INT PRIMARY KEY);",
364 }],
365 |ast_a, ast_b| ast_a.migrate(&ast_b),
366 );
367 }
368
369 #[test]
370 fn apply_drop_table() {
371 run_test_cases(
372 vec![TestCase {
373 dialect: Dialect::Generic,
374 sql_a: "CREATE TABLE bar (id INT PRIMARY KEY)",
375 sql_b: "DROP TABLE bar; CREATE TABLE foo (id INT PRIMARY KEY)",
376 expect: "CREATE TABLE foo (id INT PRIMARY KEY);",
377 }],
378 |ast_a, ast_b| ast_a.migrate(&ast_b),
379 );
380 }
381
382 #[test]
383 fn apply_alter_table_add_column() {
384 run_test_cases(
385 vec![TestCase {
386 dialect: Dialect::Generic,
387 sql_a: "CREATE TABLE bar (id INT PRIMARY KEY)",
388 sql_b: "ALTER TABLE bar ADD COLUMN bar TEXT",
389 expect: "CREATE TABLE bar (id INT PRIMARY KEY, bar TEXT);",
390 }],
391 |ast_a, ast_b| ast_a.migrate(&ast_b),
392 );
393 }
394
395 #[test]
396 fn apply_alter_table_drop_column() {
397 run_test_cases(
398 vec![TestCase {
399 dialect: Dialect::Generic,
400 sql_a: "CREATE TABLE bar (bar TEXT, id INT PRIMARY KEY)",
401 sql_b: "ALTER TABLE bar DROP COLUMN bar",
402 expect: "CREATE TABLE bar (id INT PRIMARY KEY);",
403 }],
404 |ast_a, ast_b| ast_a.migrate(&ast_b),
405 );
406 }
407
408 #[test]
409 fn apply_alter_table_alter_column() {
410 run_test_cases(
411 vec![
412 TestCase {
413 dialect: Dialect::Generic,
414 sql_a: "CREATE TABLE bar (bar TEXT, id INT PRIMARY KEY)",
415 sql_b: "ALTER TABLE bar ALTER COLUMN bar SET NOT NULL",
416 expect: "CREATE TABLE bar (bar TEXT NOT NULL, id INT PRIMARY KEY);",
417 },
418 TestCase {
419 dialect: Dialect::Generic,
420 sql_a: "CREATE TABLE bar (bar TEXT NOT NULL, id INT PRIMARY KEY)",
421 sql_b: "ALTER TABLE bar ALTER COLUMN bar DROP NOT NULL",
422 expect: "CREATE TABLE bar (bar TEXT, id INT PRIMARY KEY);",
423 },
424 TestCase {
425 dialect: Dialect::Generic,
426 sql_a: "CREATE TABLE bar (bar TEXT NOT NULL DEFAULT 'foo', id INT PRIMARY KEY)",
427 sql_b: "ALTER TABLE bar ALTER COLUMN bar DROP DEFAULT",
428 expect: "CREATE TABLE bar (bar TEXT NOT NULL, id INT PRIMARY KEY);",
429 },
430 TestCase {
431 dialect: Dialect::Generic,
432 sql_a: "CREATE TABLE bar (bar TEXT, id INT PRIMARY KEY)",
433 sql_b: "ALTER TABLE bar ALTER COLUMN bar SET DATA TYPE INTEGER",
434 expect: "CREATE TABLE bar (bar INTEGER, id INT PRIMARY KEY);",
435 },
436 TestCase {
437 dialect: Dialect::PostgreSql,
438 sql_a: "CREATE TABLE bar (bar TEXT, id INT PRIMARY KEY)",
439 sql_b: "ALTER TABLE bar ALTER COLUMN bar SET DATA TYPE timestamp with time zone\n USING timestamp with time zone 'epoch' + foo_timestamp * interval '1 second'",
440 expect: "CREATE TABLE bar (bar TIMESTAMP WITH TIME ZONE, id INT PRIMARY KEY);",
441 },
442 TestCase {
443 dialect: Dialect::Generic,
444 sql_a: "CREATE TABLE bar (bar INTEGER, id INT PRIMARY KEY)",
445 sql_b: "ALTER TABLE bar ALTER COLUMN bar ADD GENERATED BY DEFAULT AS IDENTITY",
446 expect: "CREATE TABLE bar (\n bar INTEGER GENERATED BY DEFAULT AS IDENTITY,\n id INT PRIMARY KEY\n);",
447 },
448 TestCase {
449 dialect: Dialect::Generic,
450 sql_a: "CREATE TABLE bar (bar INTEGER, id INT PRIMARY KEY)",
451 sql_b: "ALTER TABLE bar ALTER COLUMN bar ADD GENERATED ALWAYS AS IDENTITY (START WITH 10)",
452 expect: "CREATE TABLE bar (\n bar INTEGER GENERATED ALWAYS AS IDENTITY (START WITH 10),\n id INT PRIMARY KEY\n);",
453 },
454 ],
455 |ast_a, ast_b| ast_a.migrate(&ast_b),
456 );
457 }
458
459 #[test]
460 fn apply_create_index() {
461 run_test_cases(
462 vec![
463 TestCase {
464 dialect: Dialect::Generic,
465 sql_a: "CREATE UNIQUE INDEX title_idx ON films (title);",
466 sql_b: "CREATE INDEX code_idx ON films (code);",
467 expect: "CREATE UNIQUE INDEX title_idx ON films(title);\n\nCREATE INDEX code_idx ON films(code);",
468 },
469 TestCase {
470 dialect: Dialect::Generic,
471 sql_a: "CREATE UNIQUE INDEX title_idx ON films (title);",
472 sql_b: "DROP INDEX title_idx;",
473 expect: "",
474 },
475 TestCase {
476 dialect: Dialect::Generic,
477 sql_a: "CREATE UNIQUE INDEX title_idx ON films (title);",
478 sql_b: "DROP INDEX title_idx;CREATE INDEX code_idx ON films (code);",
479 expect: "CREATE INDEX code_idx ON films(code);",
480 },
481 ],
482 |ast_a, ast_b| ast_a.migrate(&ast_b),
483 );
484 }
485
486 #[test]
487 fn apply_alter_create_type() {
488 run_test_cases(
489 vec![TestCase {
490 dialect: Dialect::Generic,
491 sql_a: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
492 sql_b: "CREATE TYPE compfoo AS (f1 int, f2 text);",
493 expect: "CREATE TYPE bug_status AS ENUM ('open', 'closed');\n\nCREATE TYPE compfoo AS (f1 INT, f2 TEXT);",
494 }],
495 |ast_a, ast_b| ast_a.migrate(&ast_b),
496 );
497 }
498
499 #[test]
500 fn apply_alter_type_rename() {
501 run_test_cases(
502 vec![TestCase {
503 dialect: Dialect::Generic,
504 sql_a: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
505 sql_b: "ALTER TYPE bug_status RENAME TO issue_status",
506 expect: "CREATE TYPE issue_status AS ENUM ('open', 'closed');",
507 }],
508 |ast_a, ast_b| ast_a.migrate(&ast_b),
509 );
510 }
511
512 #[test]
513 fn apply_alter_type_add_value() {
514 run_test_cases(
515 vec![
516 TestCase {
517 dialect: Dialect::Generic,
518 sql_a: "CREATE TYPE bug_status AS ENUM ('open');",
519 sql_b: "ALTER TYPE bug_status ADD VALUE 'new' BEFORE 'open';",
520 expect: "CREATE TYPE bug_status AS ENUM ('new', 'open');",
521 },
522 TestCase {
523 dialect: Dialect::Generic,
524 sql_a: "CREATE TYPE bug_status AS ENUM ('open');",
525 sql_b: "ALTER TYPE bug_status ADD VALUE 'closed' AFTER 'open';",
526 expect: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
527 },
528 TestCase {
529 dialect: Dialect::Generic,
530 sql_a: "CREATE TYPE bug_status AS ENUM ('open');",
531 sql_b: "ALTER TYPE bug_status ADD VALUE 'closed';",
532 expect: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
533 },
534 ],
535 |ast_a, ast_b| ast_a.migrate(&ast_b),
536 );
537 }
538
539 #[test]
540 fn apply_alter_type_rename_value() {
541 run_test_cases(
542 vec![TestCase {
543 dialect: Dialect::Generic,
544 sql_a: "CREATE TYPE bug_status AS ENUM ('new', 'closed');",
545 sql_b: "ALTER TYPE bug_status RENAME VALUE 'new' TO 'open';",
546 expect: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
547 }],
548 |ast_a, ast_b| ast_a.migrate(&ast_b),
549 );
550 }
551
552 #[test]
553 fn apply_create_extension() {
554 run_test_cases(
555 vec![TestCase {
556 dialect: Dialect::Generic,
557 sql_a: "CREATE EXTENSION hstore;",
558 sql_b: "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";",
559 expect: "CREATE EXTENSION hstore;\n\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";",
560 }],
561 |ast_a, ast_b| ast_a.migrate(&ast_b),
562 );
563 }
564
565 #[test]
566 fn apply_create_domain() {
567 run_test_cases(
568 vec![TestCase {
569 dialect: Dialect::PostgreSql,
570 sql_a: "CREATE DOMAIN positive_int AS INTEGER CHECK (VALUE > 0);",
571 sql_b: "CREATE DOMAIN email AS VARCHAR(255) CHECK (VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$');",
572 expect: "CREATE DOMAIN positive_int AS INTEGER CHECK (VALUE > 0);\n\nCREATE DOMAIN email AS VARCHAR(255) CHECK (\n VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n);",
573 }],
574 |ast_a, ast_b| ast_a.migrate(&ast_b),
575 );
576 }
577}