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![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}