From 3d8486438264aa541b1ecd35688dc66b23328331 Mon Sep 17 00:00:00 2001 From: yukkop Date: Tue, 23 Dec 2025 19:31:44 +0000 Subject: [PATCH] fix(`package`): `migrator`: sqlite transactions --- package/migrator/migrator.sh | 43 +--- .../postgresql/migration-failure-rollback.sh | 214 ++++++++++++++++ .../test/sqlite/migration-failure-rollback.sh | 239 ++++++++++++++++++ 3 files changed, 459 insertions(+), 37 deletions(-) create mode 100644 test/package/migrator/test/postgresql/migration-failure-rollback.sh create mode 100644 test/package/migrator/test/sqlite/migration-failure-rollback.sh diff --git a/package/migrator/migrator.sh b/package/migrator/migrator.sh index 45d3071..a624550 100644 --- a/package/migrator/migrator.sh +++ b/package/migrator/migrator.sh @@ -100,7 +100,7 @@ db_query() { local db_path db_path=$(get_sqlite_path) # Use -noheader -list for clean output (one value per line, no formatting) - sqlite3 -noheader -list "$db_path" "$sql" | awk NF + sqlite3 -bail -noheader -list "$db_path" "$sql" | awk NF ;; esac } @@ -126,7 +126,7 @@ SQL sqlite) local db_path db_path=$(get_sqlite_path) - sqlite3 -batch "$db_path" < "migration/${mig1}/up.sql" < "migration/${mig1}/down.sql" + +# Create second FAILING migration (syntax error) +mig2="20250101000002-broken-migration" +mkdir -p "migration/${mig2}" +cat > "migration/${mig2}/up.sql" < "migration/${mig2}/down.sql" + +# Apply first migration (should succeed) +if ! migrator --db-url "$DATABASE_URL" migrate up; then + log error "test failed: ${WHITE}first migration should succeed" + exit 1 +fi + +# Verify first migration was recorded +count=$(psql -Atc "SELECT COUNT(*) FROM hectic.migration" "$DATABASE_URL") +if [ "$count" != "1" ]; then + log error "test failed: ${WHITE}expected 1 migration, got $count" + exit 1 +fi + +log info "first migration successful and recorded" + +# Try to apply second migration (should fail) +set +e +migrator --db-url "$DATABASE_URL" migrate up 2>&1 +exit_code=$? +set -e + +if [ "$exit_code" = "0" ]; then + log error "test failed: ${WHITE}broken migration should have failed" + exit 1 +fi + +log info "second migration failed as expected (exit code: $exit_code)" + +# CRITICAL CHECK: Verify the failed migration was NOT recorded +count_after=$(psql -Atc "SELECT COUNT(*) FROM hectic.migration" "$DATABASE_URL") +if [ "$count_after" != "1" ]; then + log error "test failed: ${WHITE}CRITICAL! Failed migration was recorded. Expected 1, got $count_after" + log error "This means the transaction was not rolled back properly!" + exit 1 +fi + +log info "✓ Failed migration was NOT recorded (transaction rolled back)" + +# Verify the description column was NOT created (transaction rollback) +if psql -Atc "SELECT description FROM products LIMIT 0" "$DATABASE_URL" >/dev/null 2>&1; then + log error "test failed: ${WHITE}CRITICAL! 'description' column exists after failed migration" + log error "This means partial changes were committed!" + exit 1 +fi + +log info "✓ No partial changes committed" + +# Verify price column from first migration still exists +if ! psql -Atc "SELECT price FROM products LIMIT 0" "$DATABASE_URL" >/dev/null 2>&1; then + log error "test failed: ${WHITE}first migration's changes were lost" + exit 1 +fi + +log info "✓ First migration's changes preserved" + +### CASE 2: Failed migration in the middle of a transaction +log notice "test case: ${WHITE}multi-statement migration fails atomically" + +# Create migration with multiple statements, one fails in the middle +mig3="20250101000003-multi-statement-fail" +mkdir -p "migration/${mig3}" +cat > "migration/${mig3}/up.sql" < "migration/${mig3}/down.sql" + +# Try to apply migration (should fail) +set +e +migrator --db-url "$DATABASE_URL" migrate up 2>&1 +exit_code=$? +set -e + +if [ "$exit_code" = "0" ]; then + log error "test failed: ${WHITE}multi-statement broken migration should have failed" + exit 1 +fi + +log info "multi-statement migration failed as expected" + +# Verify migration was NOT recorded +count_after_multi=$(psql -Atc "SELECT COUNT(*) FROM hectic.migration" "$DATABASE_URL") +if [ "$count_after_multi" != "1" ]; then + log error "test failed: ${WHITE}failed multi-statement migration was recorded" + exit 1 +fi + +log info "✓ Failed migration was NOT recorded" + +# Verify NO partial changes were committed +if psql -Atc "SELECT stock FROM products LIMIT 0" "$DATABASE_URL" >/dev/null 2>&1; then + log error "test failed: ${WHITE}CRITICAL! 'stock' column exists (partial commit in failed migration)" + exit 1 +fi + +log info "✓ No partial changes from multi-statement migration" + +# Verify no data was inserted +row_count=$(psql -Atc "SELECT COUNT(*) FROM products" "$DATABASE_URL") +if [ "$row_count" != "0" ]; then + log error "test failed: ${WHITE}data was inserted despite migration failure" + exit 1 +fi + +log info "✓ No data inserted from failed migration" + +### CASE 3: Migration with constraint violation +log notice "test case: ${WHITE}constraint violation rolls back transaction" + +mig4="20250101000004-constraint-violation" +mkdir -p "migration/${mig4}" +cat > "migration/${mig4}/up.sql" < "migration/${mig4}/down.sql" + +# Try to apply (should fail due to constraint violation) +set +e +migrator --db-url "$DATABASE_URL" migrate up 2>&1 +exit_code=$? +set -e + +if [ "$exit_code" = "0" ]; then + log error "test failed: ${WHITE}constraint violation migration should have failed" + exit 1 +fi + +log info "constraint violation migration failed as expected" + +# Verify migration was NOT recorded +final_count=$(psql -Atc "SELECT COUNT(*) FROM hectic.migration" "$DATABASE_URL") +if [ "$final_count" != "1" ]; then + log error "test failed: ${WHITE}constraint violation migration was recorded" + exit 1 +fi + +log info "✓ Constraint violation migration was NOT recorded" + +# Verify sku column was NOT added (full rollback) +if psql -Atc "SELECT sku FROM products LIMIT 0" "$DATABASE_URL" >/dev/null 2>&1; then + log error "test failed: ${WHITE}sku column exists after constraint violation" + exit 1 +fi + +log info "✓ All changes rolled back after constraint violation" + +# Verify no data was committed +final_row_count=$(psql -Atc "SELECT COUNT(*) FROM products" "$DATABASE_URL") +if [ "$final_row_count" != "0" ]; then + log error "test failed: ${WHITE}data exists after constraint violation rollback" + exit 1 +fi + +log info "✓ No data committed after constraint violation" + +log notice "test passed: all migration failures properly roll back transactions" + diff --git a/test/package/migrator/test/sqlite/migration-failure-rollback.sh b/test/package/migrator/test/sqlite/migration-failure-rollback.sh new file mode 100644 index 0000000..117735c --- /dev/null +++ b/test/package/migrator/test/sqlite/migration-failure-rollback.sh @@ -0,0 +1,239 @@ +#!/bin/dash + +HECTIC_NAMESPACE=test-sqlite-migration-failure-rollback + +log notice "test case: ${WHITE}SQLite migration failure causes transaction rollback" + +# Create SQLite database +SQLITE_DB="$PWD/test.db" +export DB_URL="sqlite://$SQLITE_DB" + +log info "using SQLite database: $SQLITE_DB" + +# Create initial schema +sqlite3 "$SQLITE_DB" "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)" + +# Initialize migrator +if ! migrator --db-url "$DB_URL" init; then + log error "test failed: ${WHITE}init failed" + exit 1 +fi + +log info "setup complete" + +### CASE 1: Failed migration doesn't create migration record +log notice "test case: ${WHITE}failed SQLite migration doesn't create record" + +# Create migrations directory +mkdir -p migration + +# Create first SUCCESSFUL migration +mig1="20250101000001-add-quantity" +mkdir -p "migration/${mig1}" +cat > "migration/${mig1}/up.sql" < "migration/${mig1}/down.sql" + +# Create second FAILING migration (syntax error) +mig2="20250101000002-broken-migration" +mkdir -p "migration/${mig2}" +cat > "migration/${mig2}/up.sql" < "migration/${mig2}/down.sql" + +# Apply first migration (should succeed) +if ! migrator --db-url "$DB_URL" migrate up; then + log error "test failed: ${WHITE}first migration should succeed" + exit 1 +fi + +# Verify first migration was recorded +count=$(sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM hectic_migration") +if [ "$count" != "1" ]; then + log error "test failed: ${WHITE}expected 1 migration, got $count" + exit 1 +fi + +log info "first migration successful and recorded" + +# Try to apply second migration (should fail) +set +e +migrator --db-url "$DB_URL" migrate up 2>&1 +exit_code=$? +set -e + +if [ "$exit_code" = "0" ]; then + log error "test failed: ${WHITE}broken migration should have failed" + exit 1 +fi + +log info "second migration failed as expected (exit code: $exit_code)" + +# CRITICAL CHECK: Verify the failed migration was NOT recorded +count_after=$(sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM hectic_migration") +if [ "$count_after" != "1" ]; then + log error "test failed: ${WHITE}CRITICAL! Failed migration was recorded. Expected 1, got $count_after" + log error "This means the transaction was not rolled back properly!" + exit 1 +fi + +log info "✓ Failed migration was NOT recorded (transaction rolled back)" + +# Verify the status column was NOT created (transaction rollback) +set +e +sqlite3 "$SQLITE_DB" "SELECT status FROM items LIMIT 0" >/dev/null 2>&1 +status_exists=$? +set -e + +if [ "$status_exists" = "0" ]; then + log error "test failed: ${WHITE}CRITICAL! 'status' column exists after failed migration" + log error "This means partial changes were committed!" + exit 1 +fi + +log info "✓ No partial changes committed" + +# Verify quantity column from first migration still exists +if ! sqlite3 "$SQLITE_DB" "SELECT quantity FROM items LIMIT 0" >/dev/null 2>&1; then + log error "test failed: ${WHITE}first migration's changes were lost" + exit 1 +fi + +log info "✓ First migration's changes preserved" + +### CASE 2: Multi-statement migration fails atomically +log notice "test case: ${WHITE}multi-statement SQLite migration fails atomically" + +mig3="20250101000003-multi-statement-fail" +mkdir -p "migration/${mig3}" +cat > "migration/${mig3}/up.sql" < "migration/${mig3}/down.sql" + +# Try to apply migration (should fail) +set +e +migrator --db-url "$DB_URL" migrate up 2>&1 +exit_code=$? +set -e + +if [ "$exit_code" = "0" ]; then + log error "test failed: ${WHITE}multi-statement broken migration should have failed" + exit 1 +fi + +log info "multi-statement migration failed as expected" + +# Verify migration was NOT recorded +count_after_multi=$(sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM hectic_migration") +if [ "$count_after_multi" != "1" ]; then + log error "test failed: ${WHITE}failed multi-statement migration was recorded" + exit 1 +fi + +log info "✓ Failed migration was NOT recorded" + +# Verify NO partial changes were committed +set +e +sqlite3 "$SQLITE_DB" "SELECT location FROM items LIMIT 0" >/dev/null 2>&1 +location_exists=$? +set -e + +if [ "$location_exists" = "0" ]; then + log error "test failed: ${WHITE}CRITICAL! 'location' column exists (partial commit in failed migration)" + exit 1 +fi + +log info "✓ No partial changes from multi-statement migration" + +# Verify temp_table was NOT created +set +e +sqlite3 "$SQLITE_DB" "SELECT * FROM temp_table LIMIT 0" >/dev/null 2>&1 +temp_exists=$? +set -e + +if [ "$temp_exists" = "0" ]; then + log error "test failed: ${WHITE}temp_table exists (partial commit in failed migration)" + exit 1 +fi + +log info "✓ No tables created from failed migration" + +### CASE 3: Constraint violation rolls back transaction +log notice "test case: ${WHITE}SQLite constraint violation rolls back transaction" + +mig4="20250101000004-constraint-violation" +mkdir -p "migration/${mig4}" +cat > "migration/${mig4}/up.sql" < "migration/${mig4}/down.sql" + +# Try to apply (should fail due to constraint violation) +set +e +migrator --db-url "$DB_URL" migrate up 2>&1 +exit_code=$? +set -e + +if [ "$exit_code" = "0" ]; then + log error "test failed: ${WHITE}constraint violation migration should have failed" + exit 1 +fi + +log info "constraint violation migration failed as expected" + +# Verify migration was NOT recorded +final_count=$(sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM hectic_migration") +if [ "$final_count" != "1" ]; then + log error "test failed: ${WHITE}constraint violation migration was recorded" + exit 1 +fi + +log info "✓ Constraint violation migration was NOT recorded" + +# Verify code column was NOT added (full rollback) +set +e +sqlite3 "$SQLITE_DB" "SELECT code FROM items LIMIT 0" >/dev/null 2>&1 +code_exists=$? +set -e + +if [ "$code_exists" = "0" ]; then + log error "test failed: ${WHITE}code column exists after constraint violation" + exit 1 +fi + +log info "✓ All changes rolled back after constraint violation" + +# Verify no data was committed +final_row_count=$(sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM items") +if [ "$final_row_count" != "0" ]; then + log error "test failed: ${WHITE}data exists after constraint violation rollback (got $final_row_count rows)" + exit 1 +fi + +log info "✓ No data committed after constraint violation" + +log notice "test passed: all SQLite migration failures properly roll back transactions" +