Skip to content

Commit df4d835

Browse files
committed
fix: actually figure out the database on the restore side
1 parent fcd762d commit df4d835

1 file changed

Lines changed: 150 additions & 47 deletions

File tree

src/controllers/overlay.rs

Lines changed: 150 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -472,13 +472,17 @@ async fn connect_overlay(
472472
Ok(pg)
473473
}
474474

475+
/// Captured FDW state from the overlay database.
476+
struct FdwState {
477+
has_extension: bool,
478+
server_host: Option<String>,
479+
server_dbname: Option<String>,
480+
has_user_mapping: bool,
481+
schemas_with_fts: HashSet<String>,
482+
}
483+
475484
/// Query the overlay database to determine the current FDW state.
476-
///
477-
/// Returns `(has_extension, server_host_if_exists, has_user_mapping, schemas_with_foreign_tables)`.
478-
async fn check_fdw_state(
479-
pg: &tokio_postgres::Client,
480-
server_name: &str,
481-
) -> Result<(bool, Option<String>, bool, HashSet<String>)> {
485+
async fn check_fdw_state(pg: &tokio_postgres::Client, server_name: &str) -> Result<FdwState> {
482486
let has_extension = pg
483487
.query_opt(
484488
"SELECT 1 FROM pg_extension WHERE extname = 'postgres_fdw'",
@@ -488,16 +492,32 @@ async fn check_fdw_state(
488492
.is_some();
489493
debug!(has_extension, "checked postgres_fdw extension");
490494

491-
let server_host: Option<String> = pg
492-
.query_opt(
493-
"SELECT option_value FROM pg_options_to_table( \
495+
let server_opts: Vec<(String, String)> = pg
496+
.query(
497+
"SELECT option_name, option_value FROM pg_options_to_table( \
494498
(SELECT srvoptions FROM pg_foreign_server WHERE srvname = $1) \
495-
) WHERE option_name = 'host'",
499+
)",
496500
&[&server_name],
497501
)
498502
.await?
499-
.map(|row| row.get(0));
500-
debug!(server = server_name, host = ?server_host, "checked FDW server");
503+
.iter()
504+
.map(|row| (row.get(0), row.get(1)))
505+
.collect();
506+
507+
let server_host = server_opts
508+
.iter()
509+
.find(|(k, _)| k == "host")
510+
.map(|(_, v)| v.clone());
511+
let server_dbname = server_opts
512+
.iter()
513+
.find(|(k, _)| k == "dbname")
514+
.map(|(_, v)| v.clone());
515+
debug!(
516+
server = server_name,
517+
host = ?server_host,
518+
dbname = ?server_dbname,
519+
"checked FDW server options"
520+
);
501521

502522
let has_user_mapping = pg
503523
.query_opt(
@@ -526,12 +546,13 @@ async fn check_fdw_state(
526546
"checked schemas with foreign tables"
527547
);
528548

529-
Ok((
549+
Ok(FdwState {
530550
has_extension,
531551
server_host,
552+
server_dbname,
532553
has_user_mapping,
533554
schemas_with_fts,
534-
))
555+
})
535556
}
536557

537558
/// Find all FDW servers owned by this operator (matching the `fdw_` prefix)
@@ -566,13 +587,62 @@ async fn drop_stale_fdw_servers(
566587
Ok(())
567588
}
568589

590+
/// Connect to the restore's `postgres` database and find the largest
591+
/// non-system database by size. This is the database whose schemas we
592+
/// import via FDW into the overlay's `app` database.
593+
async fn discover_restore_database(
594+
restore_host: &str,
595+
fdw_user: &str,
596+
fdw_password: &str,
597+
) -> Result<String> {
598+
let connstr = format!(
599+
"host={restore_host} port=5432 dbname=postgres user={fdw_user} password={fdw_password} connect_timeout=10",
600+
);
601+
debug!(
602+
host = restore_host,
603+
"connecting to restore postgres database for database discovery"
604+
);
605+
let (pg, conn) = tokio_postgres::connect(&connstr, NoTls).await?;
606+
tokio::spawn(async move {
607+
if let Err(e) = conn.await {
608+
warn!(error = %e, "restore database connection error during discovery");
609+
}
610+
});
611+
612+
let row = pg
613+
.query_opt(
614+
"SELECT datname FROM pg_database \
615+
WHERE datname NOT IN ('postgres', 'template0', 'template1') \
616+
ORDER BY pg_database_size(datname) DESC \
617+
LIMIT 1",
618+
&[],
619+
)
620+
.await?;
621+
622+
match row {
623+
Some(r) => {
624+
let name: String = r.get(0);
625+
info!(
626+
host = restore_host,
627+
database = %name,
628+
"discovered main database in restore by size"
629+
);
630+
Ok(name)
631+
}
632+
None => Err(Error::MissingField(
633+
"no non-system databases found in restore".into(),
634+
)),
635+
}
636+
}
637+
569638
/// Resolve which schemas to import via FDW.
570639
///
571640
/// Uses the explicit `schema_mapping` from the spec if present, otherwise
572641
/// discovers user schemas from the restore database.
573642
async fn resolve_fdw_schemas(
574643
replica: &PostgresPhysicalReplica,
575644
restore_host: &str,
645+
restore_dbname: &str,
576646
fdw_user: &str,
577647
fdw_password: &str,
578648
replica_name: &str,
@@ -599,13 +669,16 @@ async fn resolve_fdw_schemas(
599669
info!(
600670
replica = replica_name,
601671
restore_host = restore_host,
672+
restore_dbname = restore_dbname,
602673
"no explicit schema mapping, discovering schemas from restore database"
603674
);
604675
let restore_connstr = format!(
605-
"host={restore_host} port=5432 dbname=app user={fdw_user} password={fdw_password} connect_timeout=10",
676+
"host={restore_host} port=5432 dbname={} user={fdw_user} password={fdw_password} connect_timeout=10",
677+
restore_dbname,
606678
);
607679
debug!(
608680
host = restore_host,
681+
dbname = restore_dbname,
609682
"connecting to restore database for schema discovery"
610683
);
611684
let (restore_pg, restore_conn) = tokio_postgres::connect(&restore_connstr, NoTls).await?;
@@ -678,27 +751,33 @@ pub async fn reconcile_fdw(
678751

679752
let overlay_pg = connect_overlay(&cluster_name, namespace, &su_secret).await?;
680753

754+
// Discover the main database in the restore (largest by size)
755+
let restore_dbname = discover_restore_database(&restore_host, &fdw_user, &fdw_password).await?;
756+
681757
// Check current state
682-
let (has_extension, server_host, has_user_mapping, schemas_with_fts) =
683-
check_fdw_state(&overlay_pg, &server_name).await?;
758+
let state = check_fdw_state(&overlay_pg, &server_name).await?;
684759

685-
let server_host_correct = server_host.as_deref() == Some(&restore_host);
760+
let server_host_correct = state.server_host.as_deref() == Some(restore_host.as_str());
761+
let server_dbname_correct = state.server_dbname.as_deref() == Some(restore_dbname.as_str());
686762

687763
info!(
688764
replica = %replica_name,
689-
has_extension,
690-
server_exists = server_host.is_some(),
765+
has_extension = state.has_extension,
766+
server_exists = state.server_host.is_some(),
691767
server_host_correct,
692-
has_user_mapping,
693-
foreign_table_schemas = schemas_with_fts.len(),
768+
server_dbname = ?state.server_dbname,
769+
expected_dbname = %restore_dbname,
770+
server_dbname_correct,
771+
has_user_mapping = state.has_user_mapping,
772+
foreign_table_schemas = state.schemas_with_fts.len(),
694773
"current FDW state in overlay database"
695774
);
696775

697776
// Drop stale servers from previous restores
698777
drop_stale_fdw_servers(&overlay_pg, &server_name, &replica_name).await?;
699778

700779
// Ensure _pgro schema and extension
701-
if !has_extension {
780+
if !state.has_extension {
702781
info!(replica = %replica_name, "creating _pgro schema and postgres_fdw extension");
703782
overlay_pg
704783
.batch_execute("CREATE SCHEMA IF NOT EXISTS _pgro")
@@ -710,45 +789,68 @@ pub async fn reconcile_fdw(
710789
debug!(replica = %replica_name, "postgres_fdw extension already present");
711790
}
712791

713-
// Ensure FDW server with correct host
714-
if server_host.is_none() {
792+
// Ensure FDW server with correct host and dbname
793+
if state.server_host.is_none() {
715794
info!(
716795
replica = %replica_name,
717796
server = %server_name,
718797
host = %restore_host,
798+
dbname = %restore_dbname,
719799
"creating FDW server"
720800
);
721801
overlay_pg
722802
.batch_execute(&format!(
723803
"CREATE SERVER {server_name} FOREIGN DATA WRAPPER postgres_fdw \
724-
OPTIONS (host {}, port '5432', dbname 'postgres')",
725-
quote_literal(&restore_host),
726-
))
727-
.await?;
728-
} else if !server_host_correct {
729-
info!(
730-
replica = %replica_name,
731-
server = %server_name,
732-
old_host = ?server_host,
733-
new_host = %restore_host,
734-
"updating FDW server host"
735-
);
736-
overlay_pg
737-
.batch_execute(&format!(
738-
"ALTER SERVER {server_name} OPTIONS (SET host {})",
804+
OPTIONS (host {}, port '5432', dbname {})",
739805
quote_literal(&restore_host),
806+
quote_literal(&restore_dbname),
740807
))
741808
.await?;
742809
} else {
743-
debug!(
744-
replica = %replica_name,
745-
server = %server_name,
746-
"FDW server already correct"
747-
);
810+
// Server exists — fix any options that are wrong
811+
let mut alter_parts = Vec::new();
812+
if !server_host_correct {
813+
info!(
814+
replica = %replica_name,
815+
server = %server_name,
816+
old_host = ?state.server_host,
817+
new_host = %restore_host,
818+
"FDW server host needs update"
819+
);
820+
alter_parts.push(format!("SET host {}", quote_literal(&restore_host)));
821+
}
822+
if !server_dbname_correct {
823+
info!(
824+
replica = %replica_name,
825+
server = %server_name,
826+
old_dbname = ?state.server_dbname,
827+
new_dbname = %restore_dbname,
828+
"FDW server dbname needs update"
829+
);
830+
let verb = if state.server_dbname.is_some() {
831+
"SET"
832+
} else {
833+
"ADD"
834+
};
835+
alter_parts.push(format!("{verb} dbname {}", quote_literal(&restore_dbname)));
836+
}
837+
if alter_parts.is_empty() {
838+
debug!(
839+
replica = %replica_name,
840+
server = %server_name,
841+
"FDW server already correct"
842+
);
843+
} else {
844+
let opts = alter_parts.join(", ");
845+
debug!(server = %server_name, alter = %opts, "altering FDW server options");
846+
overlay_pg
847+
.batch_execute(&format!("ALTER SERVER {server_name} OPTIONS ({opts})"))
848+
.await?;
849+
}
748850
}
749851

750852
// Ensure user mapping (always recreate to pick up credential changes)
751-
if has_user_mapping {
853+
if state.has_user_mapping {
752854
debug!(server = %server_name, "dropping existing user mapping before recreation");
753855
overlay_pg
754856
.batch_execute(&format!(
@@ -770,6 +872,7 @@ pub async fn reconcile_fdw(
770872
let schemas = resolve_fdw_schemas(
771873
replica,
772874
&restore_host,
875+
&restore_dbname,
773876
&fdw_user,
774877
&fdw_password,
775878
&replica_name,
@@ -778,7 +881,7 @@ pub async fn reconcile_fdw(
778881

779882
let mut imported_count = 0u32;
780883
for (remote, local) in &schemas {
781-
if schemas_with_fts.contains(local) {
884+
if state.schemas_with_fts.contains(local) {
782885
debug!(
783886
local = %local,
784887
remote = %remote,

0 commit comments

Comments
 (0)