#!/usr/bin/env bash set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${repo_root}" project_name="${MINEROPS_ASSET_HISTORY_TEST_PROJECT:-minerops-asset-history-$$}" empty_database="minerops_asset_history_empty" compose=( docker compose --project-name "${project_name}" --env-file .env.dev -f compose.dev.yaml ) cleanup() { status=$? if (( status != 0 )); then echo echo "Asset ID history migration test failed. Recent PostgreSQL logs:" "${compose[@]}" logs --no-color --tail 120 postgres || true fi "${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true exit "${status}" } trap cleanup EXIT if [[ ! -f .env.dev ]]; then echo "Missing .env.dev. Run ./scripts/dev-env-init.sh first." >&2 exit 1 fi run_alembic() { "${compose[@]}" run --rm --no-deps backend alembic "$@" } echo "Starting disposable PostgreSQL..." "${compose[@]}" up --detach postgres for attempt in $(seq 1 60); do if "${compose[@]}" exec -T postgres sh -ec \ 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \ >/dev/null 2>&1; then break fi if (( attempt == 60 )); then echo "PostgreSQL did not become ready." >&2 exit 1 fi sleep 1 done echo "Building migration image..." "${compose[@]}" build backend echo "Upgrading the current head to Asset ID history..." run_alembic upgrade 0008_manual_miner_inventory run_alembic upgrade head current_revision="$(run_alembic current 2>/dev/null)" [[ "${current_revision}" == *"0009_inventory_asset_id_history"* ]] echo "Checking history schema, constraints, foreign keys, and indexes..." "${compose[@]}" exec -T postgres sh -ec \ 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \ <<'SQL' DO $$ DECLARE v_role_id UUID; BEGIN IF to_regclass('public.miner_asset_id_history') IS NULL THEN RAISE EXCEPTION 'Asset ID history table was not created'; END IF; IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'miner_asset_id_history' AND column_name = 'old_asset_id' AND is_nullable = 'YES' ) OR EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'miner_asset_id_history' AND column_name IN ('miner_id', 'new_asset_id', 'changed_at', 'changed_by') AND is_nullable <> 'NO' ) THEN RAISE EXCEPTION 'Asset ID history nullability is incorrect'; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'miner_asset_id_history' AND indexname = 'idx_miner_asset_id_history_miner_changed_at' AND indexdef LIKE '%(miner_id, changed_at, id)%' ) THEN RAISE EXCEPTION 'Chronological miner history index is missing'; END IF; SELECT id INTO v_role_id FROM roles WHERE name = 'Admin'; INSERT INTO users ( id, username, display_name, password_hash, role_id ) VALUES ( '00000000-0000-0000-0000-000000000901', 'asset-history-test', 'Asset History Test', 'not-used', v_role_id ); INSERT INTO miners (id, asset_tag, status) VALUES ( '00000000-0000-0000-0000-000000000902', 'ASSET-OLD', 'manual' ); INSERT INTO miner_asset_id_history ( miner_id, old_asset_id, new_asset_id, changed_at, changed_by ) VALUES ( '00000000-0000-0000-0000-000000000902', 'ASSET-OLD', 'ASSET-NEW', '2026-01-01 00:00:00+00', '00000000-0000-0000-0000-000000000901' ), ( '00000000-0000-0000-0000-000000000902', 'ASSET-NEW', 'ASSET-LATEST', '2026-01-02 00:00:00+00', '00000000-0000-0000-0000-000000000901' ); BEGIN INSERT INTO miner_asset_id_history ( miner_id, old_asset_id, new_asset_id, changed_by ) VALUES ( '00000000-0000-0000-0000-000000000902', ' ASSET-OLD ', 'ASSET-BAD', '00000000-0000-0000-0000-000000000901' ); RAISE EXCEPTION 'Unnormalized old Asset ID was accepted'; EXCEPTION WHEN check_violation THEN NULL; END; BEGIN INSERT INTO miner_asset_id_history ( miner_id, old_asset_id, new_asset_id, changed_by ) VALUES ( '00000000-0000-0000-0000-000000000999', NULL, 'ASSET-NEW', '00000000-0000-0000-0000-000000000901' ); RAISE EXCEPTION 'Unknown miner history was accepted'; EXCEPTION WHEN foreign_key_violation THEN NULL; END; BEGIN INSERT INTO miner_asset_id_history ( miner_id, old_asset_id, new_asset_id, changed_by ) VALUES ( '00000000-0000-0000-0000-000000000902', 'ASSET-OLD', 'ASSET-NEW', NULL ); RAISE EXCEPTION 'History without an actor was accepted'; EXCEPTION WHEN not_null_violation THEN NULL; END; BEGIN DELETE FROM miners WHERE id = '00000000-0000-0000-0000-000000000902'; RAISE EXCEPTION 'Miner with permanent Asset ID history was deleted'; EXCEPTION WHEN foreign_key_violation THEN NULL; END; END $$; SQL echo "Downgrading and re-upgrading Asset ID history..." run_alembic downgrade 0008_manual_miner_inventory table_after_downgrade="$( "${compose[@]}" exec -T postgres sh -ec \ 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At' <<'SQL' SELECT to_regclass('public.miner_asset_id_history'); SQL )" if [[ -n "${table_after_downgrade}" ]]; then echo "Asset ID history table remained after downgrade." >&2 exit 1 fi run_alembic upgrade head echo "Migrating an empty database from base through head..." "${compose[@]}" exec -T postgres sh -ec \ 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d postgres \ -c "CREATE DATABASE minerops_asset_history_empty"' >/dev/null "${compose[@]}" run --rm --no-deps \ -e "POSTGRES_DB=${empty_database}" \ backend alembic upgrade head empty_revision="$( "${compose[@]}" run --rm --no-deps \ -e "POSTGRES_DB=${empty_database}" \ backend alembic current 2>/dev/null )" [[ "${empty_revision}" == *"0009_inventory_asset_id_history"* ]] echo "Asset ID history migration test passed."