#!/usr/bin/env bash # Validate the contracts-v1 PostgreSQL migration without touching any schema # other than th_hotel_booking. This script is safe by default: it refuses a # schema containing objects outside the booking_contract_* namespace. set -euo pipefail readonly BOOKING_SCHEMA='th_hotel_booking' readonly MIGRATION_FILES=( 'server/src/main/resources/db/booking-postgresql/migration/V1__booking_contracts_v1.sql' 'server/src/main/resources/db/booking-postgresql/migration/V2__booking_sender_mapping_direction.sql' ) database_url="${BOOKING_PG_VERIFY_URL:-${TH_HOTEL_BOOKING_PG_URL:-}}" if [[ -z "${database_url}" ]]; then echo 'BOOKING_PG_VERIFY_URL (or TH_HOTEL_BOOKING_PG_URL) is required.' >&2 exit 64 fi if [[ "${BOOKING_PG_VERIFY_SCHEMA:-${BOOKING_SCHEMA}}" != "${BOOKING_SCHEMA}" ]]; then echo "Only ${BOOKING_SCHEMA} may be verified by this script." >&2 exit 64 fi for migration_file in "${MIGRATION_FILES[@]}"; do if [[ ! -f "${migration_file}" ]]; then echo "Canonical booking migration was not found: ${migration_file}" >&2 exit 66 fi done if ! command -v psql >/dev/null 2>&1; then echo 'psql is required to run PostgreSQL migration verification.' >&2 exit 69 fi psql_query() { psql -X -qAt -v ON_ERROR_STOP=1 -d "${database_url}" -c "$1" } schema_exists="$(psql_query "SELECT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = '${BOOKING_SCHEMA}');")" if [[ "${schema_exists}" == 't' ]]; then unexpected_objects="$(psql_query " SELECT count(*) FROM pg_class relation JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace WHERE namespace.nspname = '${BOOKING_SCHEMA}' AND relation.relkind IN ('r', 'p', 'S', 'v', 'm') AND relation.relname NOT LIKE 'booking_contract_%' AND relation.relname <> 'flyway_schema_history';")" if [[ "${unexpected_objects}" != '0' ]]; then echo "Refusing to use ${BOOKING_SCHEMA}: it contains ${unexpected_objects} non-contract object(s)." >&2 exit 65 fi fi # Count user objects outside the project schema. The migration is allowed to # create only th_hotel_booking objects; a count change is a hard failure. other_objects_before="$(psql_query " SELECT count(*) FROM pg_class relation JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace WHERE namespace.nspname NOT IN ('pg_catalog', 'information_schema', '${BOOKING_SCHEMA}') AND namespace.nspname NOT LIKE 'pg_toast%' AND relation.relkind IN ('r', 'p', 'S', 'v', 'm');")" psql -X -q --single-transaction -v ON_ERROR_STOP=1 -d "${database_url}" \ -f "${MIGRATION_FILES[0]}" \ -f "${MIGRATION_FILES[1]}" other_objects_after="$(psql_query " SELECT count(*) FROM pg_class relation JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace WHERE namespace.nspname NOT IN ('pg_catalog', 'information_schema', '${BOOKING_SCHEMA}') AND namespace.nspname NOT LIKE 'pg_toast%' AND relation.relkind IN ('r', 'p', 'S', 'v', 'm');")" if [[ "${other_objects_before}" != "${other_objects_after}" ]]; then echo 'PostgreSQL migration verification detected a non-booking schema object change.' >&2 exit 70 fi required_tables=( booking_contract_source_message booking_contract_source_attachment booking_contract_processing_run booking_contract_processing_attempt booking_contract_evidence_reference booking_contract_artifact booking_contract_parsed_fact booking_contract_candidate_target booking_contract_confirmation_action booking_contract_channel_profile booking_contract_sender_mapping booking_contract_rate_code_mapping ) for table_name in "${required_tables[@]}"; do present="$(psql_query "SELECT to_regclass('${BOOKING_SCHEMA}.${table_name}') IS NOT NULL;")" if [[ "${present}" != 't' ]]; then echo "Missing required booking table: ${BOOKING_SCHEMA}.${table_name}" >&2 exit 71 fi done cross_schema_fk_count="$(psql_query " SELECT count(*) FROM pg_constraint constraint JOIN pg_class source_relation ON source_relation.oid = constraint.conrelid JOIN pg_namespace source_namespace ON source_namespace.oid = source_relation.relnamespace JOIN pg_class target_relation ON target_relation.oid = constraint.confrelid JOIN pg_namespace target_namespace ON target_namespace.oid = target_relation.relnamespace WHERE constraint.contype = 'f' AND (source_namespace.nspname = '${BOOKING_SCHEMA}' OR target_namespace.nspname = '${BOOKING_SCHEMA}') AND source_namespace.nspname <> target_namespace.nspname;")" if [[ "${cross_schema_fk_count}" != '0' ]]; then echo 'Booking PostgreSQL migration created or found a cross-schema foreign key.' >&2 exit 72 fi echo "Booking PostgreSQL migration verified in ${BOOKING_SCHEMA}; other schema object count unchanged (${other_objects_after})."