use std::borrow::Cow;
use drizzle_migrations::sqlite::{
SQLiteDDL, SchemaDiff,
collection::diff_ddl,
ddl::{
ColumnDef, ForeignKeyDef, IndexColumnDef, IndexDef, PrimaryKeyDef, ReferentialAction,
TableDef,
},
statements::SqliteGenerator,
};
fn diff_to_sql(from: &SQLiteDDL, to: &SQLiteDDL) -> Vec<String> {
let diffs = diff_ddl(from, to);
let generator = SqliteGenerator::new();
let diff = SchemaDiff { diffs };
generator.generate_migration(&diff)
}
fn normalize_create_table(sql: &str) -> String {
let open = match sql.find('(') {
Some(i) => i,
None => return sql.to_string(),
};
let close = match sql.rfind(')') {
Some(i) => i,
None => return sql.to_string(),
};
let prefix = &sql[..=open]; let body = &sql[open + 1..close]; let suffix = &sql[close..];
let mut lines: Vec<&str> = body.split(",\n").map(|l| l.trim_matches('\n')).collect();
lines.sort();
format!("{}{}{}", prefix, lines.join(",\n"), suffix)
}
#[test]
fn test_create_table_basic() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns
.push(ColumnDef::new("users", "id", "integer").into_column());
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1, "Expected 1 SQL statement, got: {:?}", sql);
assert_eq!(
sql[0], "CREATE TABLE `users` (\n\t`id` INTEGER\n);",
"Unexpected CREATE TABLE SQL"
);
}
#[test]
fn test_create_table_with_primary_key_autoincrement() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns.push(
ColumnDef::new("users", "id", "integer")
.primary_key()
.autoincrement()
.into_column(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1);
assert_eq!(
sql[0], "CREATE TABLE `users` (\n\t`id` INTEGER AUTOINCREMENT NOT NULL\n);",
"Unexpected CREATE TABLE with PRIMARY KEY AUTOINCREMENT"
);
}
#[test]
fn test_create_table_with_named_pk_constraint() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns
.push(ColumnDef::new("users", "id", "integer").into_column());
const PK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
to.pks.push(
PrimaryKeyDef::new("users", "users_pk")
.columns(PK_COLS)
.into_primary_key(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1);
assert_eq!(
sql[0], "CREATE TABLE `users` (\n\t`id` INTEGER PRIMARY KEY\n);",
"Unexpected CREATE TABLE with named PK constraint"
);
}
#[test]
fn test_create_multiple_tables() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns
.push(ColumnDef::new("users", "id", "integer").into_column());
to.tables.push(TableDef::new("posts").into_table());
to.columns
.push(ColumnDef::new("posts", "id", "integer").into_column());
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(
sql.len(),
2,
"Expected 2 CREATE TABLE statements, got: {:?}",
sql
);
let mut sorted_sql = sql.clone();
sorted_sql.sort();
assert_eq!(
sorted_sql[0], "CREATE TABLE `posts` (\n\t`id` INTEGER\n);",
"Unexpected posts table SQL"
);
assert_eq!(
sorted_sql[1], "CREATE TABLE `users` (\n\t`id` INTEGER\n);",
"Unexpected users table SQL"
);
}
#[test]
fn test_create_table_composite_pk() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns
.push(ColumnDef::new("users", "id1", "integer").into_column());
to.columns
.push(ColumnDef::new("users", "id2", "integer").into_column());
const PK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("id1"), Cow::Borrowed("id2")];
to.pks.push(
PrimaryKeyDef::new("users", "users_pk")
.columns(PK_COLS)
.into_primary_key(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1);
assert_eq!(
normalize_create_table(&sql[0]),
normalize_create_table(
"CREATE TABLE `users` (\n\t`id1` INTEGER,\n\t`id2` INTEGER,\n\tCONSTRAINT `users_pk` PRIMARY KEY(`id1`, `id2`)\n);"
),
);
}
#[test]
fn test_drop_and_create_table() {
let mut from = SQLiteDDL::default();
from.tables.push(TableDef::new("users1").into_table());
from.columns
.push(ColumnDef::new("users1", "id", "integer").into_column());
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users2").into_table());
to.columns
.push(ColumnDef::new("users2", "id", "integer").into_column());
let sql = diff_to_sql(&from, &to);
assert_eq!(
sql.len(),
2,
"Expected CREATE and DROP statements, got: {:?}",
sql
);
let drop_sql = sql.iter().find(|s| s.contains("DROP TABLE")).unwrap();
let create_sql = sql.iter().find(|s| s.contains("CREATE TABLE")).unwrap();
assert_eq!(
*drop_sql, "DROP TABLE `users1`;",
"Unexpected DROP TABLE SQL"
);
assert_eq!(
*create_sql, "CREATE TABLE `users2` (\n\t`id` INTEGER\n);",
"Unexpected CREATE TABLE SQL"
);
}
#[test]
fn test_create_table_self_referencing_fk() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns.push(
ColumnDef::new("users", "id", "integer")
.primary_key()
.autoincrement()
.into_column(),
);
to.columns
.push(ColumnDef::new("users", "reportee_id", "integer").into_column());
const FK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("reportee_id")];
const FK_REFS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
to.fks.push(
ForeignKeyDef::new("users", "fk_users_reportee_id_users_id_fk")
.columns(FK_COLS)
.references("users", FK_REFS)
.into_foreign_key(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1);
assert_eq!(
normalize_create_table(&sql[0]),
normalize_create_table(
"CREATE TABLE `users` (\n\t`id` INTEGER AUTOINCREMENT NOT NULL,\n\t`reportee_id` INTEGER,\n\tCONSTRAINT `fk_users_reportee_id_users_id_fk` FOREIGN KEY (`reportee_id`) REFERENCES `users`(`id`)\n);"
),
);
}
#[test]
fn test_create_table_with_index() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns.push(
ColumnDef::new("users", "id", "integer")
.primary_key()
.autoincrement()
.into_column(),
);
to.columns
.push(ColumnDef::new("users", "reportee_id", "integer").into_column());
const IDX_COLS: &[IndexColumnDef] = &[IndexColumnDef::new("reportee_id")];
to.indexes.push(
IndexDef::new("users", "reportee_idx")
.columns(IDX_COLS)
.into_index(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(
sql.len(),
2,
"Expected CREATE TABLE and CREATE INDEX statements"
);
let create_table = sql.iter().find(|s| s.contains("CREATE TABLE")).unwrap();
let create_index = sql.iter().find(|s| s.contains("CREATE INDEX")).unwrap();
assert_eq!(
normalize_create_table(create_table),
normalize_create_table(
"CREATE TABLE `users` (\n\t`id` INTEGER AUTOINCREMENT NOT NULL,\n\t`reportee_id` INTEGER\n);"
),
);
assert_eq!(
*create_index,
"CREATE INDEX `reportee_idx` ON `users`(`reportee_id`);",
);
}
#[test]
fn test_column_types() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("types_test").into_table());
to.columns
.push(ColumnDef::new("types_test", "int_col", "integer").into_column());
to.columns.push(
ColumnDef::new("types_test", "text_col", "text")
.not_null()
.into_column(),
);
to.columns.push(
ColumnDef::new("types_test", "real_col", "real")
.default_value("0.0")
.into_column(),
);
to.columns
.push(ColumnDef::new("types_test", "blob_col", "blob").into_column());
to.columns
.push(ColumnDef::new("types_test", "numeric_col", "numeric").into_column());
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1);
assert_eq!(
normalize_create_table(&sql[0]),
normalize_create_table(
"CREATE TABLE `types_test` (\n\t`int_col` INTEGER,\n\t`text_col` TEXT NOT NULL,\n\t`real_col` REAL DEFAULT 0.0,\n\t`blob_col` BLOB,\n\t`numeric_col` NUMERIC\n);"
),
);
}
#[test]
fn test_unique_column() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns.push(
ColumnDef::new("users", "id", "integer")
.primary_key()
.into_column(),
);
to.columns.push(
ColumnDef::new("users", "email", "text")
.not_null()
.unique()
.into_column(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1);
assert_eq!(
normalize_create_table(&sql[0]),
normalize_create_table(
"CREATE TABLE `users` (\n\t`id` INTEGER NOT NULL,\n\t`email` TEXT NOT NULL\n);"
),
);
}
#[test]
fn test_named_single_column_unique_is_table_constraint() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns
.push(ColumnDef::new("users", "email", "text").into_column());
const UQ_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("email")];
to.uniques.push(
drizzle_migrations::sqlite::ddl::UniqueConstraintDef::new("users", "users_email_unique")
.columns(UQ_COLS)
.explicit_name()
.into_unique_constraint(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1);
assert_eq!(
normalize_create_table(&sql[0]),
normalize_create_table(
"CREATE TABLE `users` (\n\t`email` TEXT,\n\tCONSTRAINT `users_email_unique` UNIQUE(`email`)\n);"
),
);
}
#[test]
fn test_column_collate_is_preserved_in_create_table() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns.push(
ColumnDef::new("users", "name", "text")
.collate("NOCASE")
.into_column(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1);
assert_eq!(
sql[0],
"CREATE TABLE `users` (\n\t`name` TEXT COLLATE NOCASE\n);"
);
}
#[test]
fn test_unique_index() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns.push(
ColumnDef::new("users", "id", "integer")
.primary_key()
.into_column(),
);
to.columns
.push(ColumnDef::new("users", "email", "text").into_column());
const IDX_COLS: &[IndexColumnDef] = &[IndexColumnDef::new("email")];
to.indexes.push(
IndexDef::new("users", "idx_users_email")
.columns(IDX_COLS)
.unique()
.into_index(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 2);
let create_table = sql.iter().find(|s| s.contains("CREATE TABLE")).unwrap();
let create_index = sql
.iter()
.find(|s| s.contains("CREATE UNIQUE INDEX"))
.unwrap();
assert_eq!(
normalize_create_table(create_table),
normalize_create_table(
"CREATE TABLE `users` (\n\t`id` INTEGER NOT NULL,\n\t`email` TEXT\n);"
),
);
assert_eq!(
*create_index,
"CREATE UNIQUE INDEX `idx_users_email` ON `users`(`email`);",
);
}
#[test]
fn test_foreign_key_on_delete_cascade() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns.push(
ColumnDef::new("users", "id", "integer")
.primary_key()
.into_column(),
);
to.tables.push(TableDef::new("posts").into_table());
to.columns.push(
ColumnDef::new("posts", "id", "integer")
.primary_key()
.into_column(),
);
to.columns.push(
ColumnDef::new("posts", "author_id", "integer")
.not_null()
.into_column(),
);
const FK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("author_id")];
const FK_REFS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
to.fks.push(
ForeignKeyDef::new("posts", "fk_posts_author")
.columns(FK_COLS)
.references("users", FK_REFS)
.on_delete(ReferentialAction::Cascade)
.into_foreign_key(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 2, "Expected 2 CREATE TABLE statements");
let users_sql = sql
.iter()
.find(|s| s.contains("`users`") && !s.contains("REFERENCES"))
.unwrap();
let posts_sql = sql.iter().find(|s| s.contains("`posts`")).unwrap();
assert_eq!(
*users_sql,
"CREATE TABLE `users` (\n\t`id` INTEGER NOT NULL\n);",
);
assert_eq!(
normalize_create_table(posts_sql),
normalize_create_table(
"CREATE TABLE `posts` (\n\t`id` INTEGER NOT NULL,\n\t`author_id` INTEGER NOT NULL,\n\tCONSTRAINT `fk_posts_author` FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON DELETE CASCADE\n);"
),
);
}
#[test]
fn test_no_diff_for_identical_schemas() {
let mut schema = SQLiteDDL::default();
schema.tables.push(TableDef::new("users").into_table());
schema.columns.push(
ColumnDef::new("users", "id", "integer")
.primary_key()
.into_column(),
);
let schema_clone = schema.clone();
let sql = diff_to_sql(&schema, &schema_clone);
assert!(
sql.is_empty(),
"Expected no SQL for identical schemas, got: {:?}",
sql
);
}
#[test]
fn test_drop_table() {
let mut from = SQLiteDDL::default();
from.tables.push(TableDef::new("users").into_table());
from.columns
.push(ColumnDef::new("users", "id", "integer").into_column());
let to = SQLiteDDL::default();
let sql = diff_to_sql(&from, &to);
assert_eq!(sql.len(), 1);
assert_eq!(sql[0], "DROP TABLE `users`;", "Unexpected DROP TABLE SQL");
}
#[test]
fn test_drop_index() {
let mut from = SQLiteDDL::default();
from.tables.push(TableDef::new("users").into_table());
from.columns
.push(ColumnDef::new("users", "id", "integer").into_column());
from.columns
.push(ColumnDef::new("users", "email", "text").into_column());
const IDX_COLS: &[IndexColumnDef] = &[IndexColumnDef::new("email")];
from.indexes.push(
IndexDef::new("users", "idx_users_email")
.columns(IDX_COLS)
.into_index(),
);
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("users").into_table());
to.columns
.push(ColumnDef::new("users", "id", "integer").into_column());
to.columns
.push(ColumnDef::new("users", "email", "text").into_column());
let sql = diff_to_sql(&from, &to);
assert_eq!(sql.len(), 1, "Expected 1 DROP INDEX statement");
assert_eq!(
sql[0], "DROP INDEX IF EXISTS `idx_users_email`;",
"Unexpected DROP INDEX SQL"
);
}
#[test]
fn test_strict_table() {
let mut to = SQLiteDDL::default();
to.tables
.push(TableDef::new("settings").strict().into_table());
to.columns.push(
ColumnDef::new("settings", "id", "integer")
.primary_key()
.into_column(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1);
assert_eq!(
sql[0], "CREATE TABLE `settings` (\n\t`id` INTEGER NOT NULL\n) STRICT;",
"Unexpected STRICT table SQL"
);
}
#[test]
fn test_without_rowid_table() {
let mut to = SQLiteDDL::default();
to.tables
.push(TableDef::new("settings").without_rowid().into_table());
to.columns.push(
ColumnDef::new("settings", "id", "integer")
.primary_key()
.into_column(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(sql.len(), 1);
assert_eq!(
sql[0], "CREATE TABLE `settings` (\n\t`id` INTEGER NOT NULL\n) WITHOUT ROWID;",
"Unexpected WITHOUT ROWID table SQL"
);
}
#[test]
fn test_circular_fk_dependencies() {
let mut to = SQLiteDDL::default();
to.tables.push(TableDef::new("table_a").into_table());
to.columns.push(
ColumnDef::new("table_a", "id", "integer")
.primary_key()
.into_column(),
);
to.columns
.push(ColumnDef::new("table_a", "b_id", "integer").into_column());
to.tables.push(TableDef::new("table_b").into_table());
to.columns.push(
ColumnDef::new("table_b", "id", "integer")
.primary_key()
.into_column(),
);
to.columns
.push(ColumnDef::new("table_b", "a_id", "integer").into_column());
const FK_A_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("b_id")];
const FK_A_REFS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
to.fks.push(
ForeignKeyDef::new("table_a", "fk_a_to_b")
.columns(FK_A_COLS)
.references("table_b", FK_A_REFS)
.into_foreign_key(),
);
const FK_B_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("a_id")];
const FK_B_REFS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
to.fks.push(
ForeignKeyDef::new("table_b", "fk_b_to_a")
.columns(FK_B_COLS)
.references("table_a", FK_B_REFS)
.into_foreign_key(),
);
let sql = diff_to_sql(&SQLiteDDL::default(), &to);
assert_eq!(
sql.len(),
4,
"Expected 4 SQL statements for circular FK, got: {:?}",
sql
);
assert_eq!(sql[0], "PRAGMA foreign_keys=OFF;");
let create_a = sql
.iter()
.find(|s| s.contains("CREATE TABLE `table_a`"))
.unwrap();
let create_b = sql
.iter()
.find(|s| s.contains("CREATE TABLE `table_b`"))
.unwrap();
assert_eq!(
normalize_create_table(create_a),
normalize_create_table(
"CREATE TABLE `table_a` (\n\t`id` INTEGER NOT NULL,\n\t`b_id` INTEGER,\n\tCONSTRAINT `fk_a_to_b` FOREIGN KEY (`b_id`) REFERENCES `table_b`(`id`)\n);"
),
);
assert_eq!(
normalize_create_table(create_b),
normalize_create_table(
"CREATE TABLE `table_b` (\n\t`id` INTEGER NOT NULL,\n\t`a_id` INTEGER,\n\tCONSTRAINT `fk_b_to_a` FOREIGN KEY (`a_id`) REFERENCES `table_a`(`id`)\n);"
),
);
assert_eq!(sql[3], "PRAGMA foreign_keys=ON;");
}