Testing Migrations
This commit is contained in:
1 parent
d9abeb7fb1
commit
9804771a5b
8 files changed
+1109
No files matched your search
Executable
+225
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Apply database migrations. Run from the repo root, INSIDE nix-shell (that is
|
||||
# where the prisma CLI lives).
|
||||
#
|
||||
# nix-shell shell.nix --run ./migrateDatabase.sh
|
||||
#
|
||||
# Works both ways round: it applies pending migrations to an existing database,
|
||||
# and builds a complete one from empty. Safe to re-run -- a second run is a
|
||||
# no-op. Replaces migrateDatabase.sh.
|
||||
#
|
||||
# --- why this is a separate step -----------------------------------------
|
||||
#
|
||||
# The application connects as a least-privilege account (production uses `app`)
|
||||
# holding DML rights only -- no CREATE, ALTER or DROP. That is deliberate: the
|
||||
# server fetches untrusted URLs for link previews, and a service able to reshape
|
||||
# its own tables is a far larger blast radius than that feature warrants.
|
||||
#
|
||||
# So the app never applies DDL. It verifies the schema at boot
|
||||
# (server/helpers/Schema.js) and disables link previews if anything is missing.
|
||||
#
|
||||
# --- authoring a new migration -------------------------------------------
|
||||
#
|
||||
# NEVER run `prisma migrate dev`. It can decide the database has drifted and
|
||||
# offer to RESET it, which on a box holding a production data restore would
|
||||
# destroy real data. It also wants a shadow database. Instead:
|
||||
#
|
||||
# 1. Edit prisma/schema.prisma
|
||||
# 2. NAME=add_whatever
|
||||
# DIR="prisma/migrations/$(date +%Y%m%d%H%M%S)_$NAME"
|
||||
# mkdir -p "$DIR"
|
||||
# prisma migrate diff --from-config-datasource \
|
||||
# --to-schema prisma/schema.prisma --script > "$DIR/migration.sql"
|
||||
# 3. READ the generated SQL -- this review is what makes the approach safe
|
||||
# 4. ./migrateDatabase.sh
|
||||
#
|
||||
# Note: Prisma emits `COLLATE utf8mb4_unicode_ci`, but this database is
|
||||
# `utf8mb4_general_ci` throughout, and `prisma migrate diff` does NOT report the
|
||||
# difference. Fix the collation in any generated migration by hand. This script
|
||||
# checks for drift at the end.
|
||||
#
|
||||
# --- credentials ---------------------------------------------------------
|
||||
#
|
||||
# Same convention as applyLastBackup.sh / backupDatabase.sh: source ~/.env and
|
||||
# connect as root with $PROD_DB_PASS. Falls back to passwordless root (the dev
|
||||
# case, where shell.nix also exports DATABASE_URL). Override with:
|
||||
#
|
||||
# DB_ADMIN_USER=admin DB_ADMIN_PASS=secret ./migrateDatabase.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$REPO"
|
||||
|
||||
DB_NAME="application" # hardcoded in server/config/database.js too
|
||||
|
||||
# --- preconditions -------------------------------------------------------
|
||||
|
||||
if ! command -v prisma >/dev/null 2>&1; then
|
||||
echo "prisma is not on PATH -- run this inside nix-shell:" >&2
|
||||
echo " nix-shell shell.nix --run ./migrateDatabase.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v mariadb >/dev/null 2>&1 && ! command -v mysql >/dev/null 2>&1; then
|
||||
echo "no mariadb/mysql client on PATH -- run this inside nix-shell." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v mariadb >/dev/null 2>&1; then MYSQL_BIN="mariadb"; else MYSQL_BIN="mysql"; fi
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "node is not on PATH -- needed to percent-encode the database password." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.env" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$HOME/.env"
|
||||
fi
|
||||
|
||||
ADMIN_USER="${DB_ADMIN_USER:-root}"
|
||||
ADMIN_PASS="${DB_ADMIN_PASS:-${PROD_DB_PASS:-}}"
|
||||
|
||||
mysql_args=(--user "$ADMIN_USER")
|
||||
[ -n "$ADMIN_PASS" ] && mysql_args+=(--password="$ADMIN_PASS")
|
||||
|
||||
LOCAL_SOCKET="$REPO/.mysql/mysql.sock"
|
||||
[ -S "$LOCAL_SOCKET" ] && mysql_args+=(--socket="$LOCAL_SOCKET")
|
||||
|
||||
# Prisma needs a URL, and it must carry the ADMIN credentials -- never the
|
||||
# app account, which holds no DDL.
|
||||
DB_ADMIN_HOST="${DB_ADMIN_HOST:-127.0.0.1}"
|
||||
DB_ADMIN_PORT="${DB_ADMIN_PORT:-3306}"
|
||||
|
||||
#
|
||||
# Percent-encode the password. Production passwords routinely contain @ : / ? #
|
||||
# or %, every one of which silently corrupts a URL -- an unencoded `@` would
|
||||
# truncate the host and produce a baffling connection error rather than an
|
||||
# obvious one.
|
||||
#
|
||||
encode_component() {
|
||||
node -e 'process.stdout.write(encodeURIComponent(process.argv[1] || ""))' "$1"
|
||||
}
|
||||
|
||||
ADMIN_USER_ENC="$(encode_component "$ADMIN_USER")"
|
||||
|
||||
if [ -n "$ADMIN_PASS" ]; then
|
||||
ADMIN_PASS_ENC="$(encode_component "$ADMIN_PASS")"
|
||||
export DATABASE_URL="mysql://${ADMIN_USER_ENC}:${ADMIN_PASS_ENC}@${DB_ADMIN_HOST}:${DB_ADMIN_PORT}/${DB_NAME}"
|
||||
else
|
||||
export DATABASE_URL="mysql://${ADMIN_USER_ENC}@${DB_ADMIN_HOST}:${DB_ADMIN_PORT}/${DB_NAME}"
|
||||
fi
|
||||
|
||||
# Keep the CLI from phoning checkpoint.prisma.io for update checks.
|
||||
export CHECKPOINT_DISABLE=1
|
||||
|
||||
if ! "$MYSQL_BIN" "${mysql_args[@]}" -e "SELECT 1" >/dev/null 2>&1; then
|
||||
echo "Cannot connect to the database as '$ADMIN_USER'." >&2
|
||||
echo "Set PROD_DB_PASS in ~/.env, or pass DB_ADMIN_USER / DB_ADMIN_PASS." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Collation is explicit because the server default is utf8mb4_unicode_ci while
|
||||
# every application table is utf8mb4_general_ci. A table created later without
|
||||
# an explicit COLLATE would otherwise inherit the wrong one, changing string
|
||||
# comparison and FULLTEXT ranking -- and Prisma neither models collation nor
|
||||
# reports the difference in `migrate diff`.
|
||||
#
|
||||
# For a database that already exists with the wrong default, the one-time
|
||||
# non-destructive fix is:
|
||||
# ALTER DATABASE application CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
#
|
||||
"$MYSQL_BIN" "${mysql_args[@]}" -e "CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` \
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"
|
||||
|
||||
sql() { "$MYSQL_BIN" "${mysql_args[@]}" -N -B "$DB_NAME" -e "$1" 2>/dev/null; }
|
||||
|
||||
echo '-------'
|
||||
echo "Migrating '$DB_NAME' as '$ADMIN_USER'"
|
||||
echo '-------'
|
||||
|
||||
# --- baseline an existing, pre-Prisma database ---------------------------
|
||||
#
|
||||
# This is the single most dangerous moment in adopting Prisma. A database that
|
||||
# already has the schema but no _prisma_migrations table must have the baseline
|
||||
# marked applied, NOT executed -- running 0_init against it would fail on
|
||||
# existing tables. Detect and handle it here rather than leaving it as a manual
|
||||
# step someone forgets.
|
||||
#
|
||||
HAS_MIGRATION_TABLE=$(sql "SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA='$DB_NAME' AND TABLE_NAME='_prisma_migrations'" || echo 0)
|
||||
HAS_EXISTING_SCHEMA=$(sql "SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA='$DB_NAME' AND TABLE_NAME='attachment'" || echo 0)
|
||||
|
||||
if [ "${HAS_MIGRATION_TABLE:-0}" -eq 0 ] && [ "${HAS_EXISTING_SCHEMA:-0}" -gt 0 ]; then
|
||||
echo "Existing schema found with no migration history -- baselining."
|
||||
echo "Marking 0_init as already applied (NOT executing it)."
|
||||
prisma migrate resolve --applied 0_init
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# --- apply ---------------------------------------------------------------
|
||||
|
||||
prisma migrate deploy
|
||||
|
||||
# --- seed ----------------------------------------------------------------
|
||||
|
||||
if [ -f prisma/seed.sql ]; then
|
||||
echo ""
|
||||
echo "Applying reference data (prisma/seed.sql)..."
|
||||
"$MYSQL_BIN" "${mysql_args[@]}" "$DB_NAME" < prisma/seed.sql
|
||||
fi
|
||||
|
||||
# --- verify --------------------------------------------------------------
|
||||
#
|
||||
# Print proof rather than leaving the operator to trust us.
|
||||
#
|
||||
echo ""
|
||||
echo '-------'
|
||||
echo 'Verification'
|
||||
echo '-------'
|
||||
|
||||
prisma migrate status
|
||||
|
||||
echo ""
|
||||
"$MYSQL_BIN" "${mysql_args[@]}" "$DB_NAME" --table -e "
|
||||
SELECT 'attachment FULLTEXT index' AS check_name,
|
||||
IFNULL(MAX(INDEX_TYPE), 'MISSING') AS result
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND INDEX_NAME = 'attachment_text_index'
|
||||
UNION ALL
|
||||
SELECT 'tables',
|
||||
CAST(COUNT(*) AS CHAR) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME <> '_prisma_migrations'
|
||||
UNION ALL
|
||||
SELECT 'attachment_type seed rows',
|
||||
CAST(COUNT(*) AS CHAR) FROM attachment_type
|
||||
UNION ALL
|
||||
-- _prisma_migrations is Prisma's own bookkeeping table and is created with
|
||||
-- its default collation; only application tables are checked here.
|
||||
SELECT 'non-general_ci app tables (expect 0)',
|
||||
CAST(COUNT(*) AS CHAR) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME <> '_prisma_migrations'
|
||||
AND TABLE_COLLATION <> 'utf8mb4_general_ci';
|
||||
"
|
||||
|
||||
# The FULLTEXT index backs Attachment.textSearch. If a migration ever converts
|
||||
# it to a BTREE index, attachment search silently returns nothing -- so fail
|
||||
# loudly here rather than letting that ship.
|
||||
FT=$(sql "SELECT IFNULL(MAX(INDEX_TYPE),'MISSING') FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA='$DB_NAME' AND INDEX_NAME='attachment_text_index'")
|
||||
if [ "$FT" != "FULLTEXT" ]; then
|
||||
echo "FAILED: attachment_text_index is '$FT', expected FULLTEXT." >&2
|
||||
echo "Attachment search depends on MATCH..AGAINST and is now broken." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo '-------'
|
||||
echo 'Done. Restart the API so it re-verifies the schema:'
|
||||
echo ' pm2 restart NoteServer'
|
||||
echo '-------'
|
||||
Reference in new issue
Block a user