diff --git a/TODO.md b/TODO.md index b538191..b96a404 100644 --- a/TODO.md +++ b/TODO.md @@ -2,5 +2,6 @@ ## Fixes - [x] Fix CSV import UNIQUE constraint crash: skip rows whose identifier already exists in DB +- [x] Auto-migrate both test.db and posterg.db on `just serve` via scripts/migrate.sh - [x] Fix wrong `require_once` depth in `public/admin/actions/page.php` (`../../` → `../../../`) - [x] Fix same path depth bug in `formulaire.php` and `publish.php` diff --git a/justfile b/justfile index 4f7c4bd..10e1488 100644 --- a/justfile +++ b/justfile @@ -12,7 +12,7 @@ setup: @bash scripts/setup-dev.sh [group('dev')] -serve: +serve: migrate @php -S 127.0.0.1:8000 -t public/ [group('dev')] @@ -110,6 +110,19 @@ syntax: # Database # ============================================================================ +[group('database')] +migrate: + @echo "Running migrations…" + @bash scripts/migrate.sh both + +[group('database')] +migrate-test: + @bash scripts/migrate.sh test + +[group('database')] +migrate-prod: + @bash scripts/migrate.sh prod + [group('database')] init-db: @sqlite3 storage/test.db < storage/schema.sql diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100755 index 0000000..fe2c4b6 --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,124 @@ +#!/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)" +MIGRATIONS_DIR="$REPO_ROOT/storage/migrations" +TEST_DB="$REPO_ROOT/storage/test.db" +PROD_DB="$REPO_ROOT/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 ] + ;; + *) + # Unknown migration — assume not applied + return 1 + ;; + esac +} + +migrate_db() { + local db="$1" + local label="$2" + + if [ ! -f "$db" ]; then + echo " [$label] database not found, skipping: $db" + return + 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 diff --git a/storage/posterg.db b/storage/posterg.db index 6c54dad..d1e8816 100644 Binary files a/storage/posterg.db and b/storage/posterg.db differ diff --git a/storage/test.db b/storage/test.db index fb97dda..a5901c5 100644 Binary files a/storage/test.db and b/storage/test.db differ