199 lines
8.7 KiB
Bash
Executable File
199 lines
8.7 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
|
|
)
|
|
;;
|
|
analysis)
|
|
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
|
|
)
|
|
;;
|
|
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|analysis|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"
|
|
SEQUENCE_FIX_FILE="${WORK_DIR}/sequence_fix.sql"
|
|
AUTH_SYNC_FILE="${WORK_DIR}/auth_sync.py"
|
|
|
|
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}" >/dev/null) < "${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}" >/dev/null) < "${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" >/dev/null)
|
|
(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" >/dev/null) < "${SEAT_POSITIONS_FILE}"
|
|
cat > "${AUTH_SYNC_FILE}" <<'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
|
|
(cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T backend python -) < "${AUTH_SYNC_FILE}"
|
|
|
|
echo "[6.8/6] Resetting serial sequences"
|
|
{
|
|
echo "SELECT setval(pg_get_serial_sequence('public.members', 'id'), COALESCE((SELECT MAX(id) FROM public.members), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.member_aliases', 'id'), COALESCE((SELECT MAX(id) FROM public.member_aliases), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.member_overrides', 'id'), COALESCE((SELECT MAX(id) FROM public.member_overrides), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.member_retirements', 'id'), COALESCE((SELECT MAX(id) FROM public.member_retirements), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.seat_maps', 'id'), COALESCE((SELECT MAX(id) FROM public.seat_maps), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.seat_slots', 'id'), COALESCE((SELECT MAX(id) FROM public.seat_slots), 1), true);"
|
|
if [[ "${SCOPE}" == "analysis" || "${SCOPE}" == "full" ]]; then
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_import_batches', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_import_batches), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_organization_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_organization_rows), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_mh_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_mh_rows), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_mh_pm_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_mh_pm_rows), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_raw_payment_rows', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_raw_payment_rows), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_project_aliases', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_project_aliases), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_project_category_mappings', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_project_category_mappings), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_project_pm_assignments', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_project_pm_assignments), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_projects', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_projects), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_work_logs', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_work_logs), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_work_log_segments', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_work_log_segments), 1), true);"
|
|
echo "SELECT setval(pg_get_serial_sequence('public.integration_vouchers', 'id'), COALESCE((SELECT MAX(id) FROM public.integration_vouchers), 1), true);"
|
|
fi
|
|
} > "${SEQUENCE_FIX_FILE}"
|
|
(cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db \
|
|
psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" >/dev/null) < "${SEQUENCE_FIX_FILE}"
|
|
|
|
echo
|
|
echo "Sync complete."
|
|
echo "Source: ${PROD_DIR} (8080)"
|
|
echo "Target: ${DEV_DIR} (8081)"
|
|
echo "Scope : ${SCOPE}"
|