From 1cc5e09b5f855fdf2c836063b308a09bb1fbca7e Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 18 Aug 2026 12:55:39 +0330 Subject: [PATCH] fix(replicas): parse Prisma DATABASE_URL and dump tables by name Passwords with @ broke libpq URIs; convert to keyword conninfo. pg_dump -t public."table" produced empty dumps so replicas stayed at 0. Co-authored-by: Cursor --- deploy/replicas.env.example | 7 ++-- deploy/sync-replica-data.sh | 73 +++++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/deploy/replicas.env.example b/deploy/replicas.env.example index 3bbf7e2..3c5a92a 100644 --- a/deploy/replicas.env.example +++ b/deploy/replicas.env.example @@ -10,7 +10,6 @@ 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" +# Publisher DB. Prefer this if DATABASE_URL password contains @ (must be URL-encoded here). +# Master on montech-01 listens on 15432: +# PUBLISHER_DSN="postgresql://montime:09128081392%40Montime@127.0.0.1:15432/superadmin" diff --git a/deploy/sync-replica-data.sh b/deploy/sync-replica-data.sh index 2417005..7c2ac20 100644 --- a/deploy/sync-replica-data.sh +++ b/deploy/sync-replica-data.sh @@ -45,8 +45,24 @@ if [[ -z "${DATABASE_URL:-}" && -f "$API_DIR/.env" ]]; then set -a && source "$API_DIR/.env" && set +a 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. +# Prisma DATABASE_URL often has @ in the password (09128081392@Montime). +# libpq URI parsing then treats host as "Montime@127.0.0.1". Convert to keyword form. +uri_to_conninfo() { + local u="${1%%\?*}" + if [[ "$u" != postgresql://* && "$u" != postgres://* ]]; then + echo "$u" + return 0 + fi + local rest="${u#*://}" + if [[ "$rest" =~ ^([^:]+):(.+)@([^:/]+):([0-9]+)/([^/?]+) ]]; then + printf 'host=%s port=%s dbname=%s user=%s password=%s' \ + "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}" "${BASH_REMATCH[5]}" \ + "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + return 0 + fi + echo "$u" +} + resolve_publisher_dsn() { if [[ -n "${PUBLISHER_DSN:-}" ]]; then echo "$PUBLISHER_DSN" @@ -57,14 +73,10 @@ resolve_publisher_dsn() { return 0 fi if [[ -n "${DATABASE_URL:-}" ]]; then - local url="${DATABASE_URL%%\?*}" - if [[ "$url" == postgresql://* || "$url" == postgres://* ]]; then - echo "$url" - return 0 - fi + echo "$DATABASE_URL" + return 0 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 + echo "Set DATABASE_URL or PUBLISHER_DSN (publisher Postgres)." >&2 return 1 } @@ -79,8 +91,7 @@ run_psql() { } libpq_dsn() { - local u="$1" - echo "${u%%\?*}" + uri_to_conninfo "$1" } label() { @@ -125,17 +136,45 @@ grant_app_user() { " 2>/dev/null || true } +grant_replica_writer() { + local dsn="$1" + local user="" + if [[ "$dsn" == *"://"* ]]; then + user="$(sed -E 's#^[a-z]+://([^:/]+).*#\1#' <<< "$dsn")" + else + user="$(sed -nE 's/.*user=([^ ]+).*/\1/p' <<< "$dsn")" + fi + [[ -n "$user" ]] || return 0 + echo " grant write to $user" + run_psql "$(libpq_dsn "$dsn")" -v ON_ERROR_STOP=1 -q -c " + GRANT USAGE ON SCHEMA public TO ${user}; + GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON ALL TABLES IN SCHEMA public TO ${user}; + GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${user}; + " || true +} + copy_table_from_publisher() { local replica_dsn="$1" table="$2" - local pub_dsn - pub_dsn="$(libpq_dsn "$PUBLISHER")" - pg_dump "$pub_dsn" \ - -t "public.\"$table\"" \ + local dump + dump="$(mktemp)" + pg_dump "$(libpq_dsn "$PUBLISHER")" \ + -t "$table" \ --data-only \ --no-owner \ --no-privileges \ + --column-inserts \ --disable-triggers \ - | run_psql "$(libpq_dsn "$replica_dsn")" -v ON_ERROR_STOP=1 -q + >"$dump" + local bytes + bytes="$(wc -c <"$dump" | tr -d ' ')" + echo " dumped $table (${bytes} bytes)" + if [[ "${bytes:-0}" -lt 20 ]]; then + echo " ! pg_dump produced empty output for $table" >&2 + rm -f "$dump" + return 1 + fi + run_psql "$(libpq_dsn "$replica_dsn")" -v ON_ERROR_STOP=1 -f "$dump" + rm -f "$dump" } repair_replica() { @@ -176,6 +215,8 @@ repair_replica() { -c "ALTER SUBSCRIPTION \"${sub}\" DISABLE" done + grant_replica_writer "$replica_dsn" + for table in "${drifted[@]}"; do echo " truncating + copying $table" run_psql "$(libpq_dsn "$replica_dsn")" -v ON_ERROR_STOP=1 -q \