#!/bin/bash
# ==============================================================================
# test_killitems_crash.sh
#
# Reproduce: _bt_killitems() Assert(ItemIdHasStorage) failure on unfixed debug build
#
# Scenario:
# 1. Scanner reads an index leaf page in !so->dropPin mode (e.g. Index-Only Scan)
#    and finds a dead heap tuple, recording it in so->killedItems[].
# 2. Before _bt_killitems() processes the items, scanner pauses at injection point.
# 3. Concurrent bt_merge() merges this page into its right sibling, marking
#    the page BTP_MERGED_AWAY and setting pd_lower=32.
# 4. Scanner wakes up. Without P_ISMERGEDAWAY guard, PageGetMaxOffsetNumber()
#    returns (32-24)/4 = 2, and safemergexid is misinterpreted as ItemIdData.
# 5. PageGetItem() triggers Assert(ItemIdHasStorage(itemId)) in debug builds,
#    causing the backend to abort.
# ==============================================================================
set -e

PG_BIN="$(cd "$(dirname "$0")/install/bin" && pwd)"
PGDATA="$(cd "$(dirname "$0")" && pwd)/test_killitems_data"
PGPORT=5500
export PATH="$PG_BIN:$PATH"

cleanup() {
    pg_ctl -D "$PGDATA" stop -m immediate 2>/dev/null || true
    rm -rf "$PGDATA"
}
trap cleanup EXIT

echo "=== [1/6] Initializing cluster with debug & injection_points ==="
rm -rf "$PGDATA"
initdb -D "$PGDATA" --no-sync --no-instructions -A trust >/dev/null
cat >> "$PGDATA/postgresql.conf" <<CONF
port = $PGPORT
autovacuum = off
shared_preload_libraries = 'injection_points'
CONF

pg_ctl -D "$PGDATA" -l "$PGDATA/logfile" start -w >/dev/null
PSQL="psql -p $PGPORT -d postgres -X -q"

echo "=== [2/6] Preparing test table and index ==="
$PSQL <<'SQL'
CREATE EXTENSION IF NOT EXISTS pageinspect;
CREATE EXTENSION IF NOT EXISTS injection_points;

CREATE TABLE t_crash (val int, pad text);
ALTER TABLE t_crash SET (autovacuum_enabled = false);

INSERT INTO t_crash SELECT i, repeat('x', 100) FROM generate_series(1, 1000) i;
CREATE INDEX t_crash_idx ON t_crash(val);

-- Prune pages so page 2 and page 4 both become sparse merge candidates (< 10% fill)
-- Leave val=400 as the first data tuple on page 2 (itemoffset=2)
DELETE FROM t_crash WHERE (val >= 367 AND val < 400) OR (val > 400 AND val <= 734);
DELETE FROM t_crash WHERE val > 735 AND val < 1000;
VACUUM t_crash;

-- Delete val=400 so it becomes dead in heap, but remains in index
DELETE FROM t_crash WHERE val = 400;

-- Attach injection point at _bt_killitems entry
SELECT injection_points_attach('before_bt_killitems', 'wait');
SQL

echo "=== [3/6] Starting scanner in background (will pause in _bt_killitems) ==="
$PSQL -c "
SET enable_seqscan = false;
SET enable_bitmapscan = false;
SELECT val FROM t_crash WHERE val = 400;
" &
SCAN_PID=$!

sleep 1

echo "=== [4/6] Verifying scanner paused at injection point ==="
WAITING=$($PSQL -t -A -c "
SELECT count(*) FROM pg_stat_activity 
WHERE wait_event_type = 'InjectionPoint' AND wait_event = 'before_bt_killitems';
")
if [ "$WAITING" -eq 0 ]; then
    echo "ERROR: Scanner did not pause at injection point."
    exit 1
fi
echo "  Scanner backend is paused at 'before_bt_killitems'. Good."

echo "=== [5/6] Concurrent session running bt_merge() ==="
MERGES=$($PSQL -t -A -c "SELECT bt_merge('t_crash_idx', 50, 95, 10);")
echo "  bt_merge performed $MERGES merge(s) (Page 2 marked BTP_MERGED_AWAY)."

echo "=== [6/6] Waking up scanner ==="
$PSQL -c "SELECT injection_points_wakeup('before_bt_killitems');" || true

wait $SCAN_PID 2>/dev/null || true

if ! pg_isready -p $PGPORT -q 2>/dev/null; then
    echo ""
    echo "****************************************************************"
    echo ">>> BUG SUCCESSFULLY REPRODUCED! Server crashed as expected! <<<"
    echo "****************************************************************"
    echo ""
    echo "--- PostgreSQL Server Crash Log ---"
    grep -E "TRAP:|Assert|terminating|signal" "$PGDATA/logfile" || tail -n 20 "$PGDATA/logfile"
    echo "-----------------------------------"
    exit 0
else
    echo ""
    echo "RESULT: Server remained alive (Fix is active or bug did not trigger)."
    exit 1
fi
