mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-08-12 13:41:32 +00:00
658c99b9bb
A user hit "Could not find the function public.upsert_user_profile(...) in the schema cache" when toggling push notifications. The function was missing, and so were the profiles columns it writes — while schema_migrations listed both 002 and 004 as applied. That pattern turned out to be widespread. Comparing every function declared across the migrations against the live database: 25 are missing, from six migrations all recorded as applied. Their tables exist; only the function bodies are absent. Those files carry "Run this in Supabase SQL Editor" headers, so they were pasted in by hand before apply-migrations.sh existed and a run that stopped partway was still recorded. The runner has skipped them ever since, which is why this stayed invisible until it surfaced as a user-facing error. apply-migrations.sh is not at fault: it wraps each migration and its tracking row in one transaction with ON_ERROR_STOP, so it cannot half-record anything. It inherited a dirty history. Three of the 25 are actually reached by the app, so those are repaired here: apply_for_tier_upgrade MerchantApplication.tsx:213 - tier upgrades were dead check_tier_eligibility called by the above update_p2p_reputation shared/lib/p2p-fiat.ts:803 - reputation never updated This is forward-only repair, not a replay of the source files. Replaying them would abort on their bare CREATE POLICY/INDEX/TRIGGER statements now that the tables exist, and 20241117054602 in particular would overwrite 016's newer cancel_expired_trades with its own older definition. Verified before writing: every table the three functions touch is present, and the whole migration was run inside BEGIN/ROLLBACK against production — three functions created, signatures matching what the callers pass, then rolled back leaving nothing behind. The remaining 22 stay missing on purpose and are listed in known-schema-gaps.txt. Nothing calls them, and several are trigger bodies whose triggers were never created either, so creating them would switch on behaviour that has never run. Also adds a drift check to apply-migrations.sh: after applying, it compares declared functions against the database and flags anything missing that is not in the known-gaps list. Being recorded as applied is not proof of having been applied, and that gap should never again be discovered by a user. The check already earned itself — it caught update_p2p_reputation, which my own first pass had missed to a regex that dropped digits from function names. upsert_user_profile and the profiles notification columns were applied directly to the database from their existing migrations (002, 004), which are fully idempotent; no new migration was needed for them.
124 lines
5.2 KiB
Bash
Executable File
124 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# apply-migrations.sh — idempotent, tracked migration applier for the self-hosted
|
|
# Supabase Postgres. Runs ON the Supabase host (invoked by the deploy-supabase CI
|
|
# job over SSH). Applies every versioned migration in $MIGRATIONS_DIR that is not
|
|
# yet recorded in supabase_migrations.schema_migrations, in ascending version
|
|
# order, each inside a single transaction together with its own tracking-row
|
|
# insert — so a failed migration rolls back cleanly and is never half-recorded.
|
|
#
|
|
# Safe to re-run: already-recorded versions are skipped. Non-versioned files
|
|
# (e.g. COMBINED_*.sql consolidated snapshots) are ignored.
|
|
#
|
|
# Usage: apply-migrations.sh <migrations_dir> [db_container]
|
|
set -euo pipefail
|
|
|
|
MIGRATIONS_DIR="${1:?migrations dir required}"
|
|
DB_CONTAINER="${2:-supabase-db}"
|
|
PSQL=(docker exec -i "$DB_CONTAINER" psql -U postgres -v ON_ERROR_STOP=1 --single-transaction -q)
|
|
PSQL_Q=(docker exec -i "$DB_CONTAINER" psql -U postgres -tAq)
|
|
|
|
echo "▶ apply-migrations: dir=$MIGRATIONS_DIR container=$DB_CONTAINER"
|
|
|
|
# Ensure the tracking schema/table exists (matches the Supabase CLI layout).
|
|
"${PSQL[@]}" >/dev/null <<'SQL'
|
|
CREATE SCHEMA IF NOT EXISTS supabase_migrations;
|
|
CREATE TABLE IF NOT EXISTS supabase_migrations.schema_migrations (
|
|
version text PRIMARY KEY,
|
|
statements text[],
|
|
name text
|
|
);
|
|
SQL
|
|
|
|
# Snapshot the already-applied versions once.
|
|
applied="$("${PSQL_Q[@]}" -c "SELECT version FROM supabase_migrations.schema_migrations;")"
|
|
is_applied() { grep -qxF "$1" <<<"$applied"; }
|
|
|
|
pending=0 done=0
|
|
# Sort by filename so version order == chronological order (14-digit timestamps
|
|
# and zero-padded legacy 0NN prefixes both sort correctly).
|
|
for f in $(ls "$MIGRATIONS_DIR"/*.sql 2>/dev/null | sort); do
|
|
base="$(basename "$f")"
|
|
# Versioned migrations only: <digits>_<name>.sql. Skips COMBINED_*, README, etc.
|
|
if [[ ! "$base" =~ ^([0-9]+)_(.+)\.sql$ ]]; then
|
|
echo " ↷ skip (not a versioned migration): $base"
|
|
continue
|
|
fi
|
|
version="${BASH_REMATCH[1]}"
|
|
name="${BASH_REMATCH[2]}"
|
|
|
|
if is_applied "$version"; then
|
|
continue
|
|
fi
|
|
|
|
pending=$((pending+1))
|
|
if [[ "${DRY_RUN:-}" == "1" ]]; then
|
|
echo " → [dry-run] WOULD apply $version ($name)"
|
|
continue
|
|
fi
|
|
echo " → applying $version ($name)"
|
|
# Migration body + its tracking insert in ONE transaction (--single-transaction):
|
|
# if the body errors, ON_ERROR_STOP aborts and the whole tx (including the insert)
|
|
# rolls back, so the version is never recorded for a failed apply.
|
|
# version is always digits and name always [a-z0-9_] (captured from the filename
|
|
# regex above), so direct single-quoting is safe — no injection surface.
|
|
{
|
|
cat "$f"
|
|
printf "\nINSERT INTO supabase_migrations.schema_migrations(version,name) VALUES ('%s','%s');\n" \
|
|
"$version" "$name"
|
|
} | "${PSQL[@]}"
|
|
echo " ✓ applied and recorded $version"
|
|
done=$((done+1))
|
|
done
|
|
|
|
if [[ $pending -eq 0 ]]; then
|
|
echo "✔ up to date — no pending migrations"
|
|
else
|
|
echo "✔ applied $done migration(s)"
|
|
fi
|
|
|
|
# ── Drift check ───────────────────────────────────────────────────────────────
|
|
# Being recorded as applied is not proof of having been applied. Six migrations
|
|
# carry "Run this in Supabase SQL Editor" headers and were pasted in by hand
|
|
# before this runner existed; at least one run stopped partway — the tables
|
|
# landed, the function bodies did not — and the version was recorded anyway.
|
|
# This runner then skipped them forever, so the gap stayed invisible until a
|
|
# user hit "Could not find the function ..." in production.
|
|
#
|
|
# So after applying, compare what the migrations declare against what the
|
|
# database actually has. Known, accepted gaps live in known-schema-gaps.txt;
|
|
# anything outside that list is new drift and gets surfaced loudly.
|
|
echo "▶ drift check: declared functions vs database"
|
|
|
|
gaps_file="$MIGRATIONS_DIR/../deploy/known-schema-gaps.txt"
|
|
declared="$(grep -rhoiE 'create (or replace )?function (public\.)?[a-z0-9_]+' "$MIGRATIONS_DIR"/*.sql 2>/dev/null \
|
|
| awk '{print tolower($NF)}' | sed 's/^public\.//' | sort -u)"
|
|
present="$("${PSQL_Q[@]}" -c \
|
|
"SELECT p.proname FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'public';" \
|
|
| sort -u)"
|
|
|
|
missing="$(comm -23 <(echo "$declared") <(echo "$present") || true)"
|
|
|
|
if [[ -n "$missing" ]]; then
|
|
if [[ -f "$gaps_file" ]]; then
|
|
known="$(grep -vE '^\s*(#|$)' "$gaps_file" | tr -d ' \t' | sort -u)"
|
|
unexpected="$(comm -23 <(echo "$missing") <(echo "$known") || true)"
|
|
else
|
|
unexpected="$missing"
|
|
fi
|
|
|
|
known_count="$(echo "$missing" | grep -c . || true)"
|
|
new_count="$(echo "$unexpected" | grep -c . || true)"
|
|
|
|
if [[ -n "$unexpected" ]]; then
|
|
echo "::warning::schema drift — $new_count function(s) declared in migrations but absent from the database:"
|
|
echo "$unexpected" | sed 's/^/ ✗ /'
|
|
echo " A migration is recorded as applied but its functions are not there."
|
|
echo " Fix with a forward-only repair migration, or add to known-schema-gaps.txt if intentional."
|
|
else
|
|
echo " ✔ no new drift ($known_count known gap(s), see known-schema-gaps.txt)"
|
|
fi
|
|
else
|
|
echo " ✔ every declared function is present"
|
|
fi
|