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