From 72d48c49c3f9f3d05ce5b7cb5f49be6200161f83 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Tue, 31 Mar 2026 16:19:56 +0200 Subject: [PATCH] feat(db): auto-migrate both DBs on serve via scripts/migrate.sh --- TODO.md | 1 + justfile | 15 +++++- scripts/migrate.sh | 124 +++++++++++++++++++++++++++++++++++++++++++++ storage/posterg.db | Bin 229376 -> 237568 bytes storage/test.db | Bin 237568 -> 237568 bytes 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100755 scripts/migrate.sh 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 6c54dad949f24072c392cf3be17707e2a85a60a0..d1e88165c158795c55bb9eda34d838f9afb36714 100644 GIT binary patch delta 818 zcmZo@;A=R*H$hs^h=GB@3W#CAdZLaoqfui*YXW0y0@Kz6=7s#KOu7vGj1~<1c6=GU z8+cA~|KM4`HIw@wR}kk@&N?PjUK2(O##b8~g&6A_^_|$o_4OH>cuNwKa#D+vGg5OC z<8w39ixNvR^Ye-^1T39{TpdGP6+#@Hd|VYUq!l!{6rdn4F*j8q#5E#BAt1=p*D)wk z!P_-b2QHmhP>_?!nwk=ySTg->A)~mgMoMByYDs2ps)l-Ae!04)CRnMaqcfX$sQ&bW z^^6jl7}mubVhDl4hGV;-5cB!mNe(OmYdGtVH ng$SdE2#XxEBTzLi-!n@wTLPt#y{ZHAE0YqlHz%gYQ2Ye|N15@& delta 89 zcmZoTz}L{gH$hs^kb!~00*GP2YNC!YqhVu0YXW0y0@Kz6=7s!Ug0ZddBrvLx| diff --git a/storage/test.db b/storage/test.db index fb97dda608dc7db9333e179e35d2bdcd1fd99d75..a5901c5c6513fc6ad10216fd91949352f871c68b 100644 GIT binary patch delta 1418 zcmaKr&rcIk5Xax^>n?@R9SR8kLKY&H%8zzwA%P2&(gQ&Wfs)wMy3iL~XuD;10Xb-q zgBLx>MKoSaG)58+n#MHIgn(YugT_C=3!FSiOiYYNUkgRrQa9N>%=>)b%zS5dWlLDu z67HRGY$AmE*qeL$*V?^!^VQX-jb|&N2f-eEgOBhAp2K~(12-WC6EF-tsV1=wY8-ar z#8Gvc#tkZ)%L!`OQp4`IKWlBmHe$jwtic=%!YSdqu+1bJLh^!*LoVbU(qf7lr($AZ zis`#NT|FHhZ--YBC10P^+vk%y%Q)1FydiDD5F@&#nue&q-!Q1Y$iBz$PM$+;$QuZV zgW-noYE33I6N zAZ3Ys6>L&Am__glw%`H0fUmFvvv4EjSQ&?8HF4p5Z*8YZFFxMJvKrEc3moI~V*7=1 zRix#R%amn363BK;4g&Se{C$n3k~EfQ@6HHr58!75mY215Ry;54AhuGEK!tudA?&cd z_$(wPo&>xePmiobBJ#8rOK65_Qdx~h==DxxK3X(MpVZ0`iAlP%k|`w~r+O*Unk9Ym z?1H{5>spi=1!OX}cOF9g>{736P#Tf*cDq25Q?y%1FH>}7%UDzmbxMt@=5h(T8>enyvSpJbLPzt^!`vzBHuQeNCyUSoFtArkN&f{TLAec%&Tm0Pnm zlh^&j)NzM(au znAB8bwAC5cmYgnE7IpapeEp<*eLIOywiKn4rJ|6O-mNBI2p&qO>c~(T4yOM!5Zmfs D8eo78 delta 193 zcmZoTz}IkqZ-TVo9tH*mDfBwPkztMf(4rVn?3b+f7s5V!1RZok!w4n0@Hu~HV4LK4on-i3o0KG}Hiym4H|ghy{R{A0+>TUxAlxCIb)CV+MXLz9QBWYy~WttP@$3n71|+&oeXHZ{IA&?846Iu>FTDGe;8ukry$h