diff --git a/nixos/system/bfs/bfs.nix b/nixos/system/bfs/bfs.nix index 8ceaf98..0df2ab4 100644 --- a/nixos/system/bfs/bfs.nix +++ b/nixos/system/bfs/bfs.nix @@ -49,7 +49,6 @@ in { ''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBKPbIJATVyAw7F7vBZbHkCODXFo5gvDyqhuU0gnNUNH'' ]; - boot.loader.grub.device = "/dev/vda"; boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" diff --git a/package/migrator/migrator.sh b/package/migrator/migrator.sh index fb5b61a..3c47259 100644 --- a/package/migrator/migrator.sh +++ b/package/migrator/migrator.sh @@ -431,8 +431,14 @@ ${BGREEN}Examples: ${BGREEN}Migration File Structure:$NC migration/ └── 20231201120000-migration-name/ - ├── up.sql - Forward migration - └── down.sql - Rollback migration + ├── up.sql - Forward migration (single file) + └── down.sql - Rollback migration (single file) + ${BBLACK}# or with multi-file layout:$NC + └── 20231201120000-migration-name/ + ├── up/ + │ └── entrypoint.sql - Forward migration entrypoint + └── down/ + └── entrypoint.sql - Rollback migration entrypoint ${BGREEN}Migration Naming:$NC Migrations must follow the format: YYYYMMDDHHMMSS-description @@ -609,6 +615,39 @@ migration_list() { find "$MIGRATION_DIR" -maxdepth 1 -type d -regextype posix-extended -regex '^.*/[0-9]{14}-.*$' -printf '%f\n' | sort } +# resolve_migration_path(migration_name, direction) +# direction: "up" or "down" +# Resolves the SQL file for a migration, supporting two layouts: +# 1. MIGRATION_DIR//up.sql (single-file) +# 2. MIGRATION_DIR//up/entrypoint.sql (multi-file) +# Returns the resolved path on stdout, exits with error if neither found. +resolve_migration_path() { + local name="$1" direction="$2" + local single_file="$MIGRATION_DIR/$name/${direction}.sql" + local multi_file="$MIGRATION_DIR/$name/${direction}/entrypoint.sql" + + if [ -f "$single_file" ]; then + printf '%s' "$single_file" + elif [ -f "$multi_file" ]; then + printf '%s' "$multi_file" + else + log error "migration ${direction} not found for ${WHITE}$name${NC}" + log error "expected either ${WHITE}$single_file${NC} or ${WHITE}$multi_file${NC}" + exit 1 + fi +} + +# has_migration_direction(migration_name, direction) +# direction: "up" or "down" +# Returns 0 if the migration has the given direction, 1 otherwise. +has_migration_direction() { + local name="$1" direction="$2" + local single_file="$MIGRATION_DIR/$name/${direction}.sql" + local multi_file="$MIGRATION_DIR/$name/${direction}/entrypoint.sql" + + [ -f "$single_file" ] || [ -f "$multi_file" ] +} + # index_of(array, name) index_of() { local list name m i=1 @@ -790,14 +829,9 @@ migrate() { fs_migration=$(printf '%s' "$fs_migrations" | sed -n "${i}p") escaped_name=$(printf '%s' "$fs_migration" | sed "s/'/''/g") - mig_path="$MIGRATION_DIR/$fs_migration/up.sql" + mig_path=$(resolve_migration_path "$fs_migration" "up") escaped_path=$(printf '%s' "$mig_path" | sed "s/'/''/g") - if [ ! -f "$mig_path" ]; then - log error "migration file not found: ${WHITE}$mig_path${NC}" - exit 1 - fi - mig_hash=$(sha256sum "$mig_path") log info "applying migration ${WHITE}$fs_migration${NC} (up)" @@ -847,14 +881,9 @@ SQL fs_migration=$(printf '%s' "$fs_migrations" | sed -n "${i}p") escaped_name=$(printf '%s' "$fs_migration" | sed "s/'/''/g") - mig_path="$MIGRATION_DIR/$fs_migration/down.sql" + mig_path=$(resolve_migration_path "$fs_migration" "down") escaped_path=$(printf '%s' "$mig_path" | sed "s/'/''/g") - if [ ! -f "$mig_path" ]; then - log error "migration file not found: ${WHITE}$mig_path${NC}" - exit 1 - fi - log info "reverting migration ${WHITE}$fs_migration${NC} (down)" case "$db_type" in @@ -981,12 +1010,27 @@ list() { } migration_list | while read -r name; do - dir="./${MIGRATION_DIR}/${name}" - up="$dir/up.sql" - down="$dir/down.sql" + has_up=true + has_down=true + + if ! has_migration_direction "$name" "up"; then + has_up=false + fi + if ! has_migration_direction "$name" "down"; then + has_down=false + fi - if [ ! -f "$up" ] || [ ! -f "$down" ]; then - echo "$name: missing $( [ ! -f "$up" ] && echo up.sql ) $( [ ! -f "$down" ] && echo down.sql )" + if [ "$has_up" = false ] || [ "$has_down" = false ]; then + missing="" + [ "$has_up" = false ] && missing="up.sql" + if [ "$has_down" = false ]; then + if [ -n "$missing" ]; then + missing="$missing down.sql" + else + missing="down.sql" + fi + fi + echo "$name: missing $missing" else echo "$name" fi diff --git a/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000001-create-items/down/entrypoint.sql b/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000001-create-items/down/entrypoint.sql new file mode 100644 index 0000000..7d571c3 --- /dev/null +++ b/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000001-create-items/down/entrypoint.sql @@ -0,0 +1 @@ +DROP TABLE items; diff --git a/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000001-create-items/up/entrypoint.sql b/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000001-create-items/up/entrypoint.sql new file mode 100644 index 0000000..c6a6686 --- /dev/null +++ b/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000001-create-items/up/entrypoint.sql @@ -0,0 +1,4 @@ +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); diff --git a/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000002-add-columns/down/entrypoint.sql b/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000002-add-columns/down/entrypoint.sql new file mode 100644 index 0000000..3e58c0c --- /dev/null +++ b/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000002-add-columns/down/entrypoint.sql @@ -0,0 +1,2 @@ +ALTER TABLE items DROP COLUMN price; +ALTER TABLE items DROP COLUMN description; diff --git a/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000002-add-columns/up/entrypoint.sql b/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000002-add-columns/up/entrypoint.sql new file mode 100644 index 0000000..5bed408 --- /dev/null +++ b/test/package/migrator/test/postgresql/migrate-multi-file/migration/20250101000002-add-columns/up/entrypoint.sql @@ -0,0 +1,2 @@ +ALTER TABLE items ADD COLUMN description TEXT; +ALTER TABLE items ADD COLUMN price NUMERIC; diff --git a/test/package/migrator/test/postgresql/migrate-multi-file/run.sh b/test/package/migrator/test/postgresql/migrate-multi-file/run.sh new file mode 100644 index 0000000..1877142 --- /dev/null +++ b/test/package/migrator/test/postgresql/migrate-multi-file/run.sh @@ -0,0 +1,84 @@ +#!/bin/dash + +HECTIC_NAMESPACE=test-migrate-multi-file + +log notice "test case: ${WHITE}migrate up with multi-file layout (up/entrypoint.sql)" + +# Initialize migrator +if ! migrator --db-url "$DATABASE_URL" init; then + log error "test failed: ${WHITE}init failed" + exit 1 +fi + +# Apply first migration (up/entrypoint.sql) +if ! migrator --db-url "$DATABASE_URL" migrate up; then + log error "test failed: ${WHITE}first migrate up failed" + exit 1 +fi + +# Verify migration was applied +applied_count=$(psql -Atc "SELECT COUNT(*) FROM hectic.migration" "$DATABASE_URL") +if [ "$applied_count" != "1" ]; then + log error "test failed: ${WHITE}expected 1 migration, got $applied_count" + exit 1 +fi + +# Verify table was created +if ! psql -Atc "SELECT COUNT(*) FROM items" "$DATABASE_URL" >/dev/null 2>&1; then + log error "test failed: ${WHITE}items table not created" + exit 1 +fi + +log info "first migration applied successfully" + +# Apply second migration +if ! migrator --db-url "$DATABASE_URL" migrate up; then + log error "test failed: ${WHITE}second migrate up failed" + exit 1 +fi + +# Verify both columns were added +if ! psql -Atc "SELECT description FROM items LIMIT 0" "$DATABASE_URL" >/dev/null 2>&1; then + log error "test failed: ${WHITE}description column not added" + exit 1 +fi + +if ! psql -Atc "SELECT price FROM items LIMIT 0" "$DATABASE_URL" >/dev/null 2>&1; then + log error "test failed: ${WHITE}price column not added" + exit 1 +fi + +log info "second migration applied successfully" + +# Migrate down one step +if ! migrator --db-url "$DATABASE_URL" migrate down; then + log error "test failed: ${WHITE}migrate down failed" + exit 1 +fi + +# Verify only 1 migration remains +applied_count=$(psql -Atc "SELECT COUNT(*) FROM hectic.migration" "$DATABASE_URL") +if [ "$applied_count" != "1" ]; then + log error "test failed: ${WHITE}expected 1 migration after down, got $applied_count" + exit 1 +fi + +# Verify columns were removed +if psql -Atc "SELECT description FROM items LIMIT 0" "$DATABASE_URL" >/dev/null 2>&1; then + log error "test failed: ${WHITE}description column should be removed" + exit 1 +fi + +# Migrate down to clean state +if ! migrator --db-url "$DATABASE_URL" migrate down; then + log error "test failed: ${WHITE}second migrate down failed" + exit 1 +fi + +# Verify items table was dropped +if psql -Atc "SELECT COUNT(*) FROM items" "$DATABASE_URL" >/dev/null 2>&1; then + log error "test failed: ${WHITE}items table should be dropped" + exit 1 +fi + +log notice "test passed: multi-file migration layout works correctly" diff --git a/test/package/migrator/test/postgresql/migrations-list/migration/20251104192426-multi-file-both/down/entrypoint.sql b/test/package/migrator/test/postgresql/migrations-list/migration/20251104192426-multi-file-both/down/entrypoint.sql new file mode 100644 index 0000000..e69de29 diff --git a/test/package/migrator/test/postgresql/migrations-list/migration/20251104192426-multi-file-both/up/entrypoint.sql b/test/package/migrator/test/postgresql/migrations-list/migration/20251104192426-multi-file-both/up/entrypoint.sql new file mode 100644 index 0000000..e69de29 diff --git a/test/package/migrator/test/postgresql/migrations-list/migration/20251104192428-multi-file-up-only/up/entrypoint.sql b/test/package/migrator/test/postgresql/migrations-list/migration/20251104192428-multi-file-up-only/up/entrypoint.sql new file mode 100644 index 0000000..e69de29 diff --git a/test/package/migrator/test/postgresql/migrations-list/run.sh b/test/package/migrator/test/postgresql/migrations-list/run.sh index 19fb4ae..9dbbdfd 100644 --- a/test/package/migrator/test/postgresql/migrations-list/run.sh +++ b/test/package/migrator/test/postgresql/migrations-list/run.sh @@ -13,7 +13,9 @@ printf '%s' "$result" > result printf '20251004192425-some-changes 20251004292448-some-changes 20251104172425-third-migration +20251104192426-multi-file-both 20251104192427-an-other-one +20251104192428-multi-file-up-only 20251104292469-almoust-last 20251204152446-very-last' > expected @@ -35,8 +37,10 @@ printf '%s' "$result" > result printf '20251004192425-some-changes: missing up.sql down.sql 20251004292448-some-changes -20251104172425-third-migration: missing down.sql -20251104192427-an-other-one: missing down.sql +20251104172425-third-migration: missing down.sql +20251104192426-multi-file-both +20251104192427-an-other-one: missing down.sql +20251104192428-multi-file-up-only: missing down.sql 20251104292469-almoust-last 20251204152446-very-last' > expected diff --git a/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000001-create-items/down/entrypoint.sql b/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000001-create-items/down/entrypoint.sql new file mode 100644 index 0000000..7d571c3 --- /dev/null +++ b/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000001-create-items/down/entrypoint.sql @@ -0,0 +1 @@ +DROP TABLE items; diff --git a/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000001-create-items/up/entrypoint.sql b/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000001-create-items/up/entrypoint.sql new file mode 100644 index 0000000..8a072f4 --- /dev/null +++ b/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000001-create-items/up/entrypoint.sql @@ -0,0 +1,4 @@ +CREATE TABLE items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL +); diff --git a/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000002-add-columns/down/entrypoint.sql b/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000002-add-columns/down/entrypoint.sql new file mode 100644 index 0000000..0fbf717 --- /dev/null +++ b/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000002-add-columns/down/entrypoint.sql @@ -0,0 +1,4 @@ +CREATE TABLE items_backup (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL); +INSERT INTO items_backup SELECT id, name FROM items; +DROP TABLE items; +ALTER TABLE items_backup RENAME TO items; diff --git a/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000002-add-columns/up/entrypoint.sql b/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000002-add-columns/up/entrypoint.sql new file mode 100644 index 0000000..8c66c08 --- /dev/null +++ b/test/package/migrator/test/sqlite/sqlite-multi-file/migration/20250101000002-add-columns/up/entrypoint.sql @@ -0,0 +1,2 @@ +ALTER TABLE items ADD COLUMN description TEXT; +ALTER TABLE items ADD COLUMN price REAL; diff --git a/test/package/migrator/test/sqlite/sqlite-multi-file/run.sh b/test/package/migrator/test/sqlite/sqlite-multi-file/run.sh new file mode 100644 index 0000000..7b1a0c8 --- /dev/null +++ b/test/package/migrator/test/sqlite/sqlite-multi-file/run.sh @@ -0,0 +1,90 @@ +#!/bin/dash + +HECTIC_NAMESPACE=test-sqlite-multi-file + +log notice "test case: ${WHITE}SQLite multi-file migration layout" + +# Create SQLite database +SQLITE_DB="$PWD/test.db" +export DB_URL="sqlite://$SQLITE_DB" + +log info "using SQLite database: $SQLITE_DB" + +# Initialize migrator +if ! migrator --db-url "$DB_URL" init; then + log error "test failed: ${WHITE}init failed for SQLite" + exit 1 +fi + +# Apply first migration (up/entrypoint.sql) +if ! migrator --db-url "$DB_URL" migrate up; then + log error "test failed: ${WHITE}first migration failed" + exit 1 +fi + +# Verify migration was applied +migration_count=$(sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM hectic_migration") +if [ "$migration_count" != "1" ]; then + log error "test failed: ${WHITE}expected 1 migration, got $migration_count" + exit 1 +fi + +# Verify table was created +if ! sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM items" >/dev/null 2>&1; then + log error "test failed: ${WHITE}items table not created" + exit 1 +fi + +log info "first migration applied successfully" + +# Apply second migration +if ! migrator --db-url "$DB_URL" migrate up; then + log error "test failed: ${WHITE}second migration failed" + exit 1 +fi + +# Verify columns were added +if ! sqlite3 "$SQLITE_DB" "SELECT description FROM items LIMIT 0" >/dev/null 2>&1; then + log error "test failed: ${WHITE}description column not added" + exit 1 +fi + +if ! sqlite3 "$SQLITE_DB" "SELECT price FROM items LIMIT 0" >/dev/null 2>&1; then + log error "test failed: ${WHITE}price column not added" + exit 1 +fi + +log info "second migration applied successfully" + +# Migrate down one step +if ! migrator --db-url "$DB_URL" migrate down; then + log error "test failed: ${WHITE}migrate down failed" + exit 1 +fi + +# Verify only 1 migration remains +migration_count=$(sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM hectic_migration") +if [ "$migration_count" != "1" ]; then + log error "test failed: ${WHITE}expected 1 migration after down, got $migration_count" + exit 1 +fi + +# Verify columns were removed (table recreated without extra columns) +if sqlite3 "$SQLITE_DB" "SELECT description FROM items LIMIT 0" >/dev/null 2>&1; then + log error "test failed: ${WHITE}description column should be removed" + exit 1 +fi + +# Migrate down to clean state +if ! migrator --db-url "$DB_URL" migrate down; then + log error "test failed: ${WHITE}second migrate down failed" + exit 1 +fi + +# Verify items table was dropped +if sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM items" >/dev/null 2>&1; then + log error "test failed: ${WHITE}items table should be dropped" + exit 1 +fi + +log notice "test passed: SQLite multi-file migration layout works correctly"