#!/usr/bin/env bash # Apply pending SQL migrations to one or both SQLite databases. # Usage: # scripts/migrate.sh # migrates both test.db and posterg.db # scripts/migrate.sh test # migrates storage/test.db only # scripts/migrate.sh prod # migrates storage/posterg.db only set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" APP_DIR="$REPO_ROOT/app" MIGRATIONS_DIR="$APP_DIR/storage/migrations" TEST_DB="$APP_DIR/storage/test.db" PROD_DB="$APP_DIR/storage/posterg.db" # --------------------------------------------------------------------------- # Check whether a migration's effects are already present in the DB so that # legacy databases (created before the migrations table existed) can be # bootstrapped correctly without re-running non-idempotent SQL. # --------------------------------------------------------------------------- already_applied_structurally() { local db="$1" local name="$2" case "$name" in 001_rename_keywords_to_tags.sql) # Effect: table 'tags' and 'thesis_tags' exist count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('tags','thesis_tags');") [ "$count" -eq 2 ] ;; 002_add_visibility.sql) # Effect: access_types seed rows (always safe to re-run with OR IGNORE, but mark done if rows exist) count=$(sqlite3 "$db" "SELECT COUNT(*) FROM access_types WHERE id IN (1,2,3);" 2>/dev/null || echo 0) [ "$count" -eq 3 ] ;; 003_seed_license_types.sql) # Effect: at least one row in license_types count=$(sqlite3 "$db" "SELECT COUNT(*) FROM license_types;" 2>/dev/null || echo 0) [ "$count" -gt 0 ] ;; 004_jury_roles.sql) # Effect: 'role' column on thesis_supervisors count=$(sqlite3 "$db" "SELECT COUNT(*) FROM pragma_table_info('thesis_supervisors') WHERE name='role';") [ "$count" -eq 1 ] ;; 005_add_banner.sql) # Effect: 'banner_path' column on theses count=$(sqlite3 "$db" "SELECT COUNT(*) FROM pragma_table_info('theses') WHERE name='banner_path';") [ "$count" -eq 1 ] ;; 006_add_composite_index.sql) # Effect: index idx_theses_pub_year exists (CREATE INDEX IF NOT EXISTS — safe to re-run anyway) count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_theses_pub_year';") [ "$count" -eq 1 ] ;; 008_formulaire_settings.sql) # Effect: site_settings table exists + show_contact column on authors tbl=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='site_settings';") col=$(sqlite3 "$db" "SELECT COUNT(*) FROM pragma_table_info('authors') WHERE name='show_contact';") [ "$tbl" -eq 1 ] && [ "$col" -eq 1 ] ;; 012_smtp_settings.sql) tbl=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='smtp_settings';") [ "$tbl" -eq 1 ] ;; 013_admin_password.sql) # Effect: admin_password_hash key exists in site_settings (INSERT OR IGNORE — safe to re-run) count=$(sqlite3 "$db" "SELECT COUNT(*) FROM site_settings WHERE key='admin_password_hash';") [ "$count" -eq 1 ] ;; *) # Unknown migration — assume not applied return 1 ;; esac } migrate_db() { local db="$1" local label="$2" # Auto-create from schema only when the file is absent or truly empty (no tables) local table_count table_count=$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table';" 2>/dev/null || echo 0) if [ "$table_count" -eq 0 ]; then echo " [$label] initialising from schema…" sqlite3 "$db" < "$APP_DIR/storage/schema.sql" echo " [$label] schema applied." fi # Ensure tracking table exists sqlite3 "$db" "CREATE TABLE IF NOT EXISTS schema_migrations ( name TEXT PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')) );" local applied=0 local seeded=0 local skipped=0 for migration in "$MIGRATIONS_DIR"/*.sql; do name="$(basename "$migration")" already=$(sqlite3 "$db" "SELECT COUNT(*) FROM schema_migrations WHERE name='$name';") if [ "$already" -eq 1 ]; then skipped=$((skipped + 1)) continue fi # Not in tracking table — check if it was already applied before we started tracking if already_applied_structurally "$db" "$name"; then sqlite3 "$db" "INSERT OR IGNORE INTO schema_migrations (name) VALUES ('$name');" seeded=$((seeded + 1)) echo " [$label] seeded $name (already applied)" continue fi echo " [$label] applying $name …" if sqlite3 "$db" < "$migration"; then sqlite3 "$db" "INSERT OR IGNORE INTO schema_migrations (name) VALUES ('$name');" applied=$((applied + 1)) else echo " [$label] ERROR applying $name — aborting" >&2 exit 1 fi done echo " [$label] done — $applied applied, $seeded seeded, $skipped already up-to-date" } TARGET="${1:-both}" case "$TARGET" in test) migrate_db "$TEST_DB" "test.db" ;; prod) migrate_db "$PROD_DB" "posterg.db" ;; both) migrate_db "$TEST_DB" "test.db" migrate_db "$PROD_DB" "posterg.db" ;; *) echo "Usage: $0 [test|prod|both]" >&2 exit 1 ;; esac