use aurora_db::parser::executor::ExecutionResult;
use aurora_db::{Aurora, AuroraConfig, Value};
async fn make_db() -> (Aurora, tempfile::TempDir) {
let tmp = tempfile::tempdir().unwrap();
let db = Aurora::with_config(AuroraConfig {
db_path: tmp.path().join("migration_test.db"),
..Default::default()
})
.await
.unwrap();
(db, tmp)
}
#[tokio::test]
async fn test_migration_add_field_and_backfill() {
let (db, _tmp) = make_db().await;
db.execute(r#"
schema {
define collection users {
name: String!
email: String! @indexed
}
}
"#).await.unwrap();
db.insert_into("users", vec![
("name", Value::String("Alice".into())),
("email", Value::String("alice@example.com".into())),
]).await.unwrap();
db.insert_into("users", vec![
("name", Value::String("Bob".into())),
("email", Value::String("bob@example.com".into())),
]).await.unwrap();
let result = db.execute(r#"
migrate {
"v1.1.0": {
alter collection users {
add role: String
}
migrate data in users {
set role = "member"
}
}
}
"#).await.unwrap();
if let ExecutionResult::Migration(m) = result {
assert_eq!(m.steps_applied, 1);
assert_eq!(m.version, "v1.1.0");
assert_eq!(m.status, "applied");
} else {
panic!("Expected Migration result");
}
let users = db.get_all_collection("users").await.unwrap();
assert_eq!(users.len(), 2);
for user in &users {
assert_eq!(
user.data.get("role"),
Some(&Value::String("member".into())),
);
}
}
#[tokio::test]
async fn test_migration_idempotent() {
let (db, _tmp) = make_db().await;
db.execute(r#"
schema {
define collection items {
title: String!
}
}
"#).await.unwrap();
let migration = r#"
migrate {
"v1.0.0": {
alter collection items {
add status: String
}
migrate data in items {
set status = "active"
}
}
}
"#;
let r1 = db.execute(migration).await.unwrap();
if let ExecutionResult::Migration(m) = r1 {
assert_eq!(m.steps_applied, 1);
assert_eq!(m.status, "applied");
} else { panic!("Expected Migration result"); }
let r2 = db.execute(migration).await.unwrap();
if let ExecutionResult::Migration(m) = r2 {
assert_eq!(m.steps_applied, 0);
assert_eq!(m.status, "skipped");
} else { panic!("Expected Migration result"); }
}
#[tokio::test]
async fn test_migration_multiple_versions_incremental() {
let (db, _tmp) = make_db().await;
db.execute(r#"
schema {
define collection products {
name: String!
}
}
"#).await.unwrap();
db.insert_into("products", vec![("name", Value::String("Widget".into()))]).await.unwrap();
db.execute(r#"
migrate {
"v1.0.0": {
alter collection products {
add price: Float
}
}
}
"#).await.unwrap();
let result = db.execute(r#"
migrate {
"v1.0.0": {
alter collection products {
add price: Float
}
}
"v2.0.0": {
alter collection products {
add category: String
}
migrate data in products {
set category = "general"
}
}
}
"#).await.unwrap();
if let ExecutionResult::Migration(m) = result {
assert_eq!(m.steps_applied, 1, "only v2 should be applied");
assert_eq!(m.version, "v2.0.0");
assert_eq!(m.status, "applied");
} else { panic!("Expected Migration result"); }
let products = db.get_all_collection("products").await.unwrap();
for p in &products {
assert_eq!(p.data.get("category"), Some(&Value::String("general".into())));
}
}
#[tokio::test]
async fn test_migration_data_with_where_filter() {
let (db, _tmp) = make_db().await;
db.execute(r#"
schema {
define collection orders {
amount: Float!
status: String!
label: String
}
}
"#).await.unwrap();
db.insert_into("orders", vec![
("amount", Value::Float(500.0)),
("status", Value::String("completed".into())),
]).await.unwrap();
db.insert_into("orders", vec![
("amount", Value::Float(50.0)),
("status", Value::String("pending".into())),
]).await.unwrap();
db.execute(r#"
migrate {
"v1.0.0": {
migrate data in orders {
set label = "done" where { status: { eq: "completed" } }
}
}
}
"#).await.unwrap();
let orders = db.get_all_collection("orders").await.unwrap();
for order in &orders {
let status = order.data.get("status").and_then(|v| v.as_str()).unwrap_or("");
let label = order.data.get("label");
if status == "completed" {
assert_eq!(label, Some(&Value::String("done".into())));
} else {
assert!(label.is_none() || label == Some(&Value::Null));
}
}
}
#[tokio::test]
async fn test_migration_drop_field() {
let (db, _tmp) = make_db().await;
db.execute(r#"
schema {
define collection logs {
message: String!
debug_info: String
}
}
"#).await.unwrap();
db.execute(r#"
migrate {
"v1.0.0": {
alter collection logs {
drop debug_info
}
}
}
"#).await.unwrap();
let col = db.get_collection_definition("logs").unwrap();
assert!(!col.fields.contains_key("debug_info"));
}
#[tokio::test]
async fn test_migration_rename_field() {
let (db, _tmp) = make_db().await;
db.execute(r#"
schema {
define collection events {
event_type: String!
ts: String
}
}
"#).await.unwrap();
db.insert_into("events", vec![
("event_type", Value::String("click".into())),
("ts", Value::String("2026-01-01T00:00:00Z".into())),
]).await.unwrap();
db.execute(r#"
migrate {
"v1.0.0": {
alter collection events {
rename ts to timestamp
}
}
}
"#).await.unwrap();
let col = db.get_collection_definition("events").unwrap();
assert!(!col.fields.contains_key("ts"), "old field name should be gone from schema");
assert!(col.fields.contains_key("timestamp"), "new field name should exist in schema");
let docs = db.get_all_collection("events").await.unwrap();
for doc in &docs {
assert!(!doc.data.contains_key("ts"), "old key should be gone from document");
assert!(doc.data.contains_key("timestamp"), "new key should exist in document");
}
}
#[tokio::test]
async fn test_migration_add_field_with_default() {
let (db, _tmp) = make_db().await;
db.execute(r#"
schema {
define collection accounts {
username: String!
}
}
"#).await.unwrap();
db.insert_into("accounts", vec![("username", Value::String("alice".into()))]).await.unwrap();
db.insert_into("accounts", vec![("username", Value::String("bob".into()))]).await.unwrap();
db.execute(r#"
migrate {
"v1.0.0": {
alter collection accounts {
add plan: String = "free"
}
}
}
"#).await.unwrap();
let accounts = db.get_all_collection("accounts").await.unwrap();
for acc in &accounts {
assert_eq!(
acc.data.get("plan"),
Some(&Value::String("free".into())),
"existing doc should have default value"
);
}
}
#[tokio::test]
async fn test_migration_numeric_version() {
let (db, _tmp) = make_db().await;
db.execute(r#"
schema {
define collection notes {
body: String!
}
}
"#).await.unwrap();
let result = db.execute(r#"
migrate {
1: {
alter collection notes {
add pinned: Boolean
}
}
2: {
alter collection notes {
add archived: Boolean
}
}
}
"#).await.unwrap();
if let ExecutionResult::Migration(m) = result {
assert_eq!(m.steps_applied, 2);
assert_eq!(m.status, "applied");
} else { panic!("Expected Migration result"); }
let col = db.get_collection_definition("notes").unwrap();
assert!(col.fields.contains_key("pinned"));
assert!(col.fields.contains_key("archived"));
}
#[tokio::test]
async fn test_migration_history_queryable() {
let (db, _tmp) = make_db().await;
db.execute(r#"
schema {
define collection things {
name: String!
}
}
"#).await.unwrap();
db.execute(r#"
migrate {
"v1.0.0": {
alter collection things {
add tag: String
}
}
}
"#).await.unwrap();
let result = db.execute(r#"
query {
_migrations {
version
status
applied_at
}
}
"#).await.unwrap();
if let ExecutionResult::Query(q) = result {
assert_eq!(q.documents.len(), 1);
assert_eq!(
q.documents[0].data.get("version"),
Some(&Value::String("v1.0.0".into()))
);
assert_eq!(
q.documents[0].data.get("status"),
Some(&Value::String("applied".into()))
);
} else {
panic!("Expected Query result");
}
}