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![
177 TestCase {
178 dialect: Dialect::Generic,
179 sql_a: "CREATE TABLE foo(\
180 id int PRIMARY KEY
181 )",
182 sql_b: "CREATE TABLE foo(\
183 id int PRIMARY KEY
184 );\
185 CREATE TABLE bar (id INT PRIMARY KEY);",
186 expect: "CREATE TABLE bar (id INT PRIMARY KEY);",
187 },
188 TestCase {
189 dialect: Dialect::Generic,
190 sql_a: "CREATE TABLE foo(\
191 id int PRIMARY KEY
192 )",
193 sql_b: "CREATE TABLE foo(\
194 \"id\" int PRIMARY KEY
195 );\
196 CREATE TABLE bar (id INT PRIMARY KEY);",
197 expect: "CREATE TABLE bar (id INT PRIMARY KEY);",
198 },
199 TestCase {
200 dialect: Dialect::Generic,
201 sql_a: "CREATE TABLE foo(\
202 \"id\" int PRIMARY KEY
203 )",
204 sql_b: "CREATE TABLE foo(\
205 id int PRIMARY KEY
206 );\
207 CREATE TABLE bar (id INT PRIMARY KEY);",
208 expect: "CREATE TABLE bar (id INT PRIMARY KEY);",
209 },
210 ],
211 |ast_a, ast_b| ast_a.diff(&ast_b),
212 );
213 }
214
215 #[test]
216 fn diff_drop_table() {
217 run_test_cases(
218 vec![TestCase {
219 dialect: Dialect::Generic,
220 sql_a: "CREATE TABLE foo(\
221 id int PRIMARY KEY
222 );\
223 CREATE TABLE bar (id INT PRIMARY KEY);",
224 sql_b: "CREATE TABLE foo(\
225 id int PRIMARY KEY
226 )",
227 expect: "DROP TABLE bar;",
228 }],
229 |ast_a, ast_b| ast_a.diff(&ast_b),
230 );
231 }
232
233 #[test]
234 fn diff_add_column() {
235 run_test_cases(
236 vec![TestCase {
237 dialect: Dialect::Generic,
238 sql_a: "CREATE TABLE foo(\
239 id int PRIMARY KEY
240 )",
241 sql_b: "CREATE TABLE foo(\
242 id int PRIMARY KEY,
243 bar text
244 )",
245 expect: "ALTER TABLE\n foo\nADD\n COLUMN bar TEXT;",
246 }],
247 |ast_a, ast_b| ast_a.diff(&ast_b),
248 );
249 }
250
251 #[test]
252 fn diff_drop_column() {
253 run_test_cases(
254 vec![TestCase {
255 dialect: Dialect::Generic,
256 sql_a: "CREATE TABLE foo(\
257 id int PRIMARY KEY,
258 bar text
259 )",
260 sql_b: "CREATE TABLE foo(\
261 id int PRIMARY KEY
262 )",
263 expect: "ALTER TABLE\n foo DROP COLUMN bar;",
264 }],
265 |ast_a, ast_b| ast_a.diff(&ast_b),
266 );
267 }
268
269 #[test]
270 fn diff_create_index() {
271 run_test_cases(
272 vec![
273 TestCase {
274 dialect: Dialect::Generic,
275 sql_a: "CREATE UNIQUE INDEX title_idx ON films (title);",
276 sql_b: "CREATE UNIQUE INDEX title_idx ON films ((lower(title)));",
277 expect: "DROP INDEX title_idx;\n\nCREATE UNIQUE INDEX title_idx ON films((lower(title)));",
278 },
279 TestCase {
280 dialect: Dialect::Generic,
281 sql_a: "CREATE UNIQUE INDEX IF NOT EXISTS title_idx ON films (title);",
282 sql_b: "CREATE UNIQUE INDEX IF NOT EXISTS title_idx ON films ((lower(title)));",
283 expect: "DROP INDEX IF EXISTS title_idx;\n\nCREATE UNIQUE INDEX IF NOT EXISTS title_idx ON films((lower(title)));",
284 },
285 ],
286 |ast_a, ast_b| ast_a.diff(&ast_b),
287 );
288 }
289
290 #[test]
291 fn diff_create_type() {
292 run_test_cases(
293 vec![
294 TestCase {
295 dialect: Dialect::Generic,
296 sql_a: "CREATE TYPE bug_status AS ENUM ('new', 'open');",
297 sql_b: "CREATE TYPE foo AS ENUM ('bar');",
298 expect: "DROP TYPE bug_status;\n\nCREATE TYPE foo AS ENUM ('bar');",
299 },
300 TestCase {
301 dialect: Dialect::Generic,
302 sql_a: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'closed');",
303 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'assigned', 'closed');",
304 expect: "ALTER TYPE bug_status\nADD\n VALUE 'assigned'\nAFTER\n 'open';",
305 },
306 TestCase {
307 dialect: Dialect::Generic,
308 sql_a: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
309 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'closed');",
310 expect: "ALTER TYPE bug_status\nADD\n VALUE 'new' BEFORE 'open';",
311 },
312 TestCase {
313 dialect: Dialect::Generic,
314 sql_a: "CREATE TYPE bug_status AS ENUM ('new', 'open');",
315 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'closed');",
316 expect: "ALTER TYPE bug_status\nADD\n VALUE 'closed';",
317 },
318 TestCase {
319 dialect: Dialect::Generic,
320 sql_a: "CREATE TYPE bug_status AS ENUM ('new', 'open');",
321 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'assigned', 'closed');",
322 expect: "ALTER TYPE bug_status\nADD\n VALUE 'assigned';\n\nALTER TYPE bug_status\nADD\n VALUE 'closed';",
323 },
324 TestCase {
325 dialect: Dialect::Generic,
326 sql_a: "CREATE TYPE bug_status AS ENUM ('open', 'critical');",
327 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'assigned', 'closed', 'critical');",
328 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';",
329 },
330 TestCase {
331 dialect: Dialect::Generic,
332 sql_a: "CREATE TYPE bug_status AS ENUM ('open');",
333 sql_b: "CREATE TYPE bug_status AS ENUM ('new', 'open', 'closed');",
334 expect: "ALTER TYPE bug_status\nADD\n VALUE 'new' BEFORE 'open';\n\nALTER TYPE bug_status\nADD\n VALUE 'closed';",
335 },
336 ],
337 |ast_a, ast_b| ast_a.diff(&ast_b),
338 );
339 }
340
341 #[test]
342 fn diff_create_extension() {
343 run_test_cases(
344 vec![TestCase {
345 dialect: Dialect::Generic,
346 sql_a: "CREATE EXTENSION hstore;",
347 sql_b: "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";",
348 expect: "DROP EXTENSION hstore;\n\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";",
349 }],
350 |ast_a, ast_b| ast_a.diff(&ast_b),
351 );
352 }
353
354 #[test]
355 fn diff_create_domain() {
356 run_test_cases(
357 vec![TestCase {
358 dialect: Dialect::PostgreSql,
359 sql_a: "",
360 sql_b: "CREATE DOMAIN email AS VARCHAR(255) CHECK (VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$');",
361 expect: "CREATE DOMAIN email AS VARCHAR(255) CHECK (\n VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n);",
362 }],
363 |ast_a, ast_b| ast_a.diff(&ast_b),
364 );
365 }
366
367 #[test]
368 fn diff_edit_domain() {
369 run_test_cases(
370 vec![TestCase {
371 dialect: Dialect::PostgreSql,
372 sql_a: "CREATE DOMAIN positive_int AS INTEGER CHECK (VALUE > 0);",
373 sql_b: "CREATE DOMAIN positive_int AS BIGINT CHECK (VALUE > 0 AND VALUE < 1000000);",
374 expect: "DROP DOMAIN IF EXISTS positive_int;\n\nCREATE DOMAIN positive_int AS BIGINT CHECK (\n VALUE > 0\n AND VALUE < 1000000\n);",
375 }],
376 |ast_a, ast_b| ast_a.diff(&ast_b),
377 );
378 }
379
380 #[test]
381 fn apply_create_table() {
382 run_test_cases(
383 vec![TestCase {
384 dialect: Dialect::Generic,
385 sql_a: "CREATE TABLE bar (id INT PRIMARY KEY);",
386 sql_b: "CREATE TABLE foo (id INT PRIMARY KEY);",
387 expect: "CREATE TABLE bar (id INT PRIMARY KEY);\n\nCREATE TABLE foo (id INT PRIMARY KEY);",
388 }],
389 |ast_a, ast_b| ast_a.migrate(&ast_b),
390 );
391 }
392
393 #[test]
394 fn apply_drop_table() {
395 run_test_cases(
396 vec![TestCase {
397 dialect: Dialect::Generic,
398 sql_a: "CREATE TABLE bar (id INT PRIMARY KEY)",
399 sql_b: "DROP TABLE bar; CREATE TABLE foo (id INT PRIMARY KEY)",
400 expect: "CREATE TABLE foo (id INT PRIMARY KEY);",
401 }],
402 |ast_a, ast_b| ast_a.migrate(&ast_b),
403 );
404 }
405
406 #[test]
407 fn apply_alter_table_add_column() {
408 run_test_cases(
409 vec![TestCase {
410 dialect: Dialect::Generic,
411 sql_a: "CREATE TABLE bar (id INT PRIMARY KEY)",
412 sql_b: "ALTER TABLE bar ADD COLUMN bar TEXT",
413 expect: "CREATE TABLE bar (id INT PRIMARY KEY, bar TEXT);",
414 }],
415 |ast_a, ast_b| ast_a.migrate(&ast_b),
416 );
417 }
418
419 #[test]
420 fn apply_alter_table_drop_column() {
421 run_test_cases(
422 vec![TestCase {
423 dialect: Dialect::Generic,
424 sql_a: "CREATE TABLE bar (bar TEXT, id INT PRIMARY KEY)",
425 sql_b: "ALTER TABLE bar DROP COLUMN bar",
426 expect: "CREATE TABLE bar (id INT PRIMARY KEY);",
427 }],
428 |ast_a, ast_b| ast_a.migrate(&ast_b),
429 );
430 }
431
432 #[test]
433 fn apply_alter_table_drop_columns_snowflake() {
434 run_test_cases(
435 vec![TestCase {
436 dialect: Dialect::Snowflake,
437 sql_a: "CREATE TABLE bar (foo TEXT, bar TEXT, id INT PRIMARY KEY)",
438 sql_b: "ALTER TABLE bar DROP COLUMN foo, bar",
439 expect: "CREATE TABLE bar (id INT PRIMARY KEY);",
440 }],
441 |ast_a, ast_b| ast_a.migrate(&ast_b),
442 );
443 }
444
445 #[test]
446 fn apply_alter_table_alter_column() {
447 run_test_cases(
448 vec![
449 TestCase {
450 dialect: Dialect::Generic,
451 sql_a: "CREATE TABLE bar (bar TEXT, id INT PRIMARY KEY)",
452 sql_b: "ALTER TABLE bar ALTER COLUMN bar SET NOT NULL",
453 expect: "CREATE TABLE bar (bar TEXT NOT NULL, id INT PRIMARY KEY);",
454 },
455 TestCase {
456 dialect: Dialect::Generic,
457 sql_a: "CREATE TABLE bar (bar TEXT NOT NULL, id INT PRIMARY KEY)",
458 sql_b: "ALTER TABLE bar ALTER COLUMN bar DROP NOT NULL",
459 expect: "CREATE TABLE bar (bar TEXT, id INT PRIMARY KEY);",
460 },
461 TestCase {
462 dialect: Dialect::Generic,
463 sql_a: "CREATE TABLE bar (bar TEXT NOT NULL DEFAULT 'foo', id INT PRIMARY KEY)",
464 sql_b: "ALTER TABLE bar ALTER COLUMN bar DROP DEFAULT",
465 expect: "CREATE TABLE bar (bar TEXT NOT NULL, id INT PRIMARY KEY);",
466 },
467 TestCase {
468 dialect: Dialect::Generic,
469 sql_a: "CREATE TABLE bar (bar TEXT, id INT PRIMARY KEY)",
470 sql_b: "ALTER TABLE bar ALTER COLUMN bar SET DATA TYPE INTEGER",
471 expect: "CREATE TABLE bar (bar INTEGER, id INT PRIMARY KEY);",
472 },
473 TestCase {
474 dialect: Dialect::PostgreSql,
475 sql_a: "CREATE TABLE bar (bar TEXT, id INT PRIMARY KEY)",
476 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'",
477 expect: "CREATE TABLE bar (bar TIMESTAMP WITH TIME ZONE, id INT PRIMARY KEY);",
478 },
479 TestCase {
480 dialect: Dialect::Generic,
481 sql_a: "CREATE TABLE bar (bar INTEGER, id INT PRIMARY KEY)",
482 sql_b: "ALTER TABLE bar ALTER COLUMN bar ADD GENERATED BY DEFAULT AS IDENTITY",
483 expect: "CREATE TABLE bar (\n bar INTEGER GENERATED BY DEFAULT AS IDENTITY,\n id INT PRIMARY KEY\n);",
484 },
485 TestCase {
486 dialect: Dialect::Generic,
487 sql_a: "CREATE TABLE bar (bar INTEGER, id INT PRIMARY KEY)",
488 sql_b: "ALTER TABLE bar ALTER COLUMN bar ADD GENERATED ALWAYS AS IDENTITY (START WITH 10)",
489 expect: "CREATE TABLE bar (\n bar INTEGER GENERATED ALWAYS AS IDENTITY (START WITH 10),\n id INT PRIMARY KEY\n);",
490 },
491 ],
492 |ast_a, ast_b| ast_a.migrate(&ast_b),
493 );
494 }
495
496 #[test]
497 fn apply_create_index() {
498 run_test_cases(
499 vec![
500 TestCase {
501 dialect: Dialect::Generic,
502 sql_a: "CREATE UNIQUE INDEX title_idx ON films (title);",
503 sql_b: "CREATE INDEX code_idx ON films (code);",
504 expect: "CREATE UNIQUE INDEX title_idx ON films(title);\n\nCREATE INDEX code_idx ON films(code);",
505 },
506 TestCase {
507 dialect: Dialect::Generic,
508 sql_a: "CREATE UNIQUE INDEX title_idx ON films (title);",
509 sql_b: "DROP INDEX title_idx;",
510 expect: "",
511 },
512 TestCase {
513 dialect: Dialect::Generic,
514 sql_a: "CREATE UNIQUE INDEX title_idx ON films (title);",
515 sql_b: "DROP INDEX title_idx;CREATE INDEX code_idx ON films (code);",
516 expect: "CREATE INDEX code_idx ON films(code);",
517 },
518 ],
519 |ast_a, ast_b| ast_a.migrate(&ast_b),
520 );
521 }
522
523 #[test]
524 fn apply_alter_create_type() {
525 run_test_cases(
526 vec![TestCase {
527 dialect: Dialect::Generic,
528 sql_a: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
529 sql_b: "CREATE TYPE compfoo AS (f1 int, f2 text);",
530 expect: "CREATE TYPE bug_status AS ENUM ('open', 'closed');\n\nCREATE TYPE compfoo AS (f1 INT, f2 TEXT);",
531 }],
532 |ast_a, ast_b| ast_a.migrate(&ast_b),
533 );
534 }
535
536 #[test]
537 fn apply_alter_type_rename() {
538 run_test_cases(
539 vec![TestCase {
540 dialect: Dialect::Generic,
541 sql_a: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
542 sql_b: "ALTER TYPE bug_status RENAME TO issue_status",
543 expect: "CREATE TYPE issue_status AS ENUM ('open', 'closed');",
544 }],
545 |ast_a, ast_b| ast_a.migrate(&ast_b),
546 );
547 }
548
549 #[test]
550 fn apply_alter_type_add_value() {
551 run_test_cases(
552 vec![
553 TestCase {
554 dialect: Dialect::Generic,
555 sql_a: "CREATE TYPE bug_status AS ENUM ('open');",
556 sql_b: "ALTER TYPE bug_status ADD VALUE 'new' BEFORE 'open';",
557 expect: "CREATE TYPE bug_status AS ENUM ('new', 'open');",
558 },
559 TestCase {
560 dialect: Dialect::Generic,
561 sql_a: "CREATE TYPE bug_status AS ENUM ('open');",
562 sql_b: "ALTER TYPE bug_status ADD VALUE 'closed' AFTER 'open';",
563 expect: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
564 },
565 TestCase {
566 dialect: Dialect::Generic,
567 sql_a: "CREATE TYPE bug_status AS ENUM ('open');",
568 sql_b: "ALTER TYPE bug_status ADD VALUE 'closed';",
569 expect: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
570 },
571 ],
572 |ast_a, ast_b| ast_a.migrate(&ast_b),
573 );
574 }
575
576 #[test]
577 fn apply_alter_type_rename_value() {
578 run_test_cases(
579 vec![TestCase {
580 dialect: Dialect::Generic,
581 sql_a: "CREATE TYPE bug_status AS ENUM ('new', 'closed');",
582 sql_b: "ALTER TYPE bug_status RENAME VALUE 'new' TO 'open';",
583 expect: "CREATE TYPE bug_status AS ENUM ('open', 'closed');",
584 }],
585 |ast_a, ast_b| ast_a.migrate(&ast_b),
586 );
587 }
588
589 #[test]
590 fn apply_create_extension() {
591 run_test_cases(
592 vec![TestCase {
593 dialect: Dialect::Generic,
594 sql_a: "CREATE EXTENSION hstore;",
595 sql_b: "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";",
596 expect: "CREATE EXTENSION hstore;\n\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";",
597 }],
598 |ast_a, ast_b| ast_a.migrate(&ast_b),
599 );
600 }
601
602 #[test]
603 fn apply_create_domain() {
604 run_test_cases(
605 vec![TestCase {
606 dialect: Dialect::PostgreSql,
607 sql_a: "CREATE DOMAIN positive_int AS INTEGER CHECK (VALUE > 0);",
608 sql_b: "CREATE DOMAIN email AS VARCHAR(255) CHECK (VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$');",
609 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);",
610 }],
611 |ast_a, ast_b| ast_a.migrate(&ast_b),
612 );
613 }
614}