@@ -462,18 +462,72 @@ async fn main() -> anyhow::Result<()> {
462462 filter. with_table_rules ( table_rule_data)
463463 } ;
464464
465- // Get project_id from CLI or saved target state
466- let effective_project_id = project_id. or_else ( || {
465+ // Get project_id from CLI, saved target state, or discover from target URL
466+ let mut effective_project_id = project_id. or_else ( || {
467467 database_replicator:: serendb:: load_target_state ( )
468468 . ok ( )
469469 . flatten ( )
470470 . map ( |state| state. project_id )
471471 } ) ;
472472
473+ // If project_id is still None and target is SerenDB, try to discover it by hostname
474+ if effective_project_id. is_none ( )
475+ && database_replicator:: utils:: is_serendb_target ( & resolved_target)
476+ {
477+ // Get API key from CLI/env, or prompt user interactively
478+ let api_key = global_api_key
479+ . clone ( )
480+ . or_else ( || database_replicator:: interactive:: get_api_key ( ) . ok ( ) ) ;
481+
482+ if let Some ( api_key) = api_key {
483+ // Extract hostname from target URL
484+ if let Ok ( parts) =
485+ database_replicator:: utils:: parse_postgres_url ( & resolved_target)
486+ {
487+ tracing:: info!(
488+ "Discovering SerenDB project for hostname {}..." ,
489+ parts. host
490+ ) ;
491+ let client = database_replicator:: serendb:: ConsoleClient :: new (
492+ Some ( & console_api) ,
493+ api_key,
494+ ) ;
495+ match client. find_project_by_hostname ( & parts. host ) . await {
496+ Ok ( Some ( project_id) ) => {
497+ effective_project_id = Some ( project_id) ;
498+ }
499+ Ok ( None ) => {
500+ tracing:: warn!(
501+ "Could not find SerenDB project matching hostname {}. \
502+ Logical replication auto-enable will be skipped.",
503+ parts. host
504+ ) ;
505+ }
506+ Err ( e) => {
507+ tracing:: warn!(
508+ "Failed to discover project from hostname: {}. \
509+ Logical replication auto-enable will be skipped.",
510+ e
511+ ) ;
512+ }
513+ }
514+ }
515+ } else {
516+ tracing:: debug!(
517+ "No API key available, skipping project discovery from target hostname"
518+ ) ;
519+ }
520+ }
521+
473522 // If project_id is available and target is SerenDB, check/enable logical replication
474523 if let Some ( ref project_id) = effective_project_id {
475524 if database_replicator:: utils:: is_serendb_target ( & resolved_target) {
476- check_and_enable_logical_replication ( project_id, & console_api) . await ?;
525+ check_and_enable_logical_replication (
526+ project_id,
527+ & console_api,
528+ & resolved_target,
529+ )
530+ . await ?;
477531 }
478532 }
479533
@@ -536,6 +590,7 @@ async fn main() -> anyhow::Result<()> {
536590async fn check_and_enable_logical_replication (
537591 project_id : & str ,
538592 console_api : & str ,
593+ target_url : & str ,
539594) -> anyhow:: Result < ( ) > {
540595 use database_replicator:: serendb:: ConsoleClient ;
541596 use dialoguer:: { theme:: ColorfulTheme , Confirm } ;
@@ -556,6 +611,29 @@ async fn check_and_enable_logical_replication(
556611 "✓ Logical replication is already enabled for project '{}'" ,
557612 project. name
558613 ) ;
614+ // Verify the actual wal_level on the database (endpoint may still be restarting)
615+ match database_replicator:: postgres:: connect_with_retry ( target_url) . await {
616+ Ok ( client) => {
617+ if let Ok ( row) = client. query_one ( "SHOW wal_level" , & [ ] ) . await {
618+ let level: String = row. get ( 0 ) ;
619+ if level == "logical" {
620+ return Ok ( ( ) ) ;
621+ }
622+ // wal_level not yet 'logical', need to wait for endpoint restart
623+ tracing:: info!(
624+ "Endpoint has wal_level='{}', waiting for restart to apply 'logical'..." ,
625+ level
626+ ) ;
627+ }
628+ }
629+ Err ( _) => {
630+ tracing:: info!( "Endpoint may be restarting, will poll for readiness..." ) ;
631+ }
632+ }
633+ // Fall through to wait for wal_level to become 'logical'
634+ println ! ( ) ;
635+ println ! ( "⏳ Waiting for endpoint to restart with wal_level=logical..." ) ;
636+ wait_for_wal_level_logical ( target_url) . await ?;
559637 return Ok ( ( ) ) ;
560638 }
561639
@@ -601,12 +679,9 @@ async fn check_and_enable_logical_replication(
601679 println ! ( ) ;
602680 println ! ( "✓ Logical replication enabled successfully!" ) ;
603681 println ! ( ) ;
604- println ! ( "⏳ Waiting for endpoints to restart (this may take up to 30 seconds) ..." ) ;
682+ println ! ( "⏳ Waiting for endpoint to restart with wal_level=logical ..." ) ;
605683
606- // Wait for endpoints to restart - the wal_level change requires endpoint restart
607- tokio:: time:: sleep ( tokio:: time:: Duration :: from_secs ( 15 ) ) . await ;
608-
609- tracing:: info!( "✓ Endpoints should now be ready with wal_level=logical" ) ;
684+ wait_for_wal_level_logical ( target_url) . await ?;
610685 } else {
611686 anyhow:: bail!(
612687 "Failed to enable logical replication. The API call succeeded but the setting was not updated.\n \
@@ -619,6 +694,69 @@ async fn check_and_enable_logical_replication(
619694 Ok ( ( ) )
620695}
621696
697+ /// Poll the database until wal_level becomes 'logical' (up to 60 seconds)
698+ async fn wait_for_wal_level_logical ( target_url : & str ) -> anyhow:: Result < ( ) > {
699+ let max_attempts = 12 ;
700+ let poll_interval = tokio:: time:: Duration :: from_secs ( 5 ) ;
701+
702+ for attempt in 1 ..=max_attempts {
703+ tokio:: time:: sleep ( poll_interval) . await ;
704+
705+ match database_replicator:: postgres:: connect_with_retry ( target_url) . await {
706+ Ok ( client) => {
707+ match client
708+ . query_one ( "SHOW wal_level" , & [ ] )
709+ . await
710+ . map ( |row| row. get :: < _ , String > ( 0 ) )
711+ {
712+ Ok ( level) if level == "logical" => {
713+ println ! ( ) ;
714+ tracing:: info!( "✓ Endpoint is ready with wal_level=logical" ) ;
715+ return Ok ( ( ) ) ;
716+ }
717+ Ok ( level) => {
718+ print ! (
719+ "\r ⏳ Attempt {}/{}: wal_level={}, waiting..." ,
720+ attempt, max_attempts, level
721+ ) ;
722+ std:: io:: Write :: flush ( & mut std:: io:: stdout ( ) ) . ok ( ) ;
723+ }
724+ Err ( _) => {
725+ print ! (
726+ "\r ⏳ Attempt {}/{}: checking wal_level..." ,
727+ attempt, max_attempts
728+ ) ;
729+ std:: io:: Write :: flush ( & mut std:: io:: stdout ( ) ) . ok ( ) ;
730+ }
731+ }
732+ }
733+ Err ( _) => {
734+ print ! (
735+ "\r ⏳ Attempt {}/{}: endpoint restarting..." ,
736+ attempt, max_attempts
737+ ) ;
738+ std:: io:: Write :: flush ( & mut std:: io:: stdout ( ) ) . ok ( ) ;
739+ }
740+ }
741+ }
742+
743+ println ! ( ) ;
744+ println ! ( ) ;
745+ println ! ( "⚠️ Timed out waiting for wal_level to become 'logical'." ) ;
746+ println ! ( ) ;
747+ println ! ( "The SerenDB endpoint may need to be manually restarted:" ) ;
748+ println ! ( " 1. Go to https://console.serendb.com" ) ;
749+ println ! ( " 2. Navigate to your project's Compute endpoints" ) ;
750+ println ! ( " 3. Click 'Restart' on the endpoint" ) ;
751+ println ! ( " 4. Wait for the endpoint to become available" ) ;
752+ println ! ( " 5. Re-run this command" ) ;
753+ println ! ( ) ;
754+ anyhow:: bail!(
755+ "Endpoint wal_level is still 'replica' after enabling logical replication. \
756+ The endpoint may need to be manually restarted via the SerenDB console."
757+ )
758+ }
759+
622760#[ allow( clippy:: too_many_arguments) ]
623761async fn init_remote (
624762 source : String ,
0 commit comments