#!/usr/bin/env bash # Reproduce: information_schema.partitions returns UINT64_MAX (18446744073709551615) # for DATA_LENGTH of a recently-dropped partition. # # Root cause: Item_func_internal_data_length::val_int() in sql/item_func.cc # returns (ulonglong)-1 on error without setting null_value = true. # The outer I_S scan sees the partition row via a stale REPEATABLE READ snapshot; # the inner INTERNAL_DATA_LENGTH() call opens a fresh attachable-transaction # snapshot where the partition is already gone → ER_UNKNOWN_PARTITION → UINT64_MAX # leaks into the result set. # # Confirmed affected: Percona Server 8.0.35-27, 8.4.8-8 (and likely all 8.x). # # Reproduction method: guaranteed two-session REPEATABLE READ trick. # Session A locks a consistent snapshot showing p1, then sleeps. # Session B drops p1 (commits) while Session A sleeps. # Session A then queries I_S.PARTITIONS: the outer scan returns a p1 row # (stale snapshot) but INTERNAL_DATA_LENGTH uses a fresh snapshot (p1 gone) # → DATA_LENGTH = UINT64_MAX = 18446744073709551615. # # Usage: # # Auto: spins up a Docker container (default: percona/percona-server:8.0.35): # bash repro_partition_ullong.sh --docker # bash repro_partition_ullong.sh --docker percona/percona-server:8.4 # # # Against an existing MySQL/Percona server (password via env var): # MYSQL_PWD=secret bash repro_partition_ullong.sh --host 127.0.0.1 --user root --db test # # Requirements: mysql client in PATH; docker only needed for --docker mode. set -euo pipefail # ---------- defaults ---------- MODE="direct" HOST="127.0.0.1" PORT="3306" USER="root" DB="test" IMAGE="percona/percona-server:8.0.35" CONTAINER="ps-ullong-repro" DOCKER_ROOT_PW="repro_test_pw" # ephemeral password for the temporary container only usage() { grep '^#' "$0" | grep -v '^#!/' | sed 's/^# \?//' exit 0 } # ---------- argument parsing ---------- while [[ $# -gt 0 ]]; do case "$1" in --host) HOST="$2"; shift 2 ;; --port) PORT="$2"; shift 2 ;; --user) USER="$2"; shift 2 ;; --db) DB="$2"; shift 2 ;; --docker) MODE="docker" shift if [[ $# -gt 0 && "${1:0:2}" != "--" ]]; then IMAGE="$1"; shift fi ;; --help|-h) usage ;; *) echo "Unknown option: $1"; exit 1 ;; esac done # ---------- connection helpers ---------- if [[ "$MODE" == "docker" ]]; then # Uses a local port mapped from the container; password is DOCKER_ROOT_PW (ephemeral). MYSQL_CONNECT="mysql -h127.0.0.1 -P3307 -uroot -p${DOCKER_ROOT_PW} --get-server-public-key" MYSQL_CMD="${MYSQL_CONNECT} ${DB}" else # Password comes from MYSQL_PWD env var (standard MySQL mechanism — not exposed on CLI). MYSQL_CONNECT="mysql -h${HOST} -P${PORT} -u${USER}" MYSQL_CMD="${MYSQL_CONNECT} ${DB}" fi # ---------- Docker setup ---------- docker_start() { echo "==> Pulling ${IMAGE} ..." docker pull "${IMAGE}" echo "==> Starting container ..." docker rm -f "${CONTAINER}" >/dev/null 2>&1 || true docker run -d --name="${CONTAINER}" \ -p 3307:3306 \ -e MYSQL_ROOT_PASSWORD="${DOCKER_ROOT_PW}" \ -e MYSQL_DATABASE="${DB}" \ "${IMAGE}" echo -n "==> Waiting for mysqld ..." for i in $(seq 1 60); do if ${MYSQL_CONNECT} -e "SELECT 1;" >/dev/null 2>&1; then echo " ready."; break fi echo -n "."; sleep 2 done } cleanup_docker() { echo "[cleanup] removing container..." docker rm -f "${CONTAINER}" >/dev/null 2>&1 || true } # ---------- main ---------- TABLE="bugtest_ullong_parts" cleanup_table() { $MYSQL_CMD -e "DROP TABLE IF EXISTS ${TABLE};" 2>/dev/null || true } if [[ "$MODE" == "docker" ]]; then trap cleanup_docker EXIT docker_start else trap cleanup_table EXIT fi echo "==> Server version:" $MYSQL_CMD -e "SELECT VERSION(), @@transaction_isolation;" 2>/dev/null echo "" echo "==> Creating partitioned table ..." $MYSQL_CMD 2>/dev/null < Running two-session REPEATABLE READ race ..." echo " Session A: lock RR snapshot (p1 visible) → SLEEP(5s) → query I_S.PARTITIONS" echo " Session B: DROP PARTITION p1 while Session A sleeps" echo "" RESULT=/tmp/bugtest_ullong_result.txt > "$RESULT" # Session A: lock RR snapshot, sleep (giving Session B time to commit the DROP), # then query I_S — outer scan sees p1 via stale snapshot but INTERNAL_DATA_LENGTH # opens a fresh attachable transaction where p1 is gone → UINT64_MAX. $MYSQL_CMD > "$RESULT" 2>&1 </dev/null echo " p1 DROPPED (committed). Waiting for Session A to finish ..." wait "$SESSION_A_PID" 2>/dev/null || true echo "" echo "==> Session A I_S.PARTITIONS output:" echo "------------------------------------------------------------" grep "^is_query" "$RESULT" | column -t 2>/dev/null || cat "$RESULT" echo "------------------------------------------------------------" UINT64MAX="18446744073709551615" if grep -q "${UINT64MAX}" "$RESULT" 2>/dev/null; then echo "" echo "*** BUG CONFIRMED ***" echo " DATA_LENGTH = ${UINT64MAX}" echo " = (ulonglong)-1 = UINT64_MAX = 2^64-1" echo "" echo " In production queries such as:" echo " SELECT (data_length + index_length) / 1024 / 1024 / 1024 FROM I_S.PARTITIONS" echo " this leaks out as: 17179869183.999999999069" echo "" echo "Root cause: Item_func_internal_data_length::val_int() in sql/item_func.cc" echo " returns (ulonglong)-1 without setting null_value=true when the partition" echo " disappears between the outer DD scan and the inner stat fetch." echo " (Item_func_internal_data_free has the correct null_value guard;" echo " data_length and index_length do not.)" exit 0 else echo "" echo "No UINT64_MAX observed." if grep -q "snapshot_locked" "$RESULT"; then echo "RR snapshot was locked with p1 visible — check the output above." fi echo "Full Session A output:" cat "$RESULT" exit 1 fi