3 # Test suite for pg_scribe --restore command
6 # - Creates temporary test databases
7 # - Tests various --restore scenarios
8 # - Verifies expected outcomes
9 # - Cleans up all resources
14 # Colors for test output
19 NC='\033[0m' # No Color
22 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
23 PG_SCRIBE="$SCRIPT_DIR/scripts/pg_scribe"
24 TEST_DIR="/tmp/pg_scribe_test_restore_$$"
25 TEST_DB_PREFIX="pg_scribe_restore_test_$$"
26 PGUSER="${PGUSER:-postgres}"
34 DATABASES_TO_CLEANUP=()
42 echo -e "${BLUE}TEST:${NC} $*"
46 echo -e "${GREEN}PASS:${NC} $*"
51 echo -e "${RED}FAIL:${NC} $*"
56 echo -e "${YELLOW}INFO:${NC} $*"
66 psql -U "$PGUSER" -d "$dbname" -tAq "$@"
72 run_psql "$dbname" -c "$query" 2>/dev/null || true
77 log_info "Creating test database: $dbname"
80 psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
83 psql -U "$PGUSER" -d postgres -c "CREATE DATABASE $dbname;" &>/dev/null
85 DATABASES_TO_CLEANUP+=("$dbname")
88 # shellcheck disable=SC2317 # Function called from cleanup trap handler
91 log_info "Dropping test database: $dbname"
93 # Terminate connections
94 psql -U "$PGUSER" -d postgres -c "
95 SELECT pg_terminate_backend(pid)
97 WHERE datname = '$dbname' AND pid <> pg_backend_pid();
101 psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
104 # shellcheck disable=SC2317 # Function called from cleanup trap handler
105 drop_replication_slot() {
108 log_info "Dropping replication slot: $slot"
110 # Check if slot exists
112 exists=$(query_db "$dbname" "
113 SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
116 if [[ -n "$exists" ]]; then
118 query_db "$dbname" "SELECT pg_drop_replication_slot('$slot');" || true
122 create_table_with_pk() {
126 CREATE TABLE $table (
127 id SERIAL PRIMARY KEY,
129 created_at TIMESTAMP DEFAULT now()
134 initialize_backup() {
136 local backup_dir="$2"
140 "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null
141 SLOTS_TO_CLEANUP+=("$dbname:$slot")
148 test_basic_restore_from_base_only() {
150 log_test "Basic restore from base backup only"
152 local source_db="${TEST_DB_PREFIX}_source_basic"
153 local restore_db="${TEST_DB_PREFIX}_restore_basic"
154 local slot="test_slot_basic"
155 local backup_dir="$TEST_DIR/basic"
157 # Setup source database
158 create_test_db "$source_db"
159 create_table_with_pk "$source_db" "users"
160 query_db "$source_db" "INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie');"
163 mkdir -p "$backup_dir"
164 initialize_backup "$source_db" "$backup_dir" "$slot"
166 # Restore to new database with --create
167 if "$PG_SCRIBE" --restore -d "$restore_db" -f "$backup_dir" -C -U "$PGUSER" &>/dev/null; then
168 DATABASES_TO_CLEANUP+=("$restore_db")
172 count=$(query_db "$restore_db" "SELECT COUNT(*) FROM users;")
173 if [[ "$count" -ne 3 ]]; then
174 log_fail "Expected 3 rows, got $count"
178 # Verify specific data (psql returns one value per line)
180 names=$(query_db "$restore_db" "SELECT name FROM users ORDER BY id;")
181 local expected_names=$'Alice\nBob\nCharlie'
182 if [[ "$names" != "$expected_names" ]]; then
183 log_fail "Data mismatch: expected '$expected_names', got '$names'"
187 log_pass "Basic restore successful"
190 log_fail "Restore command failed"
195 test_restore_with_incremental_backups() {
197 log_test "Restore with base + incremental backups"
199 local source_db="${TEST_DB_PREFIX}_source_incr"
200 local restore_db="${TEST_DB_PREFIX}_restore_incr"
201 local slot="test_slot_incr"
202 local backup_dir="$TEST_DIR/incremental"
204 # Setup source database
205 create_test_db "$source_db"
206 create_table_with_pk "$source_db" "orders"
207 query_db "$source_db" "INSERT INTO orders (name) VALUES ('Order1'), ('Order2');"
210 mkdir -p "$backup_dir"
211 initialize_backup "$source_db" "$backup_dir" "$slot"
213 # Start collecting incremental changes
214 local incr_file="$backup_dir/incremental.sql"
215 "$PG_SCRIBE" --start -d "$source_db" -f "$incr_file" -S "$slot" -U "$PGUSER" &>/dev/null &
216 local pg_scribe_pid=$!
218 # Wait for streaming to start
222 query_db "$source_db" "INSERT INTO orders (name) VALUES ('Order3');"
223 query_db "$source_db" "UPDATE orders SET name = 'Order1_Updated' WHERE name = 'Order1';"
224 query_db "$source_db" "DELETE FROM orders WHERE name = 'Order2';"
226 # Wait for changes to be captured
230 kill "$pg_scribe_pid" 2>/dev/null || true
231 wait "$pg_scribe_pid" 2>/dev/null || true
233 # Verify incremental file exists and has content
234 if [[ ! -s "$incr_file" ]]; then
235 log_fail "Incremental backup file is empty or missing"
239 # Restore to new database
240 if "$PG_SCRIBE" --restore -d "$restore_db" -f "$backup_dir" -C -U "$PGUSER" &>/dev/null; then
241 DATABASES_TO_CLEANUP+=("$restore_db")
243 # Verify final state (should have Order1_Updated and Order3, not Order2)
245 count=$(query_db "$restore_db" "SELECT COUNT(*) FROM orders;")
246 if [[ "$count" -ne 2 ]]; then
247 log_fail "Expected 2 rows after incremental restore, got $count"
251 # Verify Order1 was updated
253 order1_name=$(query_db "$restore_db" "SELECT name FROM orders WHERE id = 1;")
254 if [[ "$order1_name" != "Order1_Updated" ]]; then
255 log_fail "UPDATE not applied: expected 'Order1_Updated', got '$order1_name'"
259 # Verify Order3 exists
261 order3_exists=$(query_db "$restore_db" "SELECT COUNT(*) FROM orders WHERE name = 'Order3';")
262 if [[ "$order3_exists" -ne 1 ]]; then
263 log_fail "INSERT not applied: Order3 not found"
267 # Verify Order2 was deleted
269 order2_exists=$(query_db "$restore_db" "SELECT COUNT(*) FROM orders WHERE name = 'Order2';")
270 if [[ "$order2_exists" -ne 0 ]]; then
271 log_fail "DELETE not applied: Order2 still exists"
275 log_pass "Restore with incrementals successful"
278 log_fail "Restore command failed"
283 test_restore_with_ddl_changes() {
285 log_test "Restore with DDL changes in incremental"
287 local source_db="${TEST_DB_PREFIX}_source_ddl"
288 local restore_db="${TEST_DB_PREFIX}_restore_ddl"
289 local slot="test_slot_ddl"
290 local backup_dir="$TEST_DIR/ddl"
292 # Setup source database
293 create_test_db "$source_db"
294 create_table_with_pk "$source_db" "products"
295 query_db "$source_db" "INSERT INTO products (name) VALUES ('Widget');"
298 mkdir -p "$backup_dir"
299 initialize_backup "$source_db" "$backup_dir" "$slot"
301 # Start collecting incremental changes
302 local incr_file="$backup_dir/incremental.sql"
303 "$PG_SCRIBE" --start -d "$source_db" -f "$incr_file" -S "$slot" -U "$PGUSER" &>/dev/null &
304 local pg_scribe_pid=$!
306 # Wait for streaming to start
309 # Add a column (DDL change)
310 query_db "$source_db" "ALTER TABLE products ADD COLUMN price NUMERIC(10,2);"
312 # Insert data using new column
313 query_db "$source_db" "INSERT INTO products (name, price) VALUES ('Gadget', 19.99);"
315 # Wait for changes to be captured
319 kill "$pg_scribe_pid" 2>/dev/null || true
320 wait "$pg_scribe_pid" 2>/dev/null || true
322 # Restore to new database
323 if "$PG_SCRIBE" --restore -d "$restore_db" -f "$backup_dir" -C -U "$PGUSER" &>/dev/null; then
324 DATABASES_TO_CLEANUP+=("$restore_db")
326 # Verify new column exists
327 local has_price_column
328 has_price_column=$(query_db "$restore_db" "
329 SELECT COUNT(*) FROM information_schema.columns
330 WHERE table_name = 'products' AND column_name = 'price';
332 if [[ "$has_price_column" -ne 1 ]]; then
333 log_fail "DDL not applied: price column not found"
337 # Verify data with new column
339 gadget_price=$(query_db "$restore_db" "SELECT price FROM products WHERE name = 'Gadget';")
340 if [[ "$gadget_price" != "19.99" ]]; then
341 log_fail "Data with new column not correct: expected '19.99', got '$gadget_price'"
345 log_pass "Restore with DDL changes successful"
348 log_fail "Restore command failed"
353 test_restore_sequence_synchronization() {
355 log_test "Restore with sequence synchronization"
357 local source_db="${TEST_DB_PREFIX}_source_seq"
358 local restore_db="${TEST_DB_PREFIX}_restore_seq"
359 local slot="test_slot_seq"
360 local backup_dir="$TEST_DIR/sequence"
362 # Setup source database
363 create_test_db "$source_db"
364 create_table_with_pk "$source_db" "items"
365 query_db "$source_db" "INSERT INTO items (name) VALUES ('Item1'), ('Item2'), ('Item3');"
368 mkdir -p "$backup_dir"
369 initialize_backup "$source_db" "$backup_dir" "$slot"
371 # Add more data to advance sequence
372 query_db "$source_db" "INSERT INTO items (name) VALUES ('Item4'), ('Item5');"
374 # Collect incremental
375 local incr_file="$backup_dir/incremental.sql"
376 "$PG_SCRIBE" --start -d "$source_db" -f "$incr_file" -S "$slot" -U "$PGUSER" &>/dev/null &
377 local pg_scribe_pid=$!
379 kill "$pg_scribe_pid" 2>/dev/null || true
380 wait "$pg_scribe_pid" 2>/dev/null || true
382 # Restore to new database
383 if "$PG_SCRIBE" --restore -d "$restore_db" -f "$backup_dir" -C -U "$PGUSER" &>/dev/null; then
384 DATABASES_TO_CLEANUP+=("$restore_db")
386 # Get current sequence value
388 seq_val=$(query_db "$restore_db" "SELECT last_value FROM items_id_seq;")
390 # Should be at least 5 (we inserted 5 items total)
391 if [[ "$seq_val" -lt 5 ]]; then
392 log_fail "Sequence not synchronized: expected >= 5, got $seq_val"
396 # Try inserting new row - should get ID 6
397 query_db "$restore_db" "INSERT INTO items (name) VALUES ('Item6');"
399 new_id=$(query_db "$restore_db" "SELECT id FROM items WHERE name = 'Item6';")
400 if [[ "$new_id" -ne 6 ]]; then
401 log_fail "Next sequence value incorrect: expected 6, got $new_id"
405 log_pass "Sequence synchronization successful"
408 log_fail "Restore command failed"
413 test_restore_no_sync_sequences() {
415 log_test "Restore with --no-sync-sequences flag"
417 local source_db="${TEST_DB_PREFIX}_source_noseq"
418 local restore_db="${TEST_DB_PREFIX}_restore_noseq"
419 local slot="test_slot_noseq"
420 local backup_dir="$TEST_DIR/noseq"
422 # Setup source database
423 create_test_db "$source_db"
424 create_table_with_pk "$source_db" "records"
425 query_db "$source_db" "INSERT INTO records (name) VALUES ('Rec1'), ('Rec2');"
428 mkdir -p "$backup_dir"
429 initialize_backup "$source_db" "$backup_dir" "$slot"
431 # Restore with --no-sync-sequences
432 if "$PG_SCRIBE" --restore -d "$restore_db" -f "$backup_dir" -C -U "$PGUSER" --no-sync-sequences &>/dev/null; then
433 DATABASES_TO_CLEANUP+=("$restore_db")
435 # Sequence should be at the value from pg_dump (2)
437 seq_val=$(query_db "$restore_db" "SELECT last_value FROM records_id_seq;")
439 # With --no-sync-sequences, sequence value is from pg_dump
440 # It should match what's in the backup
441 if [[ "$seq_val" -lt 1 ]]; then
442 log_fail "Sequence value unexpectedly low: $seq_val"
446 log_pass "Restore with --no-sync-sequences successful"
449 log_fail "Restore command failed"
454 test_restore_to_existing_database() {
456 log_test "Restore to existing database (without --create)"
458 local source_db="${TEST_DB_PREFIX}_source_exist"
459 local restore_db="${TEST_DB_PREFIX}_restore_exist"
460 local slot="test_slot_exist"
461 local backup_dir="$TEST_DIR/existing"
463 # Setup source database
464 create_test_db "$source_db"
465 create_table_with_pk "$source_db" "data"
466 query_db "$source_db" "INSERT INTO data (name) VALUES ('Test1');"
469 mkdir -p "$backup_dir"
470 initialize_backup "$source_db" "$backup_dir" "$slot"
472 # Pre-create target database
473 create_test_db "$restore_db"
475 # Restore without --create
476 if "$PG_SCRIBE" --restore -d "$restore_db" -f "$backup_dir" -U "$PGUSER" &>/dev/null; then
479 count=$(query_db "$restore_db" "SELECT COUNT(*) FROM data;")
480 if [[ "$count" -ne 1 ]]; then
481 log_fail "Expected 1 row, got $count"
485 log_pass "Restore to existing database successful"
488 log_fail "Restore command failed"
493 test_restore_fails_if_db_exists_with_create() {
495 log_test "Restore fails if database exists with --create"
497 local source_db="${TEST_DB_PREFIX}_source_exists"
498 local restore_db="${TEST_DB_PREFIX}_restore_exists"
499 local slot="test_slot_exists"
500 local backup_dir="$TEST_DIR/exists"
502 # Setup source database
503 create_test_db "$source_db"
504 create_table_with_pk "$source_db" "test"
505 query_db "$source_db" "INSERT INTO test (name) VALUES ('Test');"
508 mkdir -p "$backup_dir"
509 initialize_backup "$source_db" "$backup_dir" "$slot"
511 # Pre-create target database
512 create_test_db "$restore_db"
514 # Restore with --create should fail
516 "$PG_SCRIBE" --restore -d "$restore_db" -f "$backup_dir" -C -U "$PGUSER" &>/dev/null || exit_code=$?
518 if [[ $exit_code -eq 4 ]]; then
519 log_pass "Correctly failed when database exists with --create"
522 log_fail "Expected exit code 4, got $exit_code"
527 test_restore_missing_backup_directory() {
529 log_test "Restore fails with missing backup directory"
531 local restore_db="${TEST_DB_PREFIX}_restore_missing"
532 local backup_dir="$TEST_DIR/nonexistent"
534 # Try to restore from non-existent directory
536 "$PG_SCRIBE" --restore -d "$restore_db" -f "$backup_dir" -U "$PGUSER" &>/dev/null || exit_code=$?
538 if [[ $exit_code -eq 4 ]]; then
539 log_pass "Correctly failed with missing backup directory"
542 log_fail "Expected exit code 4, got $exit_code"
547 test_restore_specific_base_backup() {
549 log_test "Restore specific base backup with --base-backup"
551 local source_db="${TEST_DB_PREFIX}_source_specific"
552 local restore_db="${TEST_DB_PREFIX}_restore_specific"
553 local slot="test_slot_specific"
554 local backup_dir="$TEST_DIR/specific"
556 # Setup source database
557 create_test_db "$source_db"
558 create_table_with_pk "$source_db" "entries"
559 query_db "$source_db" "INSERT INTO entries (name) VALUES ('Entry1');"
561 # Initialize backup (creates first base backup)
562 mkdir -p "$backup_dir"
563 initialize_backup "$source_db" "$backup_dir" "$slot"
565 # Find the base backup file
567 base_file=$(find "$backup_dir" -maxdepth 1 -name 'base-*.sql' -print -quit)
569 if [[ -z "$base_file" ]]; then
570 log_fail "Base backup file not found"
574 # Restore using specific base backup
575 if "$PG_SCRIBE" --restore -d "$restore_db" -f "$backup_dir" --base-backup="$base_file" -C -U "$PGUSER" &>/dev/null; then
576 DATABASES_TO_CLEANUP+=("$restore_db")
580 count=$(query_db "$restore_db" "SELECT COUNT(*) FROM entries;")
581 if [[ "$count" -ne 1 ]]; then
582 log_fail "Expected 1 row, got $count"
586 log_pass "Restore with specific base backup successful"
589 log_fail "Restore command failed"
594 test_restore_multiple_tables() {
596 log_test "Restore multiple tables with relationships"
598 local source_db="${TEST_DB_PREFIX}_source_multi"
599 local restore_db="${TEST_DB_PREFIX}_restore_multi"
600 local slot="test_slot_multi"
601 local backup_dir="$TEST_DIR/multi"
603 # Setup source database with multiple related tables
604 create_test_db "$source_db"
605 query_db "$source_db" "
606 CREATE TABLE customers (
607 id SERIAL PRIMARY KEY,
610 CREATE TABLE orders (
611 id SERIAL PRIMARY KEY,
612 customer_id INTEGER REFERENCES customers(id),
613 product TEXT NOT NULL
616 query_db "$source_db" "INSERT INTO customers (name) VALUES ('Alice'), ('Bob');"
617 query_db "$source_db" "INSERT INTO orders (customer_id, product) VALUES (1, 'Widget'), (2, 'Gadget');"
620 mkdir -p "$backup_dir"
621 initialize_backup "$source_db" "$backup_dir" "$slot"
624 if "$PG_SCRIBE" --restore -d "$restore_db" -f "$backup_dir" -C -U "$PGUSER" &>/dev/null; then
625 DATABASES_TO_CLEANUP+=("$restore_db")
629 customer_count=$(query_db "$restore_db" "SELECT COUNT(*) FROM customers;")
630 if [[ "$customer_count" -ne 2 ]]; then
631 log_fail "Expected 2 customers, got $customer_count"
637 order_count=$(query_db "$restore_db" "SELECT COUNT(*) FROM orders;")
638 if [[ "$order_count" -ne 2 ]]; then
639 log_fail "Expected 2 orders, got $order_count"
643 # Verify foreign key relationship
645 alice_orders=$(query_db "$restore_db" "
646 SELECT o.product FROM orders o
647 JOIN customers c ON c.id = o.customer_id
648 WHERE c.name = 'Alice';
650 if [[ "$alice_orders" != "Widget" ]]; then
651 log_fail "Foreign key relationship not preserved"
655 log_pass "Multiple table restore successful"
658 log_fail "Restore command failed"
667 # shellcheck disable=SC2317 # Function called via trap handler
669 log_info "Cleaning up test resources..."
671 # Stop any background pg_scribe processes
672 pkill -f "pg_scribe.*--start" 2>/dev/null || true
673 pkill -f "pg_recvlogical" 2>/dev/null || true
676 # Drop replication slots
677 for slot_info in "${SLOTS_TO_CLEANUP[@]}"; do
678 IFS=':' read -r dbname slot <<< "$slot_info"
679 drop_replication_slot "$dbname" "$slot" 2>/dev/null || true
683 for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
684 drop_test_db "$dbname"
687 # Remove test directory
688 if [[ -d "$TEST_DIR" ]]; then
692 log_info "Cleanup complete"
700 echo "========================================"
701 echo "pg_scribe --restore Test Suite"
702 echo "========================================"
705 # Verify pg_scribe exists
706 if [[ ! -x "$PG_SCRIBE" ]]; then
707 echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
711 # Verify PostgreSQL is running
712 if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
713 echo "ERROR: Cannot connect to PostgreSQL"
717 # Verify wal_level is logical
719 wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
720 if [[ "$wal_level" != "logical" ]]; then
721 echo "ERROR: wal_level must be 'logical', currently: $wal_level"
722 echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
726 # Verify wal2sql extension is available
727 if ! psql -U "$PGUSER" -d postgres -c "CREATE EXTENSION IF NOT EXISTS wal2sql;" &>/dev/null; then
728 echo "ERROR: wal2sql extension not available"
729 echo "Build and install: cd wal2sql && make && make install"
733 # Create test directory
736 # Set up cleanup trap
737 trap cleanup EXIT INT TERM
739 echo "Running tests..."
742 # Run all tests (use || true to prevent set -e from exiting)
743 test_basic_restore_from_base_only || true
744 test_restore_with_incremental_backups || true
745 test_restore_with_ddl_changes || true
746 test_restore_sequence_synchronization || true
747 test_restore_no_sync_sequences || true
748 test_restore_to_existing_database || true
749 test_restore_fails_if_db_exists_with_create || true
750 test_restore_missing_backup_directory || true
751 test_restore_specific_base_backup || true
752 test_restore_multiple_tables || true
756 echo "========================================"
758 echo "========================================"
759 echo "Tests run: $TESTS_RUN"
760 echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
761 echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
764 if [[ $TESTS_FAILED -eq 0 ]]; then
765 echo -e "${GREEN}All tests passed!${NC}"
768 echo -e "${RED}Some tests failed!${NC}"