Skip to main content

sql_schema/
lib.rs

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        // NOTE: this must match how clap::ValueEnum displays variants
65        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}