diff --git a/tests/schema_migration_test.lua b/tests/schema_migration_test.lua new file mode 100644 index 0000000..99367e1 --- /dev/null +++ b/tests/schema_migration_test.lua @@ -0,0 +1,337 @@ +-- tests/schema_migration_test.lua +-- Тестирование миграции схемы данных + +local function setup_schema_test() + print("=== Setup Schema Migration Test ===") + + db.execute("CREATE DATABASE IF NOT EXISTS schema_test") + db.execute("USE schema_test") + + -- Создаём коллекцию со старой схемой + db.execute([[ + CREATE COLLECTION IF NOT EXISTS products + WITH SCHEMA { + version = "1.0.0", + fields = { + _id = {type = "string", required = true}, + name = {type = "string", required = true}, + price = {type = "float"} + } + } + ]]) + + -- Вставляем тестовые данные + for i = 1, 50 do + db.execute(string.format([[ + INSERT INTO products VALUES { + _id = "prod_%d", + name = "Product %d", + price = %.2f + } + ]], i, i, math.random(100, 1000) / 100)) + end + + print("Inserted 50 test products") + + return true +end + +local function test_schema_version() + print("\n=== Test 1: Schema Version ===") + + local version = db.execute("SHOW SCHEMA VERSION") + print(string.format("Current schema version: %s", version)) + + if version and version ~= "" then + print("✅ Schema version retrieved") + return true + else + print("❌ Failed to get schema version") + return false + end +end + +local function test_add_field_migration() + print("\n=== Test 2: Add Field Migration ===") + + -- Миграция: добавляем поле description + local migration = [[ + MIGRATE SCHEMA products + ADD FIELD description { + type = "string", + default = "No description", + required = false + } + ]] + + local result = db.execute(migration) + print("Migration result: " .. tostring(result)) + + -- Проверяем, что у старых документов появилось поле description + local sample = db.execute("SELECT * FROM products LIMIT 1") + local sample_data = db.json_decode(sample) + + if sample_data[1].description == "No description" then + print("✅ Field added and default value applied") + return true + else + print("❌ Field migration failed") + return false + end +end + +local function test_change_field_type_migration() + print("\n=== Test 3: Change Field Type Migration ===") + + -- Миграция: меняем тип price на string с префиксом "$" + local migration = [[ + MIGRATE SCHEMA products + MODIFY FIELD price + TYPE string + TRANSFORM value -> "$" .. tostring(value) + ]] + + local result = db.execute(migration) + print("Migration result: " .. tostring(result)) + + -- Проверяем преобразование + local sample = db.execute("SELECT price FROM products LIMIT 1") + local sample_data = db.json_decode(sample) + + if string.sub(sample_data[1].price, 1, 1) == "$" then + print("✅ Field type conversion successful") + return true + else + print("❌ Field type conversion failed") + return false + end +end + +local function test_rename_field_migration() + print("\n=== Test 4: Rename Field Migration ===") + + -- Миграция: переименовываем name в title + local migration = [[ + MIGRATE SCHEMA products + RENAME FIELD name TO title + ]] + + local result = db.execute(migration) + print("Migration result: " .. tostring(result)) + + -- Проверяем, что поле переименовано + local sample = db.execute("SELECT title FROM products LIMIT 1") + local sample_data = db.json_decode(sample) + + local check_name = db.execute("SELECT name FROM products LIMIT 1") + + if sample_data[1].title and check_name == "null" then + print("✅ Field renamed successfully") + return true + else + print("❌ Field rename failed") + return false + end +end + +local function test_remove_field_migration() + print("\n=== Test 5: Remove Field Migration ===") + + -- Миграция: удаляем поле description + local migration = [[ + MIGRATE SCHEMA products + REMOVE FIELD description + ]] + + local result = db.execute(migration) + print("Migration result: " .. tostring(result)) + + -- Проверяем, что поле удалено + local sample = db.execute("SELECT * FROM products LIMIT 1") + local sample_data = db.json_decode(sample) + + if sample_data[1].description == nil then + print("✅ Field removed successfully") + return true + else + print("❌ Field removal failed") + return false + end +end + +local function test_add_validation_migration() + print("\n=== Test 6: Add Validation Migration ===") + + -- Добавляем валидацию для поля price + local migration = [[ + MIGRATE SCHEMA products + ADD VALIDATION price { + min = 0, + max = 10000, + type = "float" + } + ]] + + local result = db.execute(migration) + print("Migration result: " .. tostring(result)) + + -- Проверяем валидацию + local success, err = pcall(function() + return db.execute("INSERT INTO products VALUES {_id = 'invalid', title = 'Invalid', price = 20000}") + end) + + if not success and string.find(err or "", "validation") then + print("✅ Validation added and working") + return true + else + print("❌ Validation not working correctly") + return false + end +end + +local function test_rollback_migration() + print("\n=== Test 7: Rollback Migration ===") + + -- Сохраняем текущее состояние + local before_state = db.execute("SHOW SCHEMA STATUS") + + -- Откатываем последнюю миграцию + local result = db.execute("ROLLBACK SCHEMA MIGRATION") + print("Rollback result: " .. tostring(result)) + + -- Проверяем, что validation убрана + local success, err = pcall(function() + return db.execute("INSERT INTO products VALUES {_id = 'rollback_test', title = 'Rollback Test', price = 20000}") + end) + + if success then + print("✅ Rollback successful - validation removed") + return true + else + print("❌ Rollback failed") + return false + end +end + +local function test_schema_migration_history() + print("\n=== Test 8: Schema Migration History ===") + + local history = db.execute("SHOW SCHEMA MIGRATION HISTORY") + local history_data = db.json_decode(history) + + print("Migration history:") + for i, migration in ipairs(history_data) do + print(string.format(" %d: %s - %s (applied: %s)", + i, migration.id, migration.description, + os.date("%Y-%m-%d %H:%M:%S", migration.applied_at / 1000))) + end + + if #history_data >= 5 then + print("✅ Migration history contains multiple entries") + return true + else + print("⚠️ Migration history shorter than expected") + return false + end +end + +local function test_lazy_schema_migration() + print("\n=== Test 9: Lazy Schema Migration ===") + + -- Включаем ленивую миграцию + db.execute("SET SCHEMA MIGRATION STRATEGY lazy") + + -- Меняем схему + db.execute([[ + MIGRATE SCHEMA products + ADD FIELD tags { + type = "array", + default = {} + } + ]]) + + -- Существующие документы не должны иметь tags сразу + local sample = db.execute("SELECT tags FROM products LIMIT 1") + local sample_data = db.json_decode(sample) + + -- При первом чтении tags может отсутствовать + -- При следующем чтении (после миграции) должно появиться + + print("✅ Lazy migration configured") + return true +end + +local function test_schema_migration_metrics() + print("\n=== Test 10: Schema Migration Metrics ===") + + local metrics = db.execute("SHOW SCHEMA MIGRATION METRICS") + local metrics_data = db.json_decode(metrics) + + print("Migration metrics:") + for key, value in pairs(metrics_data) do + print(string.format(" %s: %s", key, tostring(value))) + end + + local required_fields = { + "total_migrations", + "applied_migrations", + "pending_migrations", + "current_version" + } + + for _, field in ipairs(required_fields) do + if metrics_data[field] == nil then + print("❌ Missing metric: " .. field) + return false + end + end + + print("✅ All required metrics present") + return true +end + +local function cleanup_schema() + print("\n=== Cleanup ===") + db.execute("DROP DATABASE IF EXISTS schema_test") + print("Cleanup completed") +end + +local function run_all_schema_migration_tests() + print("╔══════════════════════════════════════════════════════════════╗") + print("║ SCHEMA MIGRATION TESTS SUITE ║") + print("╚══════════════════════════════════════════════════════════════╝") + + setup_schema_test() + + local results = { + test_schema_version(), + test_add_field_migration(), + test_change_field_type_migration(), + test_rename_field_migration(), + test_remove_field_migration(), + test_add_validation_migration(), + test_rollback_migration(), + test_schema_migration_history(), + test_lazy_schema_migration(), + test_schema_migration_metrics() + } + + print("\n╔══════════════════════════════════════════════════════════════╗") + print("║ TEST RESULTS ║") + print("╚══════════════════════════════════════════════════════════════╝") + + local passed = 0 + for i, result in ipairs(results) do + local status = result and "✅ PASSED" or "⚠️ SKIPPED/FAILED" + print(string.format("Test %d: %s", i, status)) + if result then passed = passed + 1 end + end + + print(string.format("\nTotal: %d/%d tests passed", passed, #results)) + + cleanup_schema() + + return passed == #results +end + +return run_all_schema_migration_tests()