Skip to main content

rustlavel_db/
schema.rs

1//! The schema builder — what a migration writes.
2//!
3//! ```ignore
4//! schema.create("users", |t| {
5//!     t.id();
6//!     t.string("name");
7//!     t.string("email").unique();
8//!     t.timestamps();
9//! });
10//! ```
11//!
12//! Every identifier is validated, so a migration cannot smuggle statements in
13//! through a column name.
14
15use crate::dialect::{ColumnType, Dialect, quote_qualified, validate_identifier};
16use crate::Database;
17use rustlavel_core::Result;
18
19/// A column's default value.
20///
21/// `Now` and `Uuid` stay symbolic until the statement is rendered, because only
22/// then is it known whether the expression is `now()`, `current_timestamp(6)`
23/// or `sysutcdatetime()`.
24#[derive(Debug, Clone)]
25enum Default {
26    /// Already rendered, including any quoting.
27    Literal(String),
28    Now,
29    Uuid,
30}
31
32/// A column being defined.
33#[derive(Debug, Clone)]
34pub struct Column {
35    name: String,
36    kind: ColumnType,
37    nullable: bool,
38    unique: bool,
39    primary: bool,
40    default: Option<Default>,
41    /// `(table, column)` for a foreign key.
42    references: Option<(String, String)>,
43    on_delete: Option<&'static str>,
44    index: bool,
45}
46
47impl Column {
48    fn new(name: &str, kind: ColumnType) -> Self {
49        Column {
50            name: name.to_string(),
51            kind,
52            nullable: false,
53            unique: false,
54            primary: false,
55            default: None,
56            references: None,
57            on_delete: None,
58            index: false,
59        }
60    }
61
62    /// Allow NULL. Columns are NOT NULL by default, which is the safer default
63    /// and the opposite of what SQL gives you.
64    pub fn nullable(&mut self) -> &mut Self {
65        self.nullable = true;
66        self
67    }
68
69    pub fn unique(&mut self) -> &mut Self {
70        self.unique = true;
71        self
72    }
73
74    pub fn primary(&mut self) -> &mut Self {
75        self.primary = true;
76        self
77    }
78
79    pub fn index(&mut self) -> &mut Self {
80        self.index = true;
81        self
82    }
83
84    /// A literal default. Strings are quoted; use [`Column::default_raw`] for
85    /// an expression such as `now()`.
86    pub fn default(&mut self, value: &str) -> &mut Self {
87        self.default = Some(Default::Literal(format!("'{}'", value.replace('\'', "''"))));
88        self
89    }
90
91    pub fn default_int(&mut self, value: i64) -> &mut Self {
92        self.default = Some(Default::Literal(value.to_string()));
93        self
94    }
95
96    /// MySQL and SQL Server store a boolean as a number, so `true` is written
97    /// as `1` where `true` would not parse.
98    pub fn default_bool(&mut self, value: bool) -> &mut Self {
99        self.default = Some(Default::Literal(if value { "TRUE_LITERAL" } else { "FALSE_LITERAL" }.into()));
100        self
101    }
102
103    /// Default to the current time, in whatever the database calls it.
104    pub fn default_now(&mut self) -> &mut Self {
105        self.default = Some(Default::Now);
106        self
107    }
108
109    /// A default expression, written verbatim. Only migration authors reach
110    /// this, never user input.
111    pub fn default_raw(&mut self, expression: &str) -> &mut Self {
112        self.default = Some(Default::Literal(expression.to_string()));
113        self
114    }
115
116    /// A foreign key to another table's column.
117    pub fn references(&mut self, table: &str, column: &str) -> &mut Self {
118        self.references = Some((table.to_string(), column.to_string()));
119        self
120    }
121
122    /// `on delete cascade` — only meaningful with [`Column::references`].
123    pub fn cascade_on_delete(&mut self) -> &mut Self {
124        self.on_delete = Some("cascade");
125        self
126    }
127
128    pub fn null_on_delete(&mut self) -> &mut Self {
129        self.on_delete = Some("set null");
130        self
131    }
132
133    fn to_sql(&self, dialect: &dyn Dialect) -> Result<String> {
134        validate_identifier(&self.name, dialect.max_identifier_length())?;
135        let mut sql = format!("{} {}", dialect.quote(&self.name), dialect.column_type(&self.kind));
136
137        if self.primary {
138            sql.push_str(" primary key");
139        } else if !self.nullable {
140            sql.push_str(" not null");
141        }
142
143        if self.unique && !self.primary {
144            sql.push_str(" unique");
145        }
146
147        if let Some(default) = &self.default {
148            let rendered = match default {
149                Default::Now => dialect.now().to_string(),
150                Default::Uuid => match dialect.uuid_default() {
151                    Some(expression) => expression.to_string(),
152                    // MySQL has no portable one, so the application supplies
153                    // the value rather than the schema pretending otherwise.
154                    None => return Err(rustlavel_core::Error::msg(format!(
155                        "`{}` cannot default a uuid column: {} has no expression for it. \
156                         Generate the id in the application instead.",
157                        self.name,
158                        dialect.name()
159                    ))),
160                },
161                Default::Literal(literal) if literal == "TRUE_LITERAL" => {
162                    if dialect.booleans_are_integers() { "1".into() } else { "true".into() }
163                }
164                Default::Literal(literal) if literal == "FALSE_LITERAL" => {
165                    if dialect.booleans_are_integers() { "0".into() } else { "false".into() }
166                }
167                Default::Literal(literal) => literal.clone(),
168            };
169            sql.push_str(&format!(" default {rendered}"));
170        }
171
172        // No inline `references` here: MySQL parses one and silently creates
173        // no constraint at all, so foreign keys are emitted as table-level
174        // constraints by `foreign_key_sql` instead.
175
176        Ok(sql)
177    }
178
179    /// The table-level constraint for this column's foreign key, if it has one.
180    fn foreign_key_sql(&self, dialect: &dyn Dialect, table: &str) -> Result<Option<String>> {
181        let Some((target, target_column)) = &self.references else { return Ok(None) };
182
183        let limit = dialect.max_identifier_length();
184        validate_identifier(target_column, limit)?;
185
186        // Laravel's naming, so a constraint can be found by the column it is on.
187        let name = format!("{table}_{}_foreign", self.name);
188        validate_identifier(&name, limit)?;
189
190        let mut sql = format!(
191            "constraint {} foreign key ({}) references {} ({})",
192            dialect.quote(&name),
193            dialect.quote(&self.name),
194            quote_qualified(dialect, target)?,
195            dialect.quote(target_column)
196        );
197        if let Some(action) = self.on_delete {
198            sql.push_str(&format!(" on delete {action}"));
199        }
200        Ok(Some(sql))
201    }
202}
203
204/// The table being created or altered.
205#[derive(Default)]
206pub struct Table {
207    columns: Vec<Column>,
208    /// Multi-column indexes and uniques, which a single column cannot express.
209    indexes: Vec<(Vec<String>, bool)>,
210    drops: Vec<String>,
211}
212
213impl Table {
214    fn add(&mut self, column: Column) -> &mut Column {
215        self.columns.push(column);
216        self.columns.last_mut().expect("just pushed")
217    }
218
219    /// `id bigserial primary key` — the conventional key every table gets.
220    pub fn id(&mut self) -> &mut Column {
221        let mut column = Column::new("id", ColumnType::Id);
222        column.primary = true;
223        self.add(column)
224    }
225
226    /// A UUID primary key, for tables whose ids are exposed publicly.
227    pub fn uuid_id(&mut self) -> &mut Column {
228        let mut column = Column::new("id", ColumnType::UuidId);
229        column.primary = true;
230        column.default = Some(Default::Uuid);
231        self.add(column)
232    }
233
234    pub fn string(&mut self, name: &str) -> &mut Column {
235        self.add(Column::new(name, ColumnType::String { length: 255 }))
236    }
237
238    pub fn string_with(&mut self, name: &str, length: u32) -> &mut Column {
239        self.add(Column::new(name, ColumnType::String { length }))
240    }
241
242    pub fn text(&mut self, name: &str) -> &mut Column {
243        self.add(Column::new(name, ColumnType::Text))
244    }
245
246    pub fn integer(&mut self, name: &str) -> &mut Column {
247        self.add(Column::new(name, ColumnType::Integer))
248    }
249
250    pub fn big_integer(&mut self, name: &str) -> &mut Column {
251        self.add(Column::new(name, ColumnType::BigInteger))
252    }
253
254    pub fn float(&mut self, name: &str) -> &mut Column {
255        self.add(Column::new(name, ColumnType::Float))
256    }
257
258    /// An exact decimal — money belongs here, never in a float.
259    pub fn decimal(&mut self, name: &str, precision: u32, scale: u32) -> &mut Column {
260        self.add(Column::new(name, ColumnType::Decimal { precision, scale }))
261    }
262
263    pub fn boolean(&mut self, name: &str) -> &mut Column {
264        self.add(Column::new(name, ColumnType::Boolean))
265    }
266
267    pub fn json(&mut self, name: &str) -> &mut Column {
268        self.add(Column::new(name, ColumnType::Json))
269    }
270
271    pub fn uuid(&mut self, name: &str) -> &mut Column {
272        self.add(Column::new(name, ColumnType::Uuid))
273    }
274
275    pub fn date(&mut self, name: &str) -> &mut Column {
276        self.add(Column::new(name, ColumnType::Date))
277    }
278
279    pub fn timestamp(&mut self, name: &str) -> &mut Column {
280        self.add(Column::new(name, ColumnType::Timestamp))
281    }
282
283    pub fn binary(&mut self, name: &str) -> &mut Column {
284        self.add(Column::new(name, ColumnType::Binary))
285    }
286
287    /// A foreign key column named after the table it points at:
288    /// `t.foreign_id("user")` becomes `user_id bigint references users (id)`.
289    pub fn foreign_id(&mut self, singular: &str) -> &mut Column {
290        let name = format!("{singular}_id");
291        let table = crate::migration::pluralize(singular);
292        let mut column = Column::new(&name, ColumnType::BigInteger);
293        column.references = Some((table, "id".to_string()));
294        column.index = true;
295        self.add(column)
296    }
297
298    /// `created_at` and `updated_at`, both defaulting to now.
299    ///
300    /// The expression is filled in when the statements are rendered, because
301    /// only then is the database known.
302    pub fn timestamps(&mut self) {
303        self.add(Column::new("created_at", ColumnType::Timestamp)).default_now();
304        self.add(Column::new("updated_at", ColumnType::Timestamp)).default_now();
305    }
306
307    /// A nullable `deleted_at`, for soft deletes.
308    pub fn soft_deletes(&mut self) {
309        self.add(Column::new("deleted_at", ColumnType::Timestamp)).nullable();
310    }
311
312    /// An index across several columns.
313    pub fn index(&mut self, columns: &[&str]) {
314        self.indexes.push((columns.iter().map(|c| (*c).to_string()).collect(), false));
315    }
316
317    pub fn unique(&mut self, columns: &[&str]) {
318        self.indexes.push((columns.iter().map(|c| (*c).to_string()).collect(), true));
319    }
320
321    /// Drop a column. Only meaningful inside `alter`.
322    pub fn drop_column(&mut self, name: &str) {
323        self.drops.push(name.to_string());
324    }
325}
326
327/// Emits DDL and runs it.
328pub struct Schema<'a> {
329    db: &'a Database,
330}
331
332impl<'a> Schema<'a> {
333    pub fn new(db: &'a Database) -> Self {
334        Schema { db }
335    }
336
337    /// Create a table.
338    pub async fn create(&self, table: &str, define: impl FnOnce(&mut Table)) -> Result<()> {
339        for statement in create_statements(self.db.dialect(), table, define)? {
340            self.db.run(&statement).await?;
341        }
342        Ok(())
343    }
344
345    /// Create the table unless it exists, safely under concurrent callers.
346    ///
347    /// For a table an application creates on the way up rather than in a
348    /// migration. **Not** `has_table` followed by `create`: two processes
349    /// booting at once both see no table, both create, and one fails.
350    ///
351    /// `create table if not exists` is sent where the dialect has it — and the
352    /// error meaning "it exists" is accepted **anyway**, because PostgreSQL's
353    /// `if not exists` is not serialised against a concurrent create: two
354    /// callers can both pass its check and the loser gets `42P07 relation
355    /// already exists`, or `23505` on `pg_type_typname_nsp_index`. That is
356    /// documented PostgreSQL behaviour, and it was measured here: ten of eleven
357    /// tests failed the first time a suite booted against an empty database.
358    /// Indexes that already exist are accepted the same way.
359    pub async fn create_if_missing(&self, table: &str, define: impl FnOnce(&mut Table)) -> Result<()> {
360        let dialect = self.db.dialect();
361        let mut statements = create_statements(dialect, table, define)?;
362        let create = statements.remove(0);
363
364        let create = match dialect.supports_if_not_exists_table() {
365            true => create.replacen("create table ", "create table if not exists ", 1),
366            false => create,
367        };
368        match self.db.run(&create).await {
369            Ok(_) => {}
370            Err(error) if is_already_exists(&error) => return Ok(()),
371            Err(error) => return Err(error),
372        }
373
374        for statement in statements {
375            if let Err(error) = self.db.run(&statement).await
376                && !is_already_exists(&error)
377            {
378                return Err(error);
379            }
380        }
381        Ok(())
382    }
383
384    /// Add or drop columns on an existing table.
385    pub async fn alter(&self, table: &str, define: impl FnOnce(&mut Table)) -> Result<()> {
386        for statement in alter_statements(self.db.dialect(), table, define)? {
387            self.db.run(&statement).await?;
388        }
389        Ok(())
390    }
391
392    pub async fn drop(&self, table: &str) -> Result<()> {
393        let dialect = self.db.dialect();
394        let quoted = quote_qualified(dialect, table)?;
395        // `cascade` is PostgreSQL's; the others drop dependent constraints on
396        // their own terms.
397        let sql = match dialect.name() {
398            "postgres" => format!("drop table if exists {quoted} cascade"),
399            "sqlserver" => format!("drop table if exists {quoted}"),
400            _ => format!("drop table if exists {quoted}"),
401        };
402        self.db.run(&sql).await?;
403        Ok(())
404    }
405
406    pub async fn rename(&self, from: &str, to: &str) -> Result<()> {
407        self.db
408            .run(&format!(
409                "alter table {} rename to {}",
410                quote_qualified(self.db.dialect(), from)?,
411                quote_qualified(self.db.dialect(), to)?
412            ))
413            .await?;
414        Ok(())
415    }
416
417    /// Whether a table exists — how the migration runner decides what to do.
418    pub async fn has_table(&self, table: &str) -> Result<bool> {
419        let dialect = self.db.dialect();
420        let found = self
421            .db
422            .scalar::<i64>(
423                &format!(
424                    "select count(*) from information_schema.tables \
425                     where table_schema = {} and table_name = {}",
426                    dialect.current_schema_expression(),
427                    dialect.placeholder(1)
428                ),
429                &[crate::Value::from(table)],
430            )
431            .await?;
432        Ok(found.unwrap_or(0) > 0)
433    }
434
435    pub async fn has_column(&self, table: &str, column: &str) -> Result<bool> {
436        let dialect = self.db.dialect();
437        let found = self
438            .db
439            .scalar::<i64>(
440                &format!(
441                    "select count(*) from information_schema.columns \
442                     where table_schema = {} and table_name = {} and column_name = {}",
443                    dialect.current_schema_expression(),
444                    dialect.placeholder(1),
445                    dialect.placeholder(2)
446                ),
447                &[crate::Value::from(table), crate::Value::from(column)],
448            )
449            .await?;
450        Ok(found.unwrap_or(0) > 0)
451    }
452}
453
454/// The statements a `create` produces: the table, then its indexes.
455pub fn create_statements(
456    dialect: &dyn Dialect,
457    table: &str,
458    define: impl FnOnce(&mut Table),
459) -> Result<Vec<String>> {
460    let mut definition = Table::default();
461    define(&mut definition);
462
463    let quoted = quote_qualified(dialect, table)?;
464    let mut lines: Vec<String> = definition
465        .columns
466        .iter()
467        .map(|column| column.to_sql(dialect))
468        .collect::<Result<_>>()?;
469
470    for column in &definition.columns {
471        if let Some(constraint) = column.foreign_key_sql(dialect, table)? {
472            lines.push(constraint);
473        }
474    }
475
476    let mut statements = vec![format!("create table {quoted} (\n  {}\n)", lines.join(",\n  "))];
477    statements.extend(index_statements(dialect, table, &definition)?);
478    Ok(statements)
479}
480
481/// Whether an error is a database saying the object already exists — the
482/// three engines say it three ways.
483fn is_already_exists(error: &rustlavel_core::Error) -> bool {
484    let text = error.to_string().to_ascii_lowercase();
485    text.contains("already exists")
486        || text.contains("42p07")
487        // PostgreSQL losing a concurrent `create table if not exists` race
488        // reports it as a duplicate in the type catalogue, not as 42P07.
489        || text.contains("pg_type_typname_nsp_index")
490        || text.contains("there is already an object")
491}
492
493/// The statements an `alter` produces.
494pub fn alter_statements(
495    dialect: &dyn Dialect,
496    table: &str,
497    define: impl FnOnce(&mut Table),
498) -> Result<Vec<String>> {
499    let mut definition = Table::default();
500    define(&mut definition);
501
502    let quoted = quote_qualified(dialect, table)?;
503    let mut statements = Vec::new();
504
505    for column in &definition.columns {
506        statements.push(format!(
507            "alter table {quoted} {} {}",
508            dialect.add_column_clause(),
509            column.to_sql(dialect)?
510        ));
511        // The constraint is a second statement: a column has to exist before
512        // anything can be declared about it.
513        if let Some(constraint) = column.foreign_key_sql(dialect, table)? {
514            statements.push(format!("alter table {quoted} add {constraint}"));
515        }
516    }
517    for name in &definition.drops {
518        validate_identifier(name, dialect.max_identifier_length())?;
519        statements.push(format!("alter table {quoted} drop column {}", dialect.quote(name)));
520    }
521
522    statements.extend(index_statements(dialect, table, &definition)?);
523    Ok(statements)
524}
525
526fn index_statements(
527    dialect: &dyn Dialect,
528    table: &str,
529    definition: &Table,
530) -> Result<Vec<String>> {
531    let quoted = quote_qualified(dialect, table)?;
532    // Only PostgreSQL understands `if not exists` on an index. Everywhere else
533    // a repeated create is an error — which is correct, since a migration runs
534    // exactly once.
535    let guard = if dialect.supports_if_not_exists_index() { "if not exists " } else { "" };
536    let limit = dialect.max_identifier_length();
537    let mut statements = Vec::new();
538
539    for column in definition.columns.iter().filter(|c| c.index) {
540        validate_identifier(&column.name, limit)?;
541        let name = format!("{table}_{}_index", column.name);
542        validate_identifier(&name, limit)?;
543        statements.push(format!(
544            "create index {guard}{} on {quoted} ({})",
545            dialect.quote(&name),
546            dialect.quote(&column.name)
547        ));
548    }
549
550    for (columns, unique) in &definition.indexes {
551        for column in columns {
552            validate_identifier(column, limit)?;
553        }
554        let name =
555            format!("{table}_{}_{}", columns.join("_"), if *unique { "unique" } else { "index" });
556        validate_identifier(&name, limit)?;
557        let quoted_columns: Vec<String> = columns.iter().map(|c| dialect.quote(c)).collect();
558        statements.push(format!(
559            "create {}index {guard}{} on {quoted} ({})",
560            if *unique { "unique " } else { "" },
561            dialect.quote(&name),
562            quoted_columns.join(", ")
563        ));
564    }
565
566    Ok(statements)
567}
568
569#[cfg(test)]
570mod tests {
571    use super::*;
572    use crate::dialect::{MySql, Postgres, SqlServer};
573
574    #[test]
575    fn builds_a_create_table_statement() {
576        let statements = create_statements(&Postgres, "users", |t| {
577            t.id();
578            t.string("name");
579            t.string("email").unique();
580            t.boolean("active").default_bool(true);
581            t.text("bio").nullable();
582            t.timestamps();
583        })
584        .unwrap();
585
586        assert_eq!(
587            statements[0],
588            "create table \"users\" (\n  \
589             \"id\" bigserial primary key,\n  \
590             \"name\" varchar(255) not null,\n  \
591             \"email\" varchar(255) not null unique,\n  \
592             \"active\" boolean not null default true,\n  \
593             \"bio\" text,\n  \
594             \"created_at\" timestamptz not null default now(),\n  \
595             \"updated_at\" timestamptz not null default now()\n)"
596        );
597    }
598
599    #[test]
600    fn a_foreign_id_points_at_the_pluralized_table_and_gets_an_index() {
601        let statements = create_statements(&Postgres, "posts", |t| {
602            t.id();
603            t.foreign_id("user").cascade_on_delete();
604        })
605        .unwrap();
606
607        // A table-level constraint, not an inline `references`: MySQL parses an
608        // inline one and silently creates no foreign key at all.
609        assert!(statements[0].contains("\"user_id\" bigint not null,"), "{}", statements[0]);
610        assert!(
611            statements[0].contains(
612                "constraint \"posts_user_id_foreign\" foreign key (\"user_id\") \
613                 references \"users\" (\"id\") on delete cascade"
614            ),
615            "{}",
616            statements[0]
617        );
618        assert_eq!(
619            statements[1],
620            "create index if not exists \"posts_user_id_index\" on \"posts\" (\"user_id\")"
621        );
622    }
623
624    #[test]
625    fn composite_indexes_get_their_own_statements() {
626        let statements = create_statements(&Postgres, "memberships", |t| {
627            t.id();
628            t.integer("team_id");
629            t.integer("user_id");
630            t.unique(&["team_id", "user_id"]);
631        })
632        .unwrap();
633
634        assert_eq!(
635            statements[1],
636            "create unique index if not exists \"memberships_team_id_user_id_unique\" \
637             on \"memberships\" (\"team_id\", \"user_id\")"
638        );
639    }
640
641    #[test]
642    fn alter_adds_and_drops_columns() {
643        let statements = alter_statements(&Postgres, "users", |t| {
644            t.string("nickname").nullable();
645            t.drop_column("legacy_flag");
646        })
647        .unwrap();
648
649        assert_eq!(statements[0], "alter table \"users\" add column \"nickname\" varchar(255)");
650        assert_eq!(statements[1], "alter table \"users\" drop column \"legacy_flag\"");
651
652        // SQL Server rejects `add column` but requires `drop column`.
653        let sqlserver = alter_statements(&SqlServer, "users", |t| {
654            t.string("nickname").nullable();
655            t.drop_column("legacy_flag");
656        })
657        .unwrap();
658        assert_eq!(sqlserver[0], "alter table [users] add [nickname] nvarchar(255)");
659        assert_eq!(sqlserver[1], "alter table [users] drop column [legacy_flag]");
660    }
661
662    #[test]
663    fn a_malicious_column_name_is_rejected() {
664        let error = create_statements(&Postgres, "users", |t| {
665            t.string("name\"; drop table users; --");
666        })
667        .unwrap_err();
668
669        assert!(error.to_string().contains("not a valid SQL identifier"));
670    }
671
672    #[test]
673    fn a_string_default_is_quoted_and_escaped() {
674        let statements = create_statements(&Postgres, "t", |t| {
675            t.string("motto").default("it's fine");
676        })
677        .unwrap();
678
679        assert!(statements[0].contains("default 'it''s fine'"));
680    }
681
682    #[test]
683    fn one_schema_definition_produces_correct_ddl_for_every_database() {
684        let define = |t: &mut Table| {
685            t.id();
686            t.string("email").unique();
687            t.boolean("active").default_bool(true);
688            t.timestamps();
689        };
690
691        let postgres = create_statements(&Postgres, "users", define).unwrap();
692        assert!(postgres[0].contains("\"id\" bigserial primary key"), "{}", postgres[0]);
693        assert!(postgres[0].contains("\"active\" boolean not null default true"));
694        assert!(postgres[0].contains("default now()"));
695
696        let mysql = create_statements(&MySql, "users", define).unwrap();
697        assert!(
698            mysql[0].contains("`id` bigint not null auto_increment primary key"),
699            "{}",
700            mysql[0]
701        );
702        // MySQL stores a boolean as a number, so `true` would not parse.
703        assert!(mysql[0].contains("`active` tinyint(1) not null default 1"), "{}", mysql[0]);
704        assert!(mysql[0].contains("default current_timestamp(6)"));
705
706        let sqlserver = create_statements(&SqlServer, "users", define).unwrap();
707        assert!(sqlserver[0].contains("[id] bigint identity(1,1) primary key"), "{}", sqlserver[0]);
708        assert!(sqlserver[0].contains("[active] bit not null default 1"), "{}", sqlserver[0]);
709        assert!(sqlserver[0].contains("default sysutcdatetime()"));
710    }
711
712    #[test]
713    fn only_postgres_guards_an_index_with_if_not_exists() {
714        let define = |t: &mut Table| {
715            t.id();
716            t.integer("team_id").index();
717        };
718
719        assert!(create_statements(&Postgres, "m", define).unwrap()[1].contains("if not exists"));
720        // Elsewhere a repeated create is an error, which is correct: a
721        // migration runs exactly once.
722        assert!(!create_statements(&MySql, "m", define).unwrap()[1].contains("if not exists"));
723        assert!(!create_statements(&SqlServer, "m", define).unwrap()[1].contains("if not exists"));
724    }
725
726    #[test]
727    fn a_uuid_default_mysql_cannot_express_is_refused_rather_than_faked() {
728        let define = |t: &mut Table| {
729            t.uuid_id();
730        };
731
732        assert!(create_statements(&Postgres, "t", define).unwrap()[0].contains("gen_random_uuid()"));
733        assert!(create_statements(&SqlServer, "t", define).unwrap()[0].contains("newid()"));
734
735        let error = create_statements(&MySql, "t", define).unwrap_err().to_string();
736        assert!(error.contains("has no expression for it"), "{error}");
737    }
738
739    #[test]
740    fn soft_deletes_add_a_nullable_timestamp() {
741        let statements = create_statements(&Postgres, "posts", |t| {
742            t.id();
743            t.soft_deletes();
744        })
745        .unwrap();
746
747        assert!(statements[0].contains("\"deleted_at\" timestamptz"));
748        assert!(!statements[0].contains("\"deleted_at\" timestamptz not null"));
749    }
750}