3 # Test suite for pg_scribe chain transfer operations
5 # This test suite verifies proper behavior when transferring streaming
6 # between chains using --new-chain --start
9 # 1. --status correctly identifies which chain is actively streaming
10 # 2. --new-chain --start seals old chain's active.sql before transferring
15 # Colors for test output
20 NC='\033[0m' # No Color
23 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
24 PG_SCRIBE="$SCRIPT_DIR/scripts/pg_scribe"
25 TEST_DIR="/tmp/pg_scribe_chain_transfer_test_$$"
26 TEST_DB_PREFIX="pg_scribe_chain_$$"
27 PGUSER="${PGUSER:-postgres}"
35 DATABASES_TO_CLEANUP=()
43 echo -e "${BLUE}TEST:${NC} $*"
47 echo -e "${GREEN}PASS:${NC} $*"
52 echo -e "${RED}FAIL:${NC} $*"
57 echo -e "${YELLOW}INFO:${NC} $*"
67 psql -U "$PGUSER" -d "$dbname" -tAq "$@"
73 run_psql "$dbname" -c "$query" 2>/dev/null || true
78 log_info "Creating test database: $dbname"
81 psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
84 psql -U "$PGUSER" -d postgres -c "CREATE DATABASE $dbname;" &>/dev/null
86 DATABASES_TO_CLEANUP+=("$dbname")
89 # shellcheck disable=SC2317 # Function called from cleanup trap handler
92 log_info "Dropping test database: $dbname"
94 # Terminate connections
95 psql -U "$PGUSER" -d postgres -c "
96 SELECT pg_terminate_backend(pid)
98 WHERE datname = '$dbname' AND pid <> pg_backend_pid();
102 psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
105 create_table_with_pk() {
109 CREATE TABLE $table (
110 id SERIAL PRIMARY KEY,
112 created_at TIMESTAMP DEFAULT now()
117 # Initialize a backup directory (creates replication slot and initial backups)
118 init_backup_system() {
120 local backup_dir="$2"
123 mkdir -p "$backup_dir"
124 "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null
126 SLOTS_TO_CLEANUP+=("$dbname:$slot")
129 # Start streaming in background, return PID
132 local backup_dir="$2"
134 "$PG_SCRIBE" --start -d "$dbname" -f "$backup_dir" -U "$PGUSER" &
137 # Wait for streaming to start
143 # Stop streaming process
145 local backup_dir="$1"
147 "$PG_SCRIBE" --stop -f "$backup_dir" &>/dev/null || true
150 # Get the chain ID that pg_scribe --status reports as active
151 get_status_active_chain() {
153 local backup_dir="$2"
157 status_output=$("$PG_SCRIBE" --status -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" 2>&1)
159 # Extract chain ID from line like " chain-20251020T050048Z (ACTIVE - streaming)"
160 echo "$status_output" | grep -oP 'chain-\K[0-9TZ]+(?=.*ACTIVE.*streaming)' || echo ""
163 # Get the chain ID that the PID is actually writing to
164 get_actual_active_chain() {
167 # Check which active.sql file the process has open
169 active_file=$(ls -l /proc/"$pid"/fd/ 2>/dev/null | grep -oP '/tmp/.*?/chain-\K[0-9TZ]+(?=/active\.sql)' || echo "")
178 test_status_reports_correct_active_chain() {
180 log_test "--status should report the chain that is actually streaming"
182 local dbname="${TEST_DB_PREFIX}_status"
183 local backup_dir="$TEST_DIR/status_test"
184 local slot="test_slot_status"
186 # Setup: Create database and init backup system
187 create_test_db "$dbname"
188 create_table_with_pk "$dbname" "users"
189 query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice');"
190 init_backup_system "$dbname" "$backup_dir" "$slot"
192 # Start streaming to first chain
193 log_info "Starting streaming to first chain..."
194 start_streaming "$dbname" "$backup_dir" >/dev/null
198 query_db "$dbname" "INSERT INTO users (name) VALUES ('Bob');"
201 # Create a new chain and transfer streaming to it (using --new-chain --start)
202 log_info "Creating new chain and transferring streaming..."
203 "$PG_SCRIBE" --new-chain --start -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null &
207 # Get the PID from pidfile
208 local pidfile="$backup_dir/.pg_scribe.pid"
210 streaming_pid=$(cat "$pidfile")
212 # Get actual active chain (from /proc)
214 actual_chain=$(get_actual_active_chain "$streaming_pid")
216 if [[ -z "$actual_chain" ]]; then
217 log_fail "Could not determine actual active chain from /proc/$streaming_pid/fd/"
218 kill -TERM "$streaming_pid" 2>/dev/null || true
222 log_info "Process $streaming_pid is actually writing to chain-$actual_chain"
224 # Get reported active chain (from --status)
226 reported_chain=$(get_status_active_chain "$dbname" "$backup_dir" "$slot")
228 if [[ -z "$reported_chain" ]]; then
229 log_fail "--status did not report any active chain"
230 kill -TERM "$streaming_pid" 2>/dev/null || true
234 log_info "--status reports chain-$reported_chain as active"
237 kill -TERM "$streaming_pid" 2>/dev/null || true
241 if [[ "$actual_chain" != "$reported_chain" ]]; then
242 log_fail "MISMATCH: --status reports chain-$reported_chain but process is writing to chain-$actual_chain"
246 log_pass "--status correctly reports the active chain"
250 test_new_chain_seals_old_active_sql() {
252 log_test "--new-chain --start should seal old chain's active.sql"
254 local dbname="${TEST_DB_PREFIX}_seal"
255 local backup_dir="$TEST_DIR/seal_test"
256 local slot="test_slot_seal"
258 # Setup: Create database and init backup system
259 create_test_db "$dbname"
260 create_table_with_pk "$dbname" "products"
261 query_db "$dbname" "INSERT INTO products (name) VALUES ('Widget');"
262 init_backup_system "$dbname" "$backup_dir" "$slot"
264 # Start streaming to first chain
265 log_info "Starting streaming to first chain..."
266 start_streaming "$dbname" "$backup_dir" >/dev/null
269 # Generate some data to ensure active.sql has content
271 query_db "$dbname" "INSERT INTO products (name) VALUES ('Product $i');"
275 # Find the old chain ID
277 old_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
279 old_chain_id=$(basename "$old_chain" | sed 's/^chain-//')
280 log_info "Old chain: $old_chain_id"
282 # Verify old chain has active.sql with data
283 if [[ ! -s "$old_chain/active.sql" ]]; then
284 log_fail "Old chain's active.sql doesn't exist or is empty before transfer"
285 stop_streaming "$backup_dir"
289 local old_active_size
290 old_active_size=$(stat -c %s "$old_chain/active.sql")
291 log_info "Old chain's active.sql size before transfer: $old_active_size bytes"
293 # Create a new chain and transfer streaming to it
294 log_info "Creating new chain and transferring streaming..."
295 "$PG_SCRIBE" --new-chain --start -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null &
299 stop_streaming "$backup_dir"
302 # Check old chain: should NOT have active.sql anymore
303 if [[ -f "$old_chain/active.sql" ]]; then
304 log_fail "CRITICAL: Old chain still has active.sql after transfer!"
305 log_fail "This is the bug - active.sql should have been sealed into a timestamped differential"
307 # Show what's in the old chain
308 log_info "Files in old chain:"
314 # Check old chain: should have a sealed differential with the content
316 sealed_diffs=$(find "$old_chain" -maxdepth 1 -name 'diff-*.sql' 2>/dev/null | wc -l)
318 if [[ $sealed_diffs -eq 0 ]]; then
319 log_fail "Old chain has no sealed differentials after transfer"
320 log_fail "The active.sql content was lost!"
324 log_info "Old chain has $sealed_diffs sealed differential(s)"
326 # Verify the sealed differential has the data that was in active.sql
327 local total_sealed_size=0
328 while IFS= read -r diff_file; do
330 size=$(stat -c %s "$diff_file")
331 total_sealed_size=$((total_sealed_size + size))
332 done < <(find "$old_chain" -maxdepth 1 -name 'diff-*.sql' 2>/dev/null)
334 log_info "Total sealed differential size: $total_sealed_size bytes"
336 # The sealed data should be at least as much as the old active.sql
337 if [[ $total_sealed_size -lt $old_active_size ]]; then
338 log_fail "Sealed differential size ($total_sealed_size) is less than old active.sql ($old_active_size)"
339 log_fail "Data may have been lost!"
343 log_pass "Old chain's active.sql was properly sealed into differential(s)"
351 # shellcheck disable=SC2317 # Function called via trap handler
353 log_info "Cleaning up test resources..."
355 # Stop any lingering streaming processes
356 if [[ -d "$TEST_DIR" ]]; then
357 find "$TEST_DIR" -name '.pg_scribe.pid' 2>/dev/null | while read -r pidfile; do
358 if [[ -f "$pidfile" ]]; then
360 pid=$(cat "$pidfile")
361 kill -TERM "$pid" 2>/dev/null || true
366 # Give processes time to stop
369 # Drop replication slots
370 for entry in "${SLOTS_TO_CLEANUP[@]}"; do
371 local dbname="${entry%%:*}"
372 local slot="${entry#*:}"
373 psql -U "$PGUSER" -d "$dbname" -c "
374 SELECT pg_drop_replication_slot('$slot')
375 FROM pg_replication_slots
376 WHERE slot_name = '$slot';
377 " &>/dev/null || true
381 for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
382 drop_test_db "$dbname"
385 # Remove test directory
386 if [[ -d "$TEST_DIR" ]]; then
390 log_info "Cleanup complete"
398 echo "========================================"
399 echo "pg_scribe Chain Transfer Tests"
400 echo "========================================"
402 echo "These tests verify proper behavior when"
403 echo "transferring streaming between chains"
406 # Verify pg_scribe exists
407 if [[ ! -x "$PG_SCRIBE" ]]; then
408 echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
412 # Verify PostgreSQL is running
413 if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
414 echo "ERROR: Cannot connect to PostgreSQL"
418 # Verify wal_level is logical
420 wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
421 if [[ "$wal_level" != "logical" ]]; then
422 echo "ERROR: wal_level must be 'logical', currently: $wal_level"
423 echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
427 # Create test directory
430 # Set up cleanup trap
431 trap cleanup EXIT INT TERM
433 echo "Running tests..."
436 # Run all tests (use || true to prevent set -e from exiting)
437 test_status_reports_correct_active_chain || true
438 test_new_chain_seals_old_active_sql || true
442 echo "========================================"
444 echo "========================================"
445 echo "Tests run: $TESTS_RUN"
446 echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
447 echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
450 if [[ $TESTS_FAILED -eq 0 ]]; then
451 echo -e "${GREEN}All tests passed!${NC}"
454 echo -e "${RED}Some tests failed!${NC}"
456 echo "This is EXPECTED before fixing the bugs."
457 echo "These failures demonstrate the chain transfer problems."