diff --git a/deploy/replicas.env.example b/deploy/replicas.env.example index 03c25cc..3bbf7e2 100644 --- a/deploy/replicas.env.example +++ b/deploy/replicas.env.example @@ -9,3 +9,8 @@ postgresql://USER:PASSWORD@SLAVE_2_IP:5432/superadmin" # Read-only app user on replicas (program-settings API). Grants re-applied after data repair. # REPLICA_APP_DB_USER=montime + +# Publisher DB for pg_dump / count checks. Required when DATABASE_URL password has @ +# (Prisma tolerates it; libpq URI parsing breaks → host "Montime@127.0.0.1"). +# Use postgres superuser on localhost; URL-encode special chars (@ → %40). +# PUBLISHER_DSN="postgresql://postgres:PASS@127.0.0.1:5432/superadmin" diff --git a/deploy/sync-replica-data.sh b/deploy/sync-replica-data.sh index 7dacfb3..2417005 100644 --- a/deploy/sync-replica-data.sh +++ b/deploy/sync-replica-data.sh @@ -45,10 +45,30 @@ if [[ -z "${DATABASE_URL:-}" && -f "$API_DIR/.env" ]]; then set -a && source "$API_DIR/.env" && set +a fi -if [[ -z "${DATABASE_URL:-}" ]]; then - echo "DATABASE_URL is empty (publisher)." >&2 - exit 1 -fi +# Prisma DATABASE_URL often has @ in the password (e.g. Admin@12345) which breaks +# libpq URI parsing → host "Montime@127.0.0.1". Use an explicit publisher DSN. +resolve_publisher_dsn() { + if [[ -n "${PUBLISHER_DSN:-}" ]]; then + echo "$PUBLISHER_DSN" + return 0 + fi + if [[ -n "${PGHOST:-}" && -n "${PGUSER:-}" ]]; then + echo "host=${PGHOST} port=${PGPORT:-5432} dbname=${PGDATABASE:-superadmin} user=${PGUSER} password=${PGPASSWORD:-}" + return 0 + fi + if [[ -n "${DATABASE_URL:-}" ]]; then + local url="${DATABASE_URL%%\?*}" + if [[ "$url" == postgresql://* || "$url" == postgres://* ]]; then + echo "$url" + return 0 + fi + fi + echo "Set PUBLISHER_DSN in deploy/replicas.env (postgresql://…@127.0.0.1:5432/superadmin)." >&2 + echo "Prisma DATABASE_URL alone is not reliable for pg_dump when the password contains @." >&2 + return 1 +} + +PUBLISHER="$(resolve_publisher_dsn)" || exit 1 mapfile -t REPLICAS < <( printf '%s\n' "$REPLICA_DSNS" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' @@ -76,6 +96,12 @@ label() { echo "${host:-?}:${port:-5432}/${db:-?}" } +if ! run_psql "$(libpq_dsn "$PUBLISHER")" -v ON_ERROR_STOP=1 -At -c "SELECT 1" >/dev/null 2>&1; then + echo "Cannot connect to publisher ($(label "$PUBLISHER"))." >&2 + echo "Add PUBLISHER_DSN to deploy/replicas.env — URL-encode @ in passwords as %40." >&2 + exit 1 +fi + table_count() { local dsn="$1" table="$2" run_psql "$(libpq_dsn "$dsn")" -v ON_ERROR_STOP=1 -At -c \ @@ -102,7 +128,7 @@ grant_app_user() { copy_table_from_publisher() { local replica_dsn="$1" table="$2" local pub_dsn - pub_dsn="$(libpq_dsn "$DATABASE_URL")" + pub_dsn="$(libpq_dsn "$PUBLISHER")" pg_dump "$pub_dsn" \ -t "public.\"$table\"" \ --data-only \ @@ -127,7 +153,7 @@ repair_replica() { fi for table in "${SYNC_TABLES[@]}"; do - pub="$(table_count "$DATABASE_URL" "$table")" + pub="$(table_count "$PUBLISHER" "$table")" replica="$(table_count "$replica_dsn" "$table")" if [[ "$pub" != "$replica" ]]; then drifted+=("$table") @@ -155,7 +181,7 @@ repair_replica() { run_psql "$(libpq_dsn "$replica_dsn")" -v ON_ERROR_STOP=1 -q \ -c "TRUNCATE TABLE public.\"$table\" CASCADE" copy_table_from_publisher "$replica_dsn" "$table" - pub="$(table_count "$DATABASE_URL" "$table")" + pub="$(table_count "$PUBLISHER" "$table")" replica="$(table_count "$replica_dsn" "$table")" if [[ "$pub" != "$replica" ]]; then echo " ! repair failed for $table (publisher=$pub replica=$replica)" >&2 @@ -194,7 +220,7 @@ check_replicas() { FROM pg_stat_subscription" 2>/dev/null || echo " (no pg_stat_subscription)" for table in "${SYNC_TABLES[@]}"; do - pub="$(table_count "$DATABASE_URL" "$table")" + pub="$(table_count "$PUBLISHER" "$table")" replica="$(table_count "$dsn" "$table")" if [[ "$pub" == "$replica" ]]; then echo " $table OK ($pub)"