Files
MH-DashBoard-organization/scripts/sync_prod_db_to_dev.sh

155 lines
4.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROD_DIR="${ROOT_DIR}"
DEV_DIR="${DEV_DIR:-/tmp/mh-dashboard-organization-dev}"
SCOPE="${1:-minimal}"
if [[ ! -f "${PROD_DIR}/docker-compose.yml" ]]; then
echo "Production workspace not found: ${PROD_DIR}" >&2
exit 1
fi
if [[ ! -f "${DEV_DIR}/docker-compose.yml" ]]; then
echo "Development workspace not found: ${DEV_DIR}" >&2
echo "Set DEV_DIR=/path/to/dev-copy if the dev workspace moved." >&2
exit 1
fi
case "${SCOPE}" in
minimal)
TABLES=(
member_aliases
member_overrides
member_retirements
members
seat_maps
seat_slots
)
;;
full)
TABLES=(
integration_import_batches
integration_raw_organization_rows
integration_raw_mh_rows
integration_raw_mh_pm_rows
integration_raw_payment_rows
integration_project_aliases
integration_project_category_mappings
integration_project_pm_assignments
integration_projects
integration_work_logs
integration_work_log_segments
integration_vouchers
member_aliases
member_overrides
member_retirements
members
seat_maps
seat_slots
)
;;
*)
echo "Usage: $0 [minimal|full]" >&2
exit 1
;;
esac
PROD_COMPOSE=(docker compose --project-directory "${PROD_DIR}")
DEV_COMPOSE=(docker compose --project-directory "${DEV_DIR}")
require_service() {
local dir="$1"
shift
(cd "${dir}" && "$@") >/dev/null
}
echo "[1/6] Checking source and target stacks"
require_service "${PROD_DIR}" "${PROD_COMPOSE[@]}" ps
require_service "${DEV_DIR}" "${DEV_COMPOSE[@]}" ps
echo "[2/6] Ensuring db containers are reachable"
(cd "${PROD_DIR}" && "${PROD_COMPOSE[@]}" exec -T db pg_isready -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}") >/dev/null
(cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db pg_isready -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}") >/dev/null
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "${WORK_DIR}"
}
trap cleanup EXIT
DUMP_FILE="${WORK_DIR}/prod_to_dev_${SCOPE}.sql"
TRUNCATE_FILE="${WORK_DIR}/truncate_${SCOPE}.sql"
SEAT_POSITIONS_FILE="${WORK_DIR}/seat_positions.csv"
echo "[3/6] Building truncate script for ${SCOPE} scope"
{
echo "BEGIN;"
echo "SET session_replication_role = replica;"
printf 'TRUNCATE TABLE %s RESTART IDENTITY CASCADE;\n' "$(IFS=,; echo "${TABLES[*]}")"
echo "SET session_replication_role = DEFAULT;"
echo "COMMIT;"
} > "${TRUNCATE_FILE}"
echo "[4/6] Dumping ${SCOPE} data from 8080 source DB"
TABLE_ARGS=()
for table in "${TABLES[@]}"; do
TABLE_ARGS+=(-t "public.${table}")
done
(cd "${PROD_DIR}" && "${PROD_COMPOSE[@]}" exec -T db \
pg_dump -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \
--data-only --column-inserts --disable-triggers --no-owner --no-privileges \
"${TABLE_ARGS[@]}") > "${DUMP_FILE}"
echo "[4.5/6] Exporting seat_positions in portable format"
(cd "${PROD_DIR}" && "${PROD_COMPOSE[@]}" exec -T db \
psql -At -F ',' -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \
-c "COPY (
SELECT member_id, seat_map_id, seat_slot_id, row_index, col_index, seat_label, updated_at
FROM public.seat_positions
ORDER BY member_id
) TO STDOUT WITH CSV") > "${SEAT_POSITIONS_FILE}"
echo "[5/6] Truncating target tables in 8081 dev DB"
(cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db \
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}") < "${TRUNCATE_FILE}"
echo "[6/6] Restoring dumped data into 8081 dev DB"
(cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db \
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}") < "${DUMP_FILE}"
echo "[6.5/6] Restoring portable seat_positions and rebuilding auth users"
(cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db \
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \
-c "DELETE FROM public.seat_positions")
(cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db \
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \
-c "COPY public.seat_positions (member_id, seat_map_id, seat_slot_id, row_index, col_index, seat_label, updated_at) FROM STDIN WITH CSV") < "${SEAT_POSITIONS_FILE}"
(cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T backend python - <<'PY'
from backend.app.main import get_conn, sync_auth_users_from_members
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute("UPDATE members SET seat_label = ''")
cur.execute(
"""
UPDATE members AS m
SET seat_label = sp.seat_label
FROM seat_positions AS sp
WHERE sp.member_id = m.id
"""
)
sync_auth_users_from_members(cur)
conn.commit()
print("members, seat labels, and auth users synced")
PY
)
echo
echo "Sync complete."
echo "Source: ${PROD_DIR} (8080)"
echo "Target: ${DEV_DIR} (8081)"
echo "Scope : ${SCOPE}"