// Copyright (c) 2023-2026 ParadeDB, Inc. // // This file is part of ParadeDB - Postgres for Search and Analytics // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use anyhow::Result; use cmd_lib::run_fun; use rstest::*; use sqlx::PgConnection; use std::path::PathBuf; use std::process::Command; use tempfile::NamedTempFile; use tests::fixtures::{conn, db::Query}; fn pg_bin_path() -> Option { let pg_config_path = std::env::var("PG_CONFIG").ok()?; match run_fun!($pg_config_path --bindir) { Ok(path) => Some(PathBuf::from(path.trim())), Err(_) => None, } } #[rstest] fn test_pg_dump_restore(mut conn: PgConnection) -> Result<()> { let Some(pg_bin) = pg_bin_path() else { eprintln!("Skipping test_pg_dump_restore: PG_CONFIG not set"); return Ok(()); }; // Query database for connection info (similar to how replication.rs gets username) let dbname = "SELECT current_database()" .fetch_one::<(String,)>(&mut conn) .0; let port = "SELECT setting FROM pg_settings WHERE name = 'port'" .fetch_one::<(String,)>(&mut conn) .0; let user = "SELECT current_user".fetch_one::<(String,)>(&mut conn).0; let host = "localhost".to_string(); r#" CREATE TABLE lt ( id uuid not null, organization_id uuid not null, is_live boolean not null, description text, metadata jsonb ); "# .execute(&mut conn); r#" INSERT INTO lt (id, organization_id, is_live, description, metadata) VALUES ('550e8400-e29b-41d4-a716-446655440000'::uuid, '660e8400-e29b-41d4-a716-446655440001'::uuid, true, 'Payment for services', '{"amount": 1000, "currency": "USD"}'), ('550e8400-e29b-41d4-a716-446655440001'::uuid, '660e8400-e29b-41d4-a716-446655440001'::uuid, false, 'Refund processed', '{"amount": -500, "currency": "USD"}'), ('550e8400-e29b-41d4-a716-446655440002'::uuid, '660e8400-e29b-41d4-a716-446655440002'::uuid, true, 'Subscription renewal', '{"amount": 99, "currency": "USD", "plan": "premium"}'); "# .execute(&mut conn); r#" CREATE INDEX lt_search_index_v3 ON lt USING bm25 ( id, organization_id, is_live, (description::pdb.ngram(3,5)), (metadata::pdb.literal), (metadata::pdb.unicode_words('alias=metadata_words')) ) WITH (key_field=id); "# .execute(&mut conn); // Verify initial data and search functionality let initial_count: Vec<(i64,)> = "SELECT COUNT(*) FROM lt".fetch(&mut conn); assert_eq!(initial_count[0].0, 3); let search_results: Vec<(String,)> = r#" SELECT id::text FROM lt WHERE lt @@@ 'description:payment' ORDER BY id "# .fetch(&mut conn); assert_eq!(search_results.len(), 1); assert_eq!(search_results[0].0, "550e8400-e29b-41d4-a716-446655440000"); // Create a temporary file for the dump let dump_file = NamedTempFile::new()?; let dump_path = dump_file.path().to_str().unwrap(); // Build pg_dump command using the correct version from PG_CONFIG let mut pg_dump_cmd = Command::new(pg_bin.join("pg_dump")); pg_dump_cmd .arg("-Fc") // Custom format .arg("--no-acl") // Skip ACLs .arg("--no-owner") // Skip ownership .arg("-h") .arg(&host) .arg("-p") .arg(&port) .arg("-U") .arg(&user) .arg("-t") .arg("lt") // Only dump this table .arg(&dbname); // Run pg_dump let pg_dump_output = pg_dump_cmd.output().expect("Failed to execute pg_dump"); if !pg_dump_output.status.success() { let stderr = String::from_utf8_lossy(&pg_dump_output.stderr); panic!("pg_dump failed: {}", stderr); } // Write dump to file std::fs::write(dump_path, &pg_dump_output.stdout)?; // Build pg_restore command let mut pg_restore_cmd = Command::new(pg_bin.join("pg_restore")); pg_restore_cmd .arg("--verbose") .arg("--clean") .arg("--if-exists") .arg("--no-acl") .arg("--no-owner") .arg("-h") .arg(&host) .arg("-p") .arg(&port) .arg("-U") .arg(&user) .arg("-d") .arg(&dbname) .arg(dump_path); // Run pg_restore let pg_restore_output = pg_restore_cmd .output() .expect("Failed to execute pg_restore"); if !pg_restore_output.status.success() { let stderr = String::from_utf8_lossy(&pg_restore_output.stderr); panic!("pg_restore failed: {}", stderr); } // Verify table is restored let table_exists: Vec<(bool,)> = r#" SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_name = 'lt' ) "# .fetch(&mut conn); assert!(table_exists[0].0); // Verify data is restored let restored_count: Vec<(i64,)> = "SELECT COUNT(*) FROM lt".fetch(&mut conn); assert_eq!(restored_count[0].0, 3); // Verify search functionality still works let search_results: Vec<(String,)> = r#" SELECT id::text FROM lt WHERE id @@@ pdb.all() ORDER BY id "# .fetch(&mut conn); assert_eq!(search_results.len(), 3); Ok(()) }