338 lines
11 KiB
Lua
338 lines
11 KiB
Lua
|
|
-- 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()
|