feat: migrator: +multifiles migrations
This commit is contained in:
@@ -49,7 +49,6 @@ in {
|
|||||||
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBKPbIJATVyAw7F7vBZbHkCODXFo5gvDyqhuU0gnNUNH''
|
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBKPbIJATVyAw7F7vBZbHkCODXFo5gvDyqhuU0gnNUNH''
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.loader.grub.device = "/dev/vda";
|
|
||||||
boot.initrd.availableKernelModules = [
|
boot.initrd.availableKernelModules = [
|
||||||
"ata_piix"
|
"ata_piix"
|
||||||
"uhci_hcd"
|
"uhci_hcd"
|
||||||
|
|||||||
@@ -431,8 +431,14 @@ ${BGREEN}Examples:
|
|||||||
${BGREEN}Migration File Structure:$NC
|
${BGREEN}Migration File Structure:$NC
|
||||||
migration/
|
migration/
|
||||||
└── 20231201120000-migration-name/
|
└── 20231201120000-migration-name/
|
||||||
├── up.sql - Forward migration
|
├── up.sql - Forward migration (single file)
|
||||||
└── down.sql - Rollback migration
|
└── 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
|
${BGREEN}Migration Naming:$NC
|
||||||
Migrations must follow the format: YYYYMMDDHHMMSS-description
|
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
|
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/<name>/up.sql (single-file)
|
||||||
|
# 2. MIGRATION_DIR/<name>/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(array, name)
|
||||||
index_of() {
|
index_of() {
|
||||||
local list name m i=1
|
local list name m i=1
|
||||||
@@ -790,14 +829,9 @@ migrate() {
|
|||||||
fs_migration=$(printf '%s' "$fs_migrations" | sed -n "${i}p")
|
fs_migration=$(printf '%s' "$fs_migrations" | sed -n "${i}p")
|
||||||
|
|
||||||
escaped_name=$(printf '%s' "$fs_migration" | sed "s/'/''/g")
|
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")
|
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")
|
mig_hash=$(sha256sum "$mig_path")
|
||||||
log info "applying migration ${WHITE}$fs_migration${NC} (up)"
|
log info "applying migration ${WHITE}$fs_migration${NC} (up)"
|
||||||
|
|
||||||
@@ -847,14 +881,9 @@ SQL
|
|||||||
fs_migration=$(printf '%s' "$fs_migrations" | sed -n "${i}p")
|
fs_migration=$(printf '%s' "$fs_migrations" | sed -n "${i}p")
|
||||||
|
|
||||||
escaped_name=$(printf '%s' "$fs_migration" | sed "s/'/''/g")
|
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")
|
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)"
|
log info "reverting migration ${WHITE}$fs_migration${NC} (down)"
|
||||||
|
|
||||||
case "$db_type" in
|
case "$db_type" in
|
||||||
@@ -981,12 +1010,27 @@ list() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
migration_list | while read -r name; do
|
migration_list | while read -r name; do
|
||||||
dir="./${MIGRATION_DIR}/${name}"
|
has_up=true
|
||||||
up="$dir/up.sql"
|
has_down=true
|
||||||
down="$dir/down.sql"
|
|
||||||
|
|
||||||
if [ ! -f "$up" ] || [ ! -f "$down" ]; then
|
if ! has_migration_direction "$name" "up"; then
|
||||||
echo "$name: missing $( [ ! -f "$up" ] && echo up.sql ) $( [ ! -f "$down" ] && echo down.sql )"
|
has_up=false
|
||||||
|
fi
|
||||||
|
if ! has_migration_direction "$name" "down"; then
|
||||||
|
has_down=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
else
|
||||||
echo "$name"
|
echo "$name"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE items;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE items (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE items DROP COLUMN price;
|
||||||
|
ALTER TABLE items DROP COLUMN description;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE items ADD COLUMN description TEXT;
|
||||||
|
ALTER TABLE items ADD COLUMN price NUMERIC;
|
||||||
@@ -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"
|
||||||
@@ -13,7 +13,9 @@ printf '%s' "$result" > result
|
|||||||
printf '20251004192425-some-changes
|
printf '20251004192425-some-changes
|
||||||
20251004292448-some-changes
|
20251004292448-some-changes
|
||||||
20251104172425-third-migration
|
20251104172425-third-migration
|
||||||
|
20251104192426-multi-file-both
|
||||||
20251104192427-an-other-one
|
20251104192427-an-other-one
|
||||||
|
20251104192428-multi-file-up-only
|
||||||
20251104292469-almoust-last
|
20251104292469-almoust-last
|
||||||
20251204152446-very-last' > expected
|
20251204152446-very-last' > expected
|
||||||
|
|
||||||
@@ -36,7 +38,9 @@ printf '%s' "$result" > result
|
|||||||
printf '20251004192425-some-changes: missing up.sql down.sql
|
printf '20251004192425-some-changes: missing up.sql down.sql
|
||||||
20251004292448-some-changes
|
20251004292448-some-changes
|
||||||
20251104172425-third-migration: missing down.sql
|
20251104172425-third-migration: missing down.sql
|
||||||
|
20251104192426-multi-file-both
|
||||||
20251104192427-an-other-one: missing down.sql
|
20251104192427-an-other-one: missing down.sql
|
||||||
|
20251104192428-multi-file-up-only: missing down.sql
|
||||||
20251104292469-almoust-last
|
20251104292469-almoust-last
|
||||||
20251204152446-very-last' > expected
|
20251204152446-very-last' > expected
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE items;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE items (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE items ADD COLUMN description TEXT;
|
||||||
|
ALTER TABLE items ADD COLUMN price REAL;
|
||||||
90
test/package/migrator/test/sqlite/sqlite-multi-file/run.sh
Normal file
90
test/package/migrator/test/sqlite/sqlite-multi-file/run.sh
Normal file
@@ -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"
|
||||||
Reference in New Issue
Block a user