#![cfg(feature = "migrations")]
use tork_orm_core::migration::generate::{generate, SchemaChange};
use tork_orm_core::migration::introspect::existing_columns;
use tork_orm_core::{ColumnDef, Database, SqlType};
use tork_orm_core::registry::TableSchema;
fn schema(table: &'static str, columns: Vec<ColumnDef>) -> TableSchema {
TableSchema { table, columns, indexes: vec![], checks: vec![] }
}
fn col(name: &'static str, sql_type: SqlType, nullable: bool) -> ColumnDef {
ColumnDef {
name,
sql_type,
primary_key: false,
auto: false,
nullable,
foreign_key: None,
default: None,
}
}
fn pk_col(name: &'static str) -> ColumnDef {
ColumnDef {
name,
sql_type: SqlType::Integer,
primary_key: true,
auto: true,
nullable: false,
foreign_key: None,
default: None,
}
}
async fn setup(sql: &str) -> Database {
let db = Database::connect(":memory:", 1).await.unwrap();
db.execute(sql.to_string(), vec![]).await.unwrap();
db
}
fn has_stmt(change: &SchemaChange, needle: &str) -> bool {
change.up.iter().any(|s| s.contains(needle))
}
fn has_down(change: &SchemaChange, needle: &str) -> bool {
change.down.iter().any(|s| s.contains(needle))
}
#[tokio::test]
async fn adds_column_missing_from_db() {
let db = setup("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL)").await;
let models = schema(
"t",
vec![
pk_col("id"),
col("name", SqlType::Text, false),
col("score", SqlType::Real, true),
],
);
let change = generate(&db, &[models]).await.unwrap();
assert!(!change.is_empty());
assert!(has_stmt(&change, "ADD COLUMN"), "expected ADD COLUMN in up: {:?}", change.up);
assert!(has_stmt(&change, "\"score\""), "expected score column: {:?}", change.up);
}
#[tokio::test]
async fn add_column_is_idempotent() {
let db = setup("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL)").await;
let models = schema(
"t",
vec![
pk_col("id"),
col("name", SqlType::Text, false),
col("score", SqlType::Real, true),
],
);
let change = generate(&db, &[models.clone()]).await.unwrap();
for stmt in &change.up {
if !stmt.trim_start().starts_with("--") {
db.execute(stmt.clone(), vec![]).await.unwrap();
}
}
let again = generate(&db, &[models]).await.unwrap();
assert!(again.is_empty(), "expected no-op after applying: {:?}", again.up);
}
#[tokio::test]
async fn add_column_down_drops_the_column() {
let db = setup("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL)").await;
let models = schema(
"t",
vec![pk_col("id"), col("name", SqlType::Text, false), col("score", SqlType::Real, true)],
);
let change = generate(&db, &[models]).await.unwrap();
assert!(has_down(&change, "DROP COLUMN"), "expected DROP COLUMN in down: {:?}", change.down);
assert!(has_down(&change, "\"score\""), "expected score in down: {:?}", change.down);
}
#[tokio::test]
async fn not_null_column_added_as_nullable_with_note() {
let db = setup("CREATE TABLE t (id INTEGER PRIMARY KEY)").await;
let models = schema(
"t",
vec![pk_col("id"), col("required", SqlType::Text, false)],
);
let change = generate(&db, &[models]).await.unwrap();
assert!(has_stmt(&change, "ADD COLUMN"));
assert!(
change.up.iter().any(|s| s.starts_with("-- NOTE:") && s.contains("nullable")),
"expected nullable NOTE: {:?}",
change.up
);
db.execute(
"INSERT INTO t (id) VALUES (1)".to_string(),
vec![],
)
.await
.unwrap();
for stmt in &change.up {
if !stmt.trim_start().starts_with("--") {
db.execute(stmt.clone(), vec![]).await.unwrap();
}
}
}
#[tokio::test]
async fn drops_column_removed_from_model() {
let db = setup(
"CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, obsolete TEXT)",
)
.await;
let models = schema(
"t",
vec![pk_col("id"), col("name", SqlType::Text, false)],
);
let change = generate(&db, &[models]).await.unwrap();
assert!(!change.is_empty());
assert!(has_stmt(&change, "DROP COLUMN"), "expected DROP COLUMN: {:?}", change.up);
assert!(has_stmt(&change, "\"obsolete\""), "expected obsolete col: {:?}", change.up);
}
#[tokio::test]
async fn drop_column_down_restores_the_column() {
let db = setup(
"CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, obsolete TEXT)",
)
.await;
let models = schema("t", vec![pk_col("id"), col("name", SqlType::Text, false)]);
let change = generate(&db, &[models]).await.unwrap();
assert!(has_down(&change, "ADD COLUMN"), "expected ADD COLUMN in down: {:?}", change.down);
assert!(has_down(&change, "\"obsolete\""));
}
#[tokio::test]
async fn drop_column_is_idempotent() {
let db = setup(
"CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, obsolete TEXT)",
)
.await;
let models = schema("t", vec![pk_col("id"), col("name", SqlType::Text, false)]);
let change = generate(&db, &[models.clone()]).await.unwrap();
for stmt in &change.up {
if !stmt.trim_start().starts_with("--") {
db.execute(stmt.clone(), vec![]).await.unwrap();
}
}
let cols = existing_columns(&db, "t").await.unwrap();
assert!(cols.iter().all(|c| c.name != "obsolete"), "obsolete should be gone");
let again = generate(&db, &[models]).await.unwrap();
assert!(again.is_empty(), "expected no-op: {:?}", again.up);
}
#[tokio::test]
async fn primary_key_removal_emits_note_not_drop() {
let db = setup("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)").await;
let models = schema("t", vec![col("name", SqlType::Text, true)]);
let change = generate(&db, &[models]).await.unwrap();
assert!(
change.is_empty(),
"pk removal should produce no executable statements: {:?}",
change.up
);
assert!(
change.up.iter().any(|s| s.starts_with("-- NOTE:")),
"expected a NOTE for pk column: {:?}",
change.up
);
}
#[tokio::test]
async fn type_mismatch_emits_note_but_no_executable_statement() {
let db = setup("CREATE TABLE t (id INTEGER PRIMARY KEY, count INTEGER NOT NULL)").await;
let models = schema(
"t",
vec![pk_col("id"), col("count", SqlType::BigInt, false)],
);
let change = generate(&db, &[models]).await.unwrap();
assert!(
change.is_empty(),
"type mismatch should have no executable statements: {:?}",
change.up
);
assert!(
change.up.iter().any(|s| s.starts_with("-- NOTE:")),
"expected a NOTE for type mismatch: {:?}",
change.up
);
}
#[tokio::test]
async fn no_diff_when_schema_matches() {
let db = setup(
"CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, bio TEXT)",
)
.await;
let models = schema(
"t",
vec![
pk_col("id"),
col("name", SqlType::Text, false),
col("bio", SqlType::Text, true),
],
);
let change = generate(&db, &[models]).await.unwrap();
assert!(change.is_empty(), "expected no diff: {:?}", change.up);
}