Skip to main content

laminar_db/db/
mod.rs

1//! The main `LaminarDB` database facade.
2#![allow(clippy::disallowed_types)] // cold path
3
4#[cfg(feature = "cluster")]
5mod assignment_authority;
6#[cfg(feature = "cluster")]
7mod catalog_bootstrap;
8mod checkpoint_namespace;
9mod checkpoint_requests;
10#[cfg(feature = "cluster")]
11mod cluster_subscription;
12pub(crate) mod committed_read;
13#[cfg(test)]
14mod datafusion_memory_tests;
15mod explain;
16pub(crate) mod read_level;
17mod session;
18mod source_watermarks;
19mod subscription_open;
20pub(crate) use source_watermarks::{RecoveredInputChannelProgress, SourceWatermarkState};
21#[cfg(feature = "cluster")]
22mod topology;
23#[cfg(feature = "cluster")]
24pub(crate) use assignment_authority::{
25    audited_stopped_recovery_successor_round, audited_stopped_terminal_round,
26};
27#[cfg(feature = "cluster")]
28pub use topology::{
29    ClusterTopologyAdoptionRequest, ClusterTopologyRequest, ClusterTopologyStatus,
30    PreparedTopologyRestore, PreparedTopologySourcePosition,
31};
32#[cfg(feature = "cluster")]
33pub use topology::{
34    ClusterTopologyObjectPlan, ClusterTopologyObjectTransition, ClusterTopologyValidation,
35    TopologyActivationRequirement, TopologyInitialization, TopologyValidationScope,
36};
37#[cfg(feature = "cluster")]
38pub(crate) use topology::{InstalledTopologyRuntime, TopologyRuntimeMetadata};
39
40use std::collections::HashMap;
41use std::sync::Arc;
42use std::time::{Duration, Instant};
43
44use arrow::array::RecordBatch;
45use datafusion::prelude::SessionContext;
46use laminar_core::catalog::CatalogObjectKind;
47use laminar_sql::parser::{parse_streaming_sql, ShowCommand, StreamingStatement};
48use laminar_sql::planner::StreamingPlanner;
49
50use crate::builder::LaminarDbBuilder;
51use crate::catalog::SourceCatalog;
52use crate::config::LaminarConfig;
53use crate::error::DbError;
54use crate::handle::{
55    DdlInfo, ExecuteResult, QueryHandle, QueryInfo, SinkInfo, SourceHandle, SourceInfo,
56    UntypedSourceHandle,
57};
58use crate::pipeline::ControlMsg;
59use crate::sql_utils;
60
61/// Cloneable async sender for the live-DDL control channel.
62pub(crate) type ControlMsgTx = crossfire::MAsyncTx<crossfire::mpsc::Array<ControlMsg>>;
63
64/// Lifecycle state of a [`LaminarDB`] instance.
65#[repr(u8)]
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub(crate) enum DbState {
68    Created = 0,
69    Starting = 1,
70    Running = 2,
71    ShuttingDown = 3,
72    Stopped = 4,
73    /// Runtime fault. Recoverable faults may restart from the catalog; terminal resource
74    /// exhaustion remains faulted for operator intervention. Reason in `LaminarDB::last_fault`.
75    Faulted = 5,
76}
77
78/// Deployment scope fixed when the database is constructed.
79///
80/// Cluster support being compiled into the process does not select cluster semantics. The
81/// builder resolves this value once and separately verifies that cluster-only handles agree with
82/// it, so admission cannot change because a controller slot happens to be populated later.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub(crate) enum RuntimeMode {
85    Local,
86    Cluster,
87}
88
89impl RuntimeMode {
90    pub(crate) const fn is_cluster(self) -> bool {
91        matches!(self, Self::Cluster)
92    }
93}
94
95impl DbState {
96    pub(crate) fn from_u8(raw: u8) -> Option<Self> {
97        Some(match raw {
98            0 => Self::Created,
99            1 => Self::Starting,
100            2 => Self::Running,
101            3 => Self::ShuttingDown,
102            4 => Self::Stopped,
103            5 => Self::Faulted,
104            _ => return None,
105        })
106    }
107
108    pub(crate) fn load(atomic: &std::sync::atomic::AtomicU8) -> Self {
109        Self::from_u8(atomic.load(std::sync::atomic::Ordering::Acquire)).unwrap_or(Self::Stopped)
110    }
111
112    pub(crate) fn store(self, atomic: &std::sync::atomic::AtomicU8) {
113        atomic.store(self as u8, std::sync::atomic::Ordering::Release);
114    }
115
116    /// Atomically transition `current → new`; returns the observed state on failure.
117    pub(crate) fn compare_exchange(
118        current: Self,
119        new: Self,
120        atomic: &std::sync::atomic::AtomicU8,
121    ) -> Result<Self, Self> {
122        use std::sync::atomic::Ordering;
123        atomic
124            .compare_exchange(
125                current as u8,
126                new as u8,
127                Ordering::AcqRel,
128                Ordering::Acquire,
129            )
130            .map(|v| Self::from_u8(v).unwrap_or(Self::Stopped))
131            .map_err(|v| Self::from_u8(v).unwrap_or(Self::Stopped))
132    }
133}
134
135const DB_IO_WORKER_THREADS: usize = 2;
136// Checkpoint and recovery ownership have finite, non-recursive async call chains that exceed
137// Tokio's 2 MiB default worker stack in debug builds. Both local and clustered runtimes execute
138// those paths, so keep the allocation explicit and bounded for the fixed two-worker executor.
139const DB_IO_WORKER_STACK_BYTES: usize = 4 * 1024 * 1024;
140
141struct DbControlRuntimeInner {
142    handle: tokio::runtime::Handle,
143    shutdown: tokio::sync::oneshot::Sender<()>,
144}
145
146/// Lazily-created executor for connector I/O and lifecycle ownership.
147///
148/// The runtime itself lives on a detached owner thread, so dropping a DB from async code never
149/// blocks on Tokio shutdown. The fixed two-worker size keeps control and feedback traffic live
150/// while one connector future is temporarily busy without adding a public tuning dimension.
151pub(crate) struct DbControlRuntime {
152    worker_stack_bytes: usize,
153    inner: parking_lot::Mutex<Option<DbControlRuntimeInner>>,
154}
155
156impl DbControlRuntime {
157    fn new() -> Self {
158        Self {
159            worker_stack_bytes: DB_IO_WORKER_STACK_BYTES,
160            inner: parking_lot::Mutex::new(None),
161        }
162    }
163
164    pub(crate) fn handle(&self) -> Result<tokio::runtime::Handle, DbError> {
165        let mut runtime_slot = self.inner.lock();
166        if let Some(runtime) = runtime_slot.as_ref() {
167            return Ok(runtime.handle.clone());
168        }
169
170        let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
171        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
172        let worker_stack_bytes = self.worker_stack_bytes;
173        let owner = std::thread::Builder::new()
174            .name("laminar-io-owner".into())
175            .spawn(move || {
176                let mut builder = tokio::runtime::Builder::new_multi_thread();
177                builder
178                    .worker_threads(DB_IO_WORKER_THREADS)
179                    .thread_name("laminar-io")
180                    .thread_stack_size(worker_stack_bytes)
181                    .enable_all();
182                let runtime = match builder.build() {
183                    Ok(runtime) => runtime,
184                    Err(error) => {
185                        let _ = ready_tx.send(Err(error.to_string()));
186                        return;
187                    }
188                };
189                if ready_tx.send(Ok(runtime.handle().clone())).is_err() {
190                    return;
191                }
192                runtime.block_on(async {
193                    let _ = shutdown_rx.await;
194                });
195                runtime.shutdown_timeout(Duration::from_secs(10));
196            })
197            .map_err(|error| {
198                DbError::Pipeline(format!("failed to spawn LaminarDB I/O runtime: {error}"))
199            })?;
200        // The shutdown sender is the nonblocking owner. The OS thread terminates after it fires.
201        drop(owner);
202
203        let handle = ready_rx
204            .recv()
205            .map_err(|_| {
206                DbError::Pipeline("LaminarDB I/O runtime exited during initialization".into())
207            })?
208            .map_err(|error| {
209                DbError::Pipeline(format!(
210                    "failed to initialize LaminarDB I/O runtime: {error}"
211                ))
212            })?;
213        *runtime_slot = Some(DbControlRuntimeInner {
214            handle: handle.clone(),
215            shutdown: shutdown_tx,
216        });
217        Ok(handle)
218    }
219}
220
221impl Drop for DbControlRuntime {
222    fn drop(&mut self) {
223        if let Some(runtime) = self.inner.get_mut().take() {
224            let _ = runtime.shutdown.send(());
225        }
226    }
227}
228
229pub(crate) fn canonical_object_name(name: &sqlparser::ast::ObjectName) -> Result<String, DbError> {
230    let [part] = name.0.as_slice() else {
231        return Err(DbError::InvalidOperation(format!(
232            "catalog identifiers must be unqualified: {name}"
233        )));
234    };
235    let ident = part.as_ident().ok_or_else(|| {
236        DbError::InvalidOperation(format!(
237            "dynamic catalog identifier is not supported: {name}"
238        ))
239    })?;
240    Ok(ident.value.clone())
241}
242
243pub(crate) fn exact_table_reference(name: &str) -> datafusion::common::TableReference {
244    // `Into<TableReference>` reparses `&str` and lowercases it independently of session config.
245    datafusion::common::TableReference::bare(name)
246}
247
248fn validate_custom_function_name(
249    kind: &str,
250    name: &str,
251    aliases: &[String],
252) -> Result<(), DbError> {
253    const CORE_WINDOW_MARKERS: [&str; 5] = ["tumble", "tumble_end", "hop", "hop_end", "session"];
254    let collision = std::iter::once(name)
255        .chain(aliases.iter().map(String::as_str))
256        .find(|candidate| {
257            CORE_WINDOW_MARKERS
258                .iter()
259                .any(|marker| candidate.eq_ignore_ascii_case(marker))
260        });
261    if let Some(collision) = collision {
262        return Err(DbError::Config(format!(
263            "custom {kind} '{name}' uses reserved CoreWindow marker name or alias '{collision}'"
264        )));
265    }
266    Ok(())
267}
268
269/// The main `LaminarDB` database handle.
270///
271/// Unified interface for SQL execution, data ingestion, and result consumption.
272pub struct LaminarDB {
273    runtime_mode: RuntimeMode,
274    pub(crate) catalog: Arc<SourceCatalog>,
275    pub(crate) planner: parking_lot::Mutex<StreamingPlanner>,
276    pub(crate) ctx: SessionContext,
277    custom_udfs: Vec<datafusion_expr::ScalarUDF>,
278    custom_udafs: Vec<datafusion_expr::AggregateUDF>,
279    pub(crate) config: LaminarConfig,
280    pub(crate) config_vars: Arc<HashMap<String, String>>,
281    pub(crate) shutdown: std::sync::atomic::AtomicBool,
282    pub(crate) coordinator:
283        Arc<tokio::sync::Mutex<Option<crate::checkpoint_coordinator::CheckpointCoordinator>>>,
284    pub(crate) connector_manager: parking_lot::Mutex<crate::connector_manager::ConnectorManager>,
285    pub(crate) connector_registry: Arc<laminar_connectors::registry::ConnectorRegistry>,
286    pub(crate) mv_registry: parking_lot::Mutex<laminar_core::mv::MvRegistry>,
287    pub(crate) table_store: Arc<parking_lot::RwLock<crate::table_store::TableStore>>,
288    pub(crate) state: Arc<std::sync::atomic::AtomicU8>,
289    /// Last recoverable runtime fault, panic, or terminal resource-exhaustion reason;
290    /// cleared on a clean start. Surfaced via `pipeline_status`/`/ready`.
291    pub(crate) last_fault: Arc<parking_lot::Mutex<Option<String>>>,
292    /// Permanent per-instance fence set when catalog cleanup cannot prove that every backing
293    /// registration was removed. Unlike a normal runtime fault, this is never restartable.
294    pub(crate) catalog_cleanup_fenced: std::sync::atomic::AtomicBool,
295    /// Deterministic provider-deregistration failure used to exercise the terminal cleanup fence.
296    #[cfg(test)]
297    pub(crate) catalog_cleanup_deregister_fault: parking_lot::Mutex<Option<String>>,
298    /// Self-ref set by `enable_supervision`; empty `Weak` (default) disables auto-restart.
299    pub(crate) supervisor_self: Arc<parking_lot::Mutex<std::sync::Weak<LaminarDB>>>,
300    /// Auto-restart timestamps within the sliding window; bounds restart storms.
301    pub(crate) restart_history: Arc<parking_lot::Mutex<Vec<Instant>>>,
302    /// Serializes start/stop/shutdown ownership across awaits. Runtime-handle `None` means an
303    /// owner is currently joining only while this lock is held; another caller must never treat
304    /// it as completed teardown.
305    pub(crate) lifecycle_lock: tokio::sync::Mutex<()>,
306    pub(crate) control_runtime: DbControlRuntime,
307    pub(crate) startup_attempt:
308        parking_lot::Mutex<Option<Arc<crate::pipeline_lifecycle::StartupAttempt>>>,
309    /// Serializes topology DDL through manifest persistence; catalog reads take a shared guard so
310    /// they cannot observe a tentative create that may still roll back.
311    pub(crate) topology_ddl_lock: tokio::sync::RwLock<()>,
312    /// One effect-free candidate compiler at a time; rejected callers do not queue more graphs.
313    #[cfg(feature = "cluster")]
314    pub(crate) topology_validation_lock: Arc<tokio::sync::Mutex<()>>,
315    // Only a private, unstarted topology catalog carries a resource-availability snapshot.
316    // Normal databases check their actual transport and ownership handles during DDL admission.
317    #[cfg(feature = "cluster")]
318    pub(crate) topology_planning_ownership_scope: Option<bool>,
319    /// Typed ownership for every user-visible catalog identifier.
320    pub(crate) catalog_namespace: parking_lot::Mutex<HashMap<String, CatalogObjectKind>>,
321    #[cfg(test)]
322    pub(crate) topology_planning_gate:
323        parking_lot::Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
324    #[cfg(test)]
325    pub(crate) stop_after_claim_gate:
326        parking_lot::Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
327    #[cfg(all(test, feature = "cluster"))]
328    pub(crate) catalog_seal_gate:
329        parking_lot::Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
330    /// One-shot fault injection for the compute-thread pre-ready ownership boundary.
331    #[cfg(all(test, feature = "cluster"))]
332    pub(crate) compute_before_ready_panic: Arc<std::sync::atomic::AtomicBool>,
333    /// Kept inside an async mutex while joined. A cancelled stop future drops only the guard,
334    /// never the sole watcher handle, so a retry cannot publish a false terminal state.
335    pub(crate) runtime_handle: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
336    /// OS-released exclusive lock for a local checkpoint namespace. Deployment
337    /// identity prevents reuse after reset; this lock prevents two live processes from writing
338    /// divergent cuts into the same deployment.
339    pub(crate) schema_creation_lock: tokio::sync::Mutex<()>,
340    pub(crate) checkpoint_namespace_lock: parking_lot::Mutex<Option<Arc<std::fs::File>>>,
341    /// Every sink actor in the active generation, retained from spawn until terminal observation.
342    /// A replacement cannot start while any prior actor can still mutate an external system.
343    pub(crate) owned_sink_handles: Arc<parking_lot::Mutex<Vec<crate::sink_task::SinkTaskHandle>>>,
344    /// Every source task in the active or draining generation. A replacement cannot start until
345    /// the stable supervisor has observed actual task exit, including after Tokio cancellation.
346    pub(crate) owned_source_tasks: crate::pipeline::streaming_coordinator::OwnedSourceTasks,
347    /// Connector children admitted before an actor existed remain fenced across startup failure.
348    pub(crate) owned_connector_task_fences: crate::connector_task_fence::OwnedConnectorTaskFences,
349    pub(crate) shutdown_signal: Arc<tokio::sync::Notify>,
350    /// Persistent terminal cancellation for the currently installed compute runtime. Unlike a
351    /// notification permit, cancellation cannot be lost while the coordinator is between awaits.
352    pub(crate) runtime_shutdown: parking_lot::RwLock<tokio_util::sync::CancellationToken>,
353    /// Process-lifetime cancellation for assignment restore I/O. A coordinated recovery stops the
354    /// current compute runtime before a successor assignment can acquire vnodes, so restartable
355    /// runtime cancellation must not make that restore permanently fail.
356    #[cfg(feature = "cluster")]
357    pub(crate) assignment_restore_shutdown: tokio_util::sync::CancellationToken,
358    pub(crate) engine_metrics:
359        parking_lot::Mutex<Option<Arc<crate::engine_metrics::EngineMetrics>>>,
360    /// Process-lifetime, fixed-capacity barrier timing evidence. Pipeline recovery generations share
361    /// this ledger; an OS process restart creates a new sequence domain.
362    #[cfg(feature = "cluster")]
363    pub(crate) checkpoint_barrier_timings:
364        Arc<crate::checkpoint_timing::CheckpointBarrierTimingLedger>,
365    pub(crate) prometheus_registry: parking_lot::Mutex<Option<Arc<prometheus::Registry>>>,
366    pub(crate) start_time: Instant,
367    pub(crate) session_properties: parking_lot::Mutex<HashMap<String, String>>,
368    /// Min of all source watermarks.
369    pub(crate) pipeline_watermark: Arc<std::sync::atomic::AtomicI64>,
370    pub(crate) lookup_registry: Arc<laminar_sql::datafusion::LookupTableRegistry>,
371    /// `None` unless `[ai]`/`[models]` are configured.
372    pub(crate) ai_runtime: Option<Arc<crate::ai::AiRuntime>>,
373    /// Runtime the AI inference workers spawn on; set alongside `ai_runtime`.
374    pub(crate) ai_handle: Option<tokio::runtime::Handle>,
375    /// Live-DDL channel; `None` outside `start..shutdown`.
376    pub(crate) control_tx: parking_lot::Mutex<Option<ControlMsgTx>>,
377    pub(crate) mv_store: Arc<parking_lot::RwLock<crate::mv_store::MvStore>>,
378    /// `None` in embedded mode.
379    #[cfg(feature = "cluster")]
380    pub(crate) cluster_controller:
381        parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::ClusterController>>>,
382    /// When set, the next start restores to this cluster-agreed epoch instead of the
383    /// local latest (taken in `start_inner`).
384    #[cfg(feature = "cluster")]
385    pub(crate) recover_target_epoch: parking_lot::Mutex<Option<u64>>,
386    /// Database-owned recovery supervisor. It outlives restartable pipeline generations and is
387    /// aborted if the facade is dropped without a graceful terminal shutdown.
388    #[cfg(feature = "cluster")]
389    pub(crate) recovery_monitor: parking_lot::Mutex<Option<tokio::task::JoinHandle<()>>>,
390    /// Prevents a timed-out coordinated stop/start from overlapping a later lifecycle attempt.
391    /// Cleared by the owning lifecycle thread only after its future actually finishes.
392    #[cfg(feature = "cluster")]
393    pub(crate) coordinated_lifecycle_active: Arc<std::sync::atomic::AtomicBool>,
394    /// Persistent ownership fence from the first observed recovery fault through the committed
395    /// recovery release. Unlike `coordinated_lifecycle_active`, this spans the gap between a
396    /// completed stop and its durable stopped report, so an operator start cannot invalidate the
397    /// report's quiescence claim.
398    #[cfg(feature = "cluster")]
399    pub(crate) coordinated_recovery_fenced: Arc<std::sync::atomic::AtomicBool>,
400    /// Monotonic process view of a terminal fault durably installed anywhere in this cluster
401    /// authority namespace. Unlike `terminal_pipeline_halt`, this is also set by remote evidence;
402    /// it fences every reopen path but must not prevent healthy peers from quiescing for Prepare.
403    /// Only replacing the entire authority namespace and constructing a new DB instance resets it.
404    #[cfg(feature = "cluster")]
405    pub(crate) durable_terminal_recovery_fence: std::sync::atomic::AtomicBool,
406    /// Process-lifetime fail-closed latch for a deterministic runtime halt. Coordinated recovery
407    /// must never rebuild the same poison input while this process remains alive.
408    pub(crate) terminal_pipeline_halt: Arc<std::sync::atomic::AtomicBool>,
409    /// Opaque local fault request retained through durable publication and cleared only while an
410    /// authorized committed recovery Release is consumed. A newer fault atomically replaces it.
411    #[cfg(feature = "cluster")]
412    pub(crate) pending_recovery_fault: Arc<std::sync::atomic::AtomicU64>,
413    /// Exact-process result of the checkpoint-artifact audit completed before the first clustered
414    /// graph generation starts. Missing or mismatched evidence never suppresses the later fallback
415    /// audit.
416    #[cfg(feature = "cluster")]
417    pub(crate) startup_checkpoint_artifact_audit:
418        parking_lot::Mutex<Option<StartupCheckpointArtifactAudit>>,
419    /// Epoch the most recent start restored from (`None` = started fresh). A rejoin fault
420    /// is reported only when prior local state existed — a fresh joiner lost no window.
421    #[cfg(feature = "cluster")]
422    pub(crate) last_recovery_epoch: parking_lot::Mutex<Option<u64>>,
423    /// Holds source intake closed during a coordinated round until every node has restarted
424    /// and rebound its shuffle receiver (the restore quorum). Sources re-read + re-shuffle the
425    /// replay window on restart; without this gate a node that restarts first shuffles into a
426    /// peer whose receiver isn't up yet and the fire-and-forget frames are lost.
427    #[cfg(feature = "cluster")]
428    pub(crate) source_gate: Arc<std::sync::atomic::AtomicBool>,
429    /// A terminal topology cut is held independently of assignment readiness. Only an
430    /// authorized recovery or topology Release may clear it after retirement and readiness.
431    #[cfg(feature = "cluster")]
432    pub(crate) topology_cut_hold: Arc<std::sync::atomic::AtomicBool>,
433    /// Exact runtime that installed a committed topology; cancellation invalidates its receipts.
434    #[cfg(feature = "cluster")]
435    pub(crate) installed_topology_runtime:
436        parking_lot::Mutex<Option<topology::InstalledTopologyRuntime>>,
437    /// One-way local data-plane fence after stable process-lease loss.
438    #[cfg(feature = "cluster")]
439    pub(crate) cluster_authority_revoked: std::sync::atomic::AtomicBool,
440    /// Serializes source/shuffle authority grants with terminal revocation.
441    #[cfg(feature = "cluster")]
442    pub(crate) cluster_authority_transition: Arc<parking_lot::Mutex<()>>,
443    pub(crate) vnode_registry: parking_lot::Mutex<Option<Arc<laminar_core::state::VnodeRegistry>>>,
444    pub(crate) physical_optimizer_rules:
445        Arc<[Arc<dyn datafusion::physical_optimizer::PhysicalOptimizerRule + Send + Sync>]>,
446    /// `target_partitions` override. Streaming plans currently require one reusable partition.
447    pub(crate) pipeline_target_partitions: Option<usize>,
448    #[cfg(feature = "cluster")]
449    pub(crate) shuffle_sender:
450        parking_lot::Mutex<Option<Arc<laminar_core::shuffle::ShuffleSender>>>,
451    #[cfg(feature = "cluster")]
452    pub(crate) shuffle_receiver:
453        Arc<parking_lot::Mutex<Option<Arc<laminar_core::shuffle::ShuffleReceiver>>>>,
454    #[cfg(feature = "cluster")]
455    pub(crate) decision_store:
456        parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::CheckpointDecisionStore>>>,
457    #[cfg(feature = "cluster")]
458    pub(crate) assignment_snapshot_store:
459        parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::AssignmentSnapshotStore>>>,
460    /// Create-once cluster catalog inventory, sealed at bootstrap and replayed at boot.
461    #[cfg(feature = "cluster")]
462    pub(crate) catalog_manifest_store:
463        parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::CatalogManifestStore>>>,
464    /// Version loaded by exact catalog replay; runtime activation requires Running and release.
465    #[cfg(feature = "cluster")]
466    pub(crate) replayed_topology: parking_lot::Mutex<Option<crate::db::topology::ReplayedTopology>>,
467    /// Pre-built shared checkpoint namespace installed during cluster construction.
468    #[cfg(feature = "cluster")]
469    cluster_checkpoint_object_store: Option<Arc<dyn object_store::ObjectStore>>,
470    /// One immutable assignment-bound vnode revocation batch shared with `OperatorGraph`.
471    #[cfg(feature = "cluster")]
472    pub(crate) pending_vnode_transition:
473        crate::vnode_transition_staging::PendingVnodeTransitionHandle,
474    /// Exact assignment and state ABI installed in the current graph generation.
475    #[cfg(feature = "cluster")]
476    pub(crate) installed_vnode_state: crate::vnode_transition_staging::InstalledVnodeStateHandle,
477    /// Serializes successor preparation with assignment-certificate and checkpoint admission.
478    /// A checkpoint retains an owned claim until its exact Prepare and source barriers are
479    /// installed, so a source drain cannot become durable in the admission-to-Prepare gap.
480    #[cfg(feature = "cluster")]
481    pub(crate) assignment_adoption_lock: Arc<tokio::sync::Mutex<()>>,
482    /// Changes whenever local assignment authority is suspended or invalidated. The snapshot
483    /// watcher uses it to reject a certificate computed from a head read before that closure.
484    #[cfg(feature = "cluster")]
485    pub(crate) assignment_authority_revision: std::sync::atomic::AtomicU64,
486    /// Linearizes assignment publication with compute-cycle entry so staged revocation is applied
487    /// before any row observes the successor assignment.
488    #[cfg(feature = "cluster")]
489    pub(crate) rotation_execution_fence: Arc<tokio::sync::RwLock<()>>,
490    /// Routes `db.checkpoint()` requests to the streaming coordinator for exact-attempt
491    /// barrier admission. `None` means no running pipeline can produce a valid cut.
492    pub(crate) force_ckpt_tx: parking_lot::Mutex<Option<ForceCheckpointTx>>,
493    pub(crate) subscription_registry: Arc<crate::subscription::SubscriptionRegistry>,
494    /// Resolved at `start()`; consulted by SUBSCRIBE WHERE.
495    pub(crate) stream_schemas: parking_lot::RwLock<HashMap<String, arrow_schema::SchemaRef>>,
496}
497
498impl Drop for LaminarDB {
499    fn drop(&mut self) {
500        #[cfg(feature = "cluster")]
501        if let Some(monitor) = self.recovery_monitor.get_mut().take() {
502            monitor.abort();
503        }
504        if matches!(
505            DbState::load(&self.state),
506            DbState::Starting | DbState::Running | DbState::ShuttingDown | DbState::Faulted
507        ) {
508            // Dropping Tokio handles detaches their tasks. A local decision hard-link already in
509            // a blocking filesystem worker could therefore outlive this facade. Keep the OS lock
510            // until process exit rather than let another in-process deployment acquire the same
511            // checkpoint namespace while old work can still publish. Graceful stop/shutdown
512            // clears the lock before reaching a terminal state and does not leak it.
513            if let Some(lock) = self.checkpoint_namespace_lock.get_mut().take() {
514                tracing::error!(
515                    "dropping an active checkpoint writer; retaining its namespace lock until process exit"
516                );
517                std::mem::forget(lock);
518            }
519        }
520    }
521}
522
523#[cfg(feature = "cluster")]
524fn checkpoint_participant_for_runtime(db: &LaminarDB) -> Option<u64> {
525    if !db.is_cluster_runtime() {
526        return None;
527    }
528    db.cluster_controller
529        .lock()
530        .as_ref()
531        .map(|controller| controller.instance_id().0)
532}
533
534#[cfg(feature = "cluster")]
535tokio::task_local! {
536    static CATALOG_MANIFEST_REPLAY: ();
537}
538
539#[cfg(feature = "cluster")]
540tokio::task_local! {
541    static CATALOG_BOOTSTRAP: ();
542}
543
544#[cfg(feature = "cluster")]
545struct CatalogBootstrapGuard<'a> {
546    db: &'a LaminarDB,
547    created: Vec<(String, CatalogObjectKind)>,
548    sealed: bool,
549}
550
551#[cfg(feature = "cluster")]
552impl CatalogBootstrapGuard<'_> {
553    fn record(&mut self, name: String, kind: CatalogObjectKind) {
554        self.created.push((name, kind));
555    }
556
557    fn sealed(mut self) {
558        self.sealed = true;
559    }
560}
561
562#[cfg(feature = "cluster")]
563impl Drop for CatalogBootstrapGuard<'_> {
564    fn drop(&mut self) {
565        if !self.sealed {
566            for (name, kind) in self.created.iter().rev() {
567                self.db
568                    .rollback_catalog_create_or_fence(name, *kind, "catalog bootstrap rollback");
569            }
570        }
571    }
572}
573
574#[cfg(feature = "cluster")]
575pub(crate) fn catalog_manifest_replay_active() -> bool {
576    CATALOG_MANIFEST_REPLAY.try_with(|()| ()).is_ok()
577}
578
579#[cfg(feature = "cluster")]
580fn catalog_bootstrap_active() -> bool {
581    CATALOG_BOOTSTRAP.try_with(|()| ()).is_ok()
582}
583
584#[cfg(not(feature = "cluster"))]
585pub(crate) const fn catalog_manifest_replay_active() -> bool {
586    false
587}
588
589fn is_topology_ddl(statement: &StreamingStatement) -> bool {
590    match statement {
591        StreamingStatement::CreateSource(_)
592        | StreamingStatement::CreateSink(_)
593        | StreamingStatement::CreateContinuousQuery { .. }
594        | StreamingStatement::DropSource { .. }
595        | StreamingStatement::DropSink { .. }
596        | StreamingStatement::DropMaterializedView { .. }
597        | StreamingStatement::CreateMaterializedView { .. }
598        | StreamingStatement::CreateStream { .. }
599        | StreamingStatement::DropStream { .. }
600        | StreamingStatement::AlterSource { .. }
601        | StreamingStatement::CreateLookupTable(_)
602        | StreamingStatement::DropLookupTable { .. } => true,
603        StreamingStatement::Standard(statement) => matches!(
604            statement.as_ref(),
605            sqlparser::ast::Statement::CreateTable(_)
606                | sqlparser::ast::Statement::Drop {
607                    object_type: sqlparser::ast::ObjectType::Table,
608                    ..
609                }
610                | sqlparser::ast::Statement::AlterTable { .. }
611        ),
612        _ => false,
613    }
614}
615
616fn mutates_database(statement: &StreamingStatement) -> bool {
617    is_topology_ddl(statement)
618        || matches!(
619            statement,
620            StreamingStatement::InsertInto { .. }
621                | StreamingStatement::Checkpoint
622                | StreamingStatement::RestoreCheckpoint { .. }
623        )
624        || matches!(
625            statement,
626            StreamingStatement::Standard(statement)
627                if matches!(statement.as_ref(), sqlparser::ast::Statement::Set(_))
628        )
629}
630
631fn reads_catalog(statement: &StreamingStatement) -> bool {
632    matches!(
633        statement,
634        StreamingStatement::Standard(statement)
635            if matches!(statement.as_ref(), sqlparser::ast::Statement::Query(_))
636    ) || matches!(statement, StreamingStatement::TemporalProbeQuery { .. })
637        || matches!(
638            statement,
639            StreamingStatement::InsertInto { .. }
640                | StreamingStatement::Show(_)
641                | StreamingStatement::Describe { .. }
642                | StreamingStatement::Explain { .. }
643        )
644}
645
646pub(crate) fn catalog_create_identity(
647    statement: &StreamingStatement,
648) -> Result<Option<(String, CatalogObjectKind, &'static str)>, DbError> {
649    let identity = match statement {
650        StreamingStatement::CreateSource(create) => (
651            canonical_object_name(&create.name)?,
652            CatalogObjectKind::Source,
653            "CREATE SOURCE",
654        ),
655        StreamingStatement::CreateSink(create) => (
656            canonical_object_name(&create.name)?,
657            CatalogObjectKind::Sink,
658            "CREATE SINK",
659        ),
660        StreamingStatement::CreateMaterializedView { name, .. } => (
661            canonical_object_name(name)?,
662            CatalogObjectKind::MaterializedView,
663            "CREATE MATERIALIZED VIEW",
664        ),
665        StreamingStatement::CreateStream { name, .. } => (
666            canonical_object_name(name)?,
667            CatalogObjectKind::Stream,
668            "CREATE STREAM",
669        ),
670        StreamingStatement::CreateLookupTable(create) => (
671            canonical_object_name(&create.name)?,
672            CatalogObjectKind::LookupTable,
673            "CREATE LOOKUP TABLE",
674        ),
675        StreamingStatement::Standard(statement) => {
676            let sqlparser::ast::Statement::CreateTable(create) = statement.as_ref() else {
677                return Ok(None);
678            };
679            (
680                canonical_object_name(&create.name)?,
681                CatalogObjectKind::Table,
682                "CREATE TABLE",
683            )
684        }
685        _ => return Ok(None),
686    };
687    Ok(Some(identity))
688}
689
690#[cfg(feature = "cluster")]
691fn validate_cluster_catalog_create(
692    db: &LaminarDB,
693    sql: &str,
694    statement: &StreamingStatement,
695) -> Result<(String, CatalogObjectKind, &'static str), DbError> {
696    if let StreamingStatement::CreateStream {
697        query_sql,
698        emit_clause,
699        retention_bytes: Some(_),
700        ..
701    } = statement
702    {
703        let output = crate::subscription::distribution::plan(
704            query_sql,
705            emit_clause.as_ref(),
706            &db.ctx,
707            db.checkpoint_key_groups(),
708        )?;
709        if !output.as_ref().is_some_and(
710            crate::subscription::distribution::PlannedSubscriptionOutput::is_vnode_partitioned,
711        ) {
712            return Err(
713                crate::subscription::ClusterSubscriptionError::UnsupportedPlan {
714                    reason:
715                        "durable cluster history requires a non-windowed managed keyed aggregate"
716                            .into(),
717                }
718                .into(),
719            );
720        }
721    }
722    if catalog_ddl_contains_comment(sql)? {
723        return Err(DbError::InvalidOperation(
724            "cluster catalog DDL cannot persist SQL comments; submit one canonical typed definition"
725                .into(),
726        ));
727    }
728    if let Some(key) = sensitive_catalog_property(statement) {
729        return Err(DbError::InvalidOperation(format!(
730            "cluster catalog DDL cannot persist secret property '{key}'; use $${{ENV_VAR}} in server TOML or ${{ENV_VAR}} through the SQL API (without a default), or omit it for a connector environment fallback"
731        )));
732    }
733    let identity = catalog_create_identity(statement)?.ok_or_else(|| {
734        DbError::InvalidOperation(
735            "cluster catalog bootstrap accepts only reversible typed CREATE statements".into(),
736        )
737    })?;
738    if matches!(
739        identity.1,
740        CatalogObjectKind::Table | CatalogObjectKind::LookupTable
741    ) {
742        return Err(DbError::InvalidOperation(format!(
743            "{} is not supported in cluster mode until reference-table rows and source positions are atomically replicated through distributed state",
744            identity.2
745        )));
746    }
747    Ok(identity)
748}
749
750pub(crate) fn uri_contains_unsupported_secret(value: &str, allow_reference: bool) -> bool {
751    if value.contains("://") {
752        return laminar_connectors::security::value_contains_uri_secret(value, allow_reference);
753    }
754    {
755        let lower = value.to_ascii_lowercase();
756        lower.split_whitespace().any(|part| {
757            part.strip_prefix("password=")
758                .or_else(|| part.strip_prefix("pwd="))
759                .is_some_and(|password| {
760                    let password = password.trim_matches(|ch| ch == '\'' || ch == '"');
761                    !(password.is_empty()
762                        || allow_reference
763                            && laminar_connectors::security::is_env_reference(password))
764                })
765        })
766    }
767}
768
769pub(crate) fn catalog_property_contains_unsupported_secret(
770    key: &str,
771    value: &str,
772    allow_reference: bool,
773) -> bool {
774    let lower = key.to_ascii_lowercase();
775    let normalized = lower.replace(['.', '-'], "_");
776    let secret_key = laminar_connectors::security::is_secret_option_key(key);
777    if secret_key {
778        return !(allow_reference && laminar_connectors::security::is_env_reference(value));
779    }
780    if value.contains("://")
781        && laminar_connectors::security::value_contains_uri_secret(value, allow_reference)
782    {
783        return true;
784    }
785    (normalized.contains("connection")
786        || normalized == "uri"
787        || normalized.ends_with("_uri")
788        || normalized == "url"
789        || normalized.ends_with("_url")
790        || normalized == "dsn")
791        && uri_contains_unsupported_secret(value, allow_reference)
792}
793
794pub(crate) fn catalog_ddl_contains_comment(sql: &str) -> Result<bool, DbError> {
795    use sqlparser::tokenizer::{Token, Tokenizer, Whitespace};
796
797    let tokens = Tokenizer::new(&sqlparser::dialect::GenericDialect {}, sql)
798        .tokenize()
799        .map_err(|error| {
800            DbError::InvalidOperation(format!("catalog DDL tokenization failed: {error}"))
801        })?;
802    Ok(tokens.into_iter().any(|token| {
803        matches!(
804            token,
805            Token::Whitespace(
806                Whitespace::SingleLineComment { .. } | Whitespace::MultiLineComment(_)
807            )
808        )
809    }))
810}
811
812pub(crate) fn sensitive_catalog_property(statement: &StreamingStatement) -> Option<String> {
813    fn find<'a>(
814        options: impl IntoIterator<Item = (&'a String, &'a String)>,
815        allow_reference: bool,
816    ) -> Option<String> {
817        options
818            .into_iter()
819            .find(|(key, value)| {
820                catalog_property_contains_unsupported_secret(key, value, allow_reference)
821            })
822            .map(|(key, _)| key.clone())
823    }
824
825    match statement {
826        StreamingStatement::CreateSource(create) => find(create.with_options.iter(), true)
827            .or_else(|| find(create.connector_options.iter(), true))
828            .or_else(|| {
829                create
830                    .format
831                    .as_ref()
832                    .and_then(|format| find(format.options.iter(), true))
833            }),
834        StreamingStatement::CreateSink(create) => find(create.connector_options.iter(), true)
835            .or_else(|| {
836                create
837                    .format
838                    .as_ref()
839                    .and_then(|format| find(format.options.iter(), true))
840            }),
841        StreamingStatement::CreateLookupTable(create) => find(create.with_options.iter(), false),
842        StreamingStatement::Standard(statement) => {
843            let sqlparser::ast::Statement::CreateTable(create) = statement.as_ref() else {
844                return None;
845            };
846            let sqlparser::ast::CreateTableOptions::With(options) = &create.table_options else {
847                return None;
848            };
849            options.iter().find_map(|option| {
850                let sqlparser::ast::SqlOption::KeyValue { key, value } = option else {
851                    return None;
852                };
853                let key = key.to_string();
854                let value = value.to_string();
855                catalog_property_contains_unsupported_secret(&key, value.trim_matches('\''), false)
856                    .then_some(key)
857            })
858        }
859        _ => None,
860    }
861}
862
863#[cfg(feature = "cluster")]
864fn connector_source_requires_schema_discovery(statement: &StreamingStatement) -> bool {
865    let StreamingStatement::CreateSource(create) = statement else {
866        return false;
867    };
868    create.connector_type.is_some() && create.columns.is_empty()
869}
870
871#[cfg(not(feature = "cluster"))]
872const fn checkpoint_participant_for_runtime(_db: &LaminarDB) -> Option<u64> {
873    None
874}
875
876/// Reply channel for a single `db.checkpoint()` request.
877pub(crate) type ForceCheckpointReply =
878    crossfire::oneshot::TxOneshot<Result<crate::checkpoint_coordinator::CheckpointResult, DbError>>;
879
880const FORCE_CHECKPOINT_RESERVATION_WAITING: u8 = 0;
881const FORCE_CHECKPOINT_RESERVATION_CLAIMED: u8 = 1;
882const FORCE_CHECKPOINT_RESERVATION_CANCELLED: u8 = 2;
883
884/// Linearizable ownership claim for one not-yet-reserved manual-checkpoint request.
885///
886/// The coordinator and caller race one atomic transition out of `WAITING`. The wake channel only
887/// makes a won coordinator claim promptly observable; it is not itself the ownership boundary.
888pub(crate) struct ForceCheckpointReservationClaim {
889    state: Arc<std::sync::atomic::AtomicU8>,
890    wake: tokio::sync::oneshot::Sender<()>,
891}
892
893pub(crate) struct ForceCheckpointReservationWait {
894    state: Arc<std::sync::atomic::AtomicU8>,
895    wake: tokio::sync::oneshot::Receiver<()>,
896}
897
898#[derive(Clone, Copy, Debug, PartialEq, Eq)]
899pub(crate) enum ForceCheckpointReservationAttachment {
900    Attached,
901    Expired,
902    Cancelled,
903}
904
905impl ForceCheckpointReservationClaim {
906    pub(crate) fn new() -> (Self, ForceCheckpointReservationWait) {
907        let state = Arc::new(std::sync::atomic::AtomicU8::new(
908            FORCE_CHECKPOINT_RESERVATION_WAITING,
909        ));
910        let (wake_tx, wake_rx) = tokio::sync::oneshot::channel();
911        (
912            Self {
913                state: Arc::clone(&state),
914                wake: wake_tx,
915            },
916            ForceCheckpointReservationWait {
917                state,
918                wake: wake_rx,
919            },
920        )
921    }
922
923    /// Claim the coordinator's bounded exact-reservation operation. This transition occurs before
924    /// durable ID reservation, whose implementation is itself hard-deadline-bounded.
925    pub(crate) fn try_claim(&self) -> bool {
926        self.state
927            .compare_exchange(
928                FORCE_CHECKPOINT_RESERVATION_WAITING,
929                FORCE_CHECKPOINT_RESERVATION_CLAIMED,
930                std::sync::atomic::Ordering::AcqRel,
931                std::sync::atomic::Ordering::Acquire,
932            )
933            .is_ok()
934    }
935
936    /// Attach this waiter after durable ID reservation. Ordinary admission is already CLAIMED.
937    /// A caller coalesced onto owned HANDOFF replay can still be WAITING, so attachment atomically
938    /// classifies an elapsed deadline even when that caller's timer task has not been polled yet.
939    pub(crate) fn attach(
940        self,
941        deadline: tokio::time::Instant,
942        now: tokio::time::Instant,
943    ) -> ForceCheckpointReservationAttachment {
944        use ForceCheckpointReservationAttachment::{Attached, Cancelled, Expired};
945
946        let attachment = match self.state.load(std::sync::atomic::Ordering::Acquire) {
947            FORCE_CHECKPOINT_RESERVATION_CLAIMED => Attached,
948            FORCE_CHECKPOINT_RESERVATION_WAITING if now >= deadline => {
949                match self.state.compare_exchange(
950                    FORCE_CHECKPOINT_RESERVATION_WAITING,
951                    FORCE_CHECKPOINT_RESERVATION_CANCELLED,
952                    std::sync::atomic::Ordering::AcqRel,
953                    std::sync::atomic::Ordering::Acquire,
954                ) {
955                    Ok(_) => Expired,
956                    Err(FORCE_CHECKPOINT_RESERVATION_CLAIMED) => Attached,
957                    Err(_) => Cancelled,
958                }
959            }
960            FORCE_CHECKPOINT_RESERVATION_WAITING => match self.state.compare_exchange(
961                FORCE_CHECKPOINT_RESERVATION_WAITING,
962                FORCE_CHECKPOINT_RESERVATION_CLAIMED,
963                std::sync::atomic::Ordering::AcqRel,
964                std::sync::atomic::Ordering::Acquire,
965            ) {
966                Ok(_) | Err(FORCE_CHECKPOINT_RESERVATION_CLAIMED) => Attached,
967                Err(_) => Cancelled,
968            },
969            _ => Cancelled,
970        };
971        if attachment == Attached {
972            let _ = self.wake.send(());
973        }
974        attachment
975    }
976}
977
978impl Drop for ForceCheckpointReservationWait {
979    fn drop(&mut self) {
980        // External future cancellation participates in the same ownership race as the public
981        // deadline. Once CLAIMED, dropping the caller cannot revoke the bounded reservation work.
982        let _ = self.state.compare_exchange(
983            FORCE_CHECKPOINT_RESERVATION_WAITING,
984            FORCE_CHECKPOINT_RESERVATION_CANCELLED,
985            std::sync::atomic::Ordering::AcqRel,
986            std::sync::atomic::Ordering::Acquire,
987        );
988    }
989}
990
991impl ForceCheckpointReservationWait {
992    /// Returns true only when the public deadline wins before exact-attempt ownership.
993    fn cancel_before_reservation(&self) -> bool {
994        self.state
995            .compare_exchange(
996                FORCE_CHECKPOINT_RESERVATION_WAITING,
997                FORCE_CHECKPOINT_RESERVATION_CANCELLED,
998                std::sync::atomic::Ordering::AcqRel,
999                std::sync::atomic::Ordering::Acquire,
1000            )
1001            .is_ok()
1002    }
1003
1004    fn is_claimed(&self) -> bool {
1005        self.state.load(std::sync::atomic::Ordering::Acquire)
1006            == FORCE_CHECKPOINT_RESERVATION_CLAIMED
1007    }
1008
1009    pub(crate) async fn wait(&mut self) -> Result<(), tokio::sync::oneshot::error::RecvError> {
1010        (&mut self.wake).await
1011    }
1012}
1013
1014/// One exact manual-checkpoint request and its caller-owned end-to-end deadline.
1015///
1016/// Dropping the reply receiver cancels admission while the request is still queued. Once an exact
1017/// attempt has been reserved, the coordinator completes or cleans up that attempt before replying.
1018pub(crate) struct ForceCheckpointRequest {
1019    pub(crate) reply: ForceCheckpointReply,
1020    /// Present until the first exact attempt adopts this request. A HANDOFF replay can move an
1021    /// already-acknowledged request back to the waiting queue, so this is intentionally optional.
1022    pub(crate) reservation_claim: Option<ForceCheckpointReservationClaim>,
1023    pub(crate) deadline: tokio::time::Instant,
1024}
1025
1026pub(crate) type ForceCheckpointTx =
1027    crossfire::MAsyncTx<crossfire::mpsc::Array<ForceCheckpointRequest>>;
1028
1029pub(crate) type ForceCheckpointRx =
1030    crossfire::AsyncRx<crossfire::mpsc::Array<ForceCheckpointRequest>>;
1031
1032pub(crate) const FORCE_CHECKPOINT_CHANNEL_CAPACITY: usize = 64;
1033
1034/// Keep rows at/after the watermark. `Ok(None)` = all rows late;
1035/// `Err` = schema drift (missing/non-timestamp column).
1036pub(crate) fn filter_late_rows(
1037    batch: &RecordBatch,
1038    column: &str,
1039    watermark: i64,
1040) -> Result<Option<RecordBatch>, laminar_core::time::FilterError> {
1041    laminar_core::time::filter_batch_by_timestamp(
1042        batch,
1043        column,
1044        watermark,
1045        laminar_core::time::ThresholdOp::GreaterEq,
1046    )
1047}
1048
1049pub(crate) use laminar_core::time::parse_duration_str;
1050
1051/// Summary of a single [`LaminarDB::adopt_assignment_snapshot`] call.
1052#[cfg(feature = "cluster")]
1053#[derive(Debug, Default)]
1054pub struct SnapshotAdoption {
1055    /// `false` when the snapshot was stale or no registry was installed.
1056    pub adopted: bool,
1057    /// The snapshot version considered.
1058    pub version: u64,
1059    /// The target topology was published only to admit a coordinated restore of its exact handoff
1060    /// checkpoint. No live vnode transition was staged and source intake remains closed.
1061    pub recovery_required: bool,
1062}
1063
1064#[cfg(feature = "cluster")]
1065#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1066enum AssignmentAdoptionMode {
1067    LiveTransition,
1068    ColdRecovery,
1069    /// Publish only the next authority-audited topology after coordinated recovery has completely
1070    /// stopped this graph: an ownerless Commit acquisition, an exact stopped-owner Commit
1071    /// (including final owner exit), an exact Abort rollback reported by a stopped owner/evidence
1072    /// process, or an exact recovery successor whose pinned cut still belongs to the stopped
1073    /// predecessor. The successor graph restores the durable cut at startup.
1074    StoppedRecoveryTopology,
1075}
1076
1077/// Result of certifying a clustered process at startup.
1078#[cfg(feature = "cluster")]
1079#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1080pub enum ClusterStartupDisposition {
1081    /// This process owns vnodes and may serve its certified assignment.
1082    Serving,
1083    /// This process owns no vnodes and remains data-plane fenced while watching assignments.
1084    Idle,
1085    /// A coordinated recovery must release this process before it may serve data.
1086    RecoveryFenced,
1087}
1088
1089/// Result of the checkpoint-artifact audit performed before this exact process starts its graph.
1090///
1091/// A clean audit is process-bound evidence that a later active inventory can belong to live
1092/// checkpoint work admitted after startup. An inventory observed on the pre-start side is instead
1093/// retained as a recovery requirement through the later startup-authority gate.
1094#[cfg(feature = "cluster")]
1095#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1096pub(crate) enum StartupCheckpointArtifactAudit {
1097    /// No unresolved inventory existed before this process started its graph.
1098    Clean(laminar_core::cluster::control::LocalProcessAuthorityIdentity),
1099    /// An unresolved inventory existed before this process started its graph.
1100    Artifacts(laminar_core::cluster::control::LocalProcessAuthorityIdentity),
1101}
1102
1103#[cfg(feature = "cluster")]
1104impl StartupCheckpointArtifactAudit {
1105    /// Exact process authority that bracketed the durable artifact read.
1106    pub(crate) const fn process(
1107        self,
1108    ) -> laminar_core::cluster::control::LocalProcessAuthorityIdentity {
1109        match self {
1110            Self::Clean(process) | Self::Artifacts(process) => process,
1111        }
1112    }
1113}
1114
1115/// Result of publishing one locally serialized assignment-authority certificate.
1116#[cfg(feature = "cluster")]
1117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1118pub(crate) struct AssignmentAuthorityActivation {
1119    /// The exact certificate was installed and published.
1120    pub(crate) installed: bool,
1121    /// Source intake was opened; recovery keeps it closed even after installation.
1122    pub(crate) intake_open: bool,
1123    /// Authority revision observed at the local publication point.
1124    pub(crate) revision: u64,
1125}
1126
1127#[cfg(feature = "cluster")]
1128fn assignment_transition_allows_activation(
1129    fence: &laminar_core::checkpoint::CheckpointAssignmentFence,
1130    requested: Option<&laminar_core::checkpoint::AssignmentDrainTransition>,
1131    installed: Option<&laminar_core::checkpoint::AssignmentDrainTransition>,
1132) -> Result<bool, DbError> {
1133    if requested.is_some_and(|transition| transition.predecessor != *fence) {
1134        return Err(DbError::Checkpoint(
1135            "assignment drain transition does not bind the installed predecessor certificate"
1136                .into(),
1137        ));
1138    }
1139    Ok(requested.is_some() || installed.is_none())
1140}
1141
1142#[cfg(feature = "cluster")]
1143fn owned_vnode_indices(
1144    assignment: &[laminar_core::state::NodeId],
1145    self_id: laminar_core::state::NodeId,
1146) -> Result<Vec<u32>, DbError> {
1147    assignment
1148        .iter()
1149        .enumerate()
1150        .filter(|(_, owner)| **owner == self_id)
1151        .map(|(vnode, _)| {
1152            u32::try_from(vnode).map_err(|_| {
1153                DbError::Checkpoint(
1154                    "vnode assignment is too large to encode a u32 vnode identifier".into(),
1155                )
1156            })
1157        })
1158        .collect()
1159}
1160
1161#[cfg(feature = "cluster")]
1162fn local_state_lacks_exact_predecessor_binding(
1163    observed_assignment_version: u64,
1164    predecessor: Option<&laminar_core::checkpoint::CheckpointAssignmentFence>,
1165    pipeline_identity: Option<&laminar_core::checkpoint::PipelineIdentity>,
1166    installed: Option<&crate::vnode_transition_staging::InstalledVnodeStateBinding>,
1167) -> bool {
1168    observed_assignment_version != 0
1169        && !predecessor
1170            .zip(pipeline_identity)
1171            .zip(installed)
1172            .is_some_and(|((assignment, pipeline_identity), installed)| {
1173                installed.matches(assignment, pipeline_identity)
1174            })
1175}
1176
1177#[cfg(feature = "cluster")]
1178const fn predecessor_readiness_withdrawal_required(
1179    requires_predecessor_binding: bool,
1180    observed_assignment_version: u64,
1181) -> bool {
1182    requires_predecessor_binding && observed_assignment_version != 0
1183}
1184
1185impl LaminarDB {
1186    pub(crate) const fn runtime_mode(&self) -> RuntimeMode {
1187        self.runtime_mode
1188    }
1189
1190    /// Whether this instance was constructed with distributed runtime semantics.
1191    pub(crate) const fn is_cluster_runtime(&self) -> bool {
1192        self.runtime_mode().is_cluster()
1193    }
1194
1195    /// Vnodes revoked by a rebalance and staged for operator-state drop on the next cycle; drained
1196    /// once the compute thread applies the revoke. Observability/test hook.
1197    #[cfg(feature = "cluster")]
1198    #[doc(hidden)]
1199    #[must_use]
1200    pub fn pending_revoke_vnode_count(&self) -> usize {
1201        self.pending_vnode_transition
1202            .lock()
1203            .as_ref()
1204            .map_or(0, |transition| transition.revoked_vnodes().len())
1205    }
1206
1207    /// Whether operator execution has not yet completed the prior assignment's vnode transition.
1208    /// Lock order matches assignment publication and graph checkpoint inspection.
1209    #[cfg(feature = "cluster")]
1210    pub(crate) fn has_unapplied_vnode_transition(&self) -> bool {
1211        self.pending_vnode_transition.lock().is_some()
1212    }
1213
1214    /// Whether the current graph has completely installed this assignment's managed vnode state.
1215    /// A process without a checkpoint coordinator has no managed pipeline identity to bind. Once
1216    /// an identity exists, readiness requires the exact assignment-and-identity success marker.
1217    #[cfg(feature = "cluster")]
1218    pub(crate) async fn local_vnode_state_is_ready(
1219        &self,
1220        registry: &laminar_core::state::VnodeRegistry,
1221        assignment: &laminar_core::checkpoint::CheckpointAssignmentFence,
1222    ) -> Result<bool, DbError> {
1223        // A Created/Starting graph has not restored its checkpoint state yet. In particular, a
1224        // replacement may publish its assignment before public startup; coordinator=None must not
1225        // be mistaken for an installed managed-state generation during that interval.
1226        if DbState::load(&self.state) != DbState::Running {
1227            return Ok(false);
1228        }
1229        let local_assignment = registry.versioned_snapshot();
1230        let owner_ids: Vec<u64> = local_assignment
1231            .owners()
1232            .iter()
1233            .map(|owner| owner.0)
1234            .collect();
1235        if !assignment.is_canonical()
1236            || assignment.assignment_version != local_assignment.version()
1237            || !assignment.matches_owner_map(&owner_ids)
1238        {
1239            return Ok(false);
1240        }
1241        if self.has_unapplied_vnode_transition() {
1242            return Ok(false);
1243        }
1244        let pipeline_identity = self
1245            .coordinator
1246            .lock()
1247            .await
1248            .as_ref()
1249            .map(crate::checkpoint_coordinator::CheckpointCoordinator::bound_pipeline_identity)
1250            .transpose()?;
1251        Ok(pipeline_identity.as_ref().is_none_or(|pipeline_identity| {
1252            self.installed_vnode_state
1253                .lock()
1254                .as_ref()
1255                .is_some_and(|installed| installed.matches(assignment, pipeline_identity))
1256        }))
1257    }
1258
1259    /// Build this process's exact assignment identity and semantic vnode-state readiness report.
1260    #[cfg(feature = "cluster")]
1261    pub(crate) fn local_vnode_state_report(
1262        &self,
1263        controller: &laminar_core::cluster::control::ClusterController,
1264        assignment: &laminar_core::state::VnodeAssignmentSnapshot,
1265        vnode_state_ready: bool,
1266    ) -> Result<laminar_core::checkpoint::CheckpointAssignmentAdoption, DbError> {
1267        let owner_ids: Vec<u64> = assignment.owners().iter().map(|owner| owner.0).collect();
1268        let vnode_count = u32::try_from(owner_ids.len()).map_err(|_| {
1269            DbError::Checkpoint("local vnode-state report owner count exceeds u32::MAX".into())
1270        })?;
1271        let report = laminar_core::checkpoint::CheckpointAssignmentAdoption {
1272            participant: laminar_core::checkpoint::CheckpointParticipant {
1273                node_id: controller.instance_id().0,
1274                boot_incarnation: controller.recovery_incarnation(),
1275            },
1276            assignment_version: assignment.version(),
1277            partitioning_abi_version: laminar_core::state::PARTITIONING_ABI_VERSION,
1278            vnode_count,
1279            assignment_digest:
1280                laminar_core::checkpoint::CheckpointAssignmentFence::owner_map_digest(
1281                    vnode_count,
1282                    &owner_ids,
1283                ),
1284            vnode_state_ready,
1285        };
1286        Ok(report)
1287    }
1288
1289    /// Durably publish a local vnode-state report. A `false` report is the withdrawal that must
1290    /// precede exposing a new pending transition for an already-reported assignment.
1291    #[cfg(feature = "cluster")]
1292    pub(crate) async fn publish_local_vnode_state_report(
1293        &self,
1294        controller: &laminar_core::cluster::control::ClusterController,
1295        assignment: &laminar_core::state::VnodeAssignmentSnapshot,
1296        vnode_state_ready: bool,
1297    ) -> Result<laminar_core::checkpoint::CheckpointAssignmentAdoption, DbError> {
1298        let report = self.local_vnode_state_report(controller, assignment, vnode_state_ready)?;
1299        controller
1300            .announce_adopted_assignment(&report)
1301            .await
1302            .map_err(|error| {
1303                DbError::Checkpoint(format!(
1304                    "could not publish local vnode-state readiness for assignment {}: {error}",
1305                    assignment.version()
1306                ))
1307            })?;
1308        Ok(report)
1309    }
1310
1311    /// Create an embedded in-memory database with default settings.
1312    ///
1313    /// # Errors
1314    ///
1315    /// Returns `DbError` if `DataFusion` context creation fails.
1316    pub fn open() -> Result<Arc<Self>, DbError> {
1317        Self::open_with_config(LaminarConfig::default())
1318    }
1319
1320    /// Create with custom configuration.
1321    ///
1322    /// # Errors
1323    ///
1324    /// Returns `DbError` if the configuration is invalid or `DataFusion` context creation fails.
1325    pub fn open_with_config(config: LaminarConfig) -> Result<Arc<Self>, DbError> {
1326        let db = Self::open_with_config_and_vars(config, HashMap::new())?;
1327        db.connector_registry.freeze();
1328        Ok(Arc::new(db))
1329    }
1330
1331    /// Create with custom configuration and config variables for SQL substitution.
1332    ///
1333    /// # Errors
1334    ///
1335    /// Returns `DbError` if the configuration is invalid or `DataFusion` context creation fails.
1336    #[allow(clippy::unnecessary_wraps)]
1337    pub(crate) fn open_with_config_and_vars(
1338        config: LaminarConfig,
1339        config_vars: HashMap<String, String>,
1340    ) -> Result<Self, DbError> {
1341        Self::open_with_config_and_vars_and_rules(
1342            config,
1343            config_vars,
1344            &[],
1345            None,
1346            RuntimeMode::Local,
1347        )
1348    }
1349
1350    /// Same as [`Self::open_with_config_and_vars`] but also installs
1351    /// the given physical-optimizer rules on the `DataFusion` session.
1352    #[allow(clippy::unnecessary_wraps)]
1353    #[allow(clippy::too_many_lines)] // flat field-init of a large struct
1354    pub(crate) fn open_with_config_and_vars_and_rules(
1355        mut config: LaminarConfig,
1356        config_vars: HashMap<String, String>,
1357        extra_optimizer_rules: &[Arc<
1358            dyn datafusion::physical_optimizer::PhysicalOptimizerRule + Send + Sync,
1359        >],
1360        target_partitions: Option<usize>,
1361        runtime_mode: RuntimeMode,
1362    ) -> Result<Self, DbError> {
1363        config.validate_and_normalize()?;
1364
1365        // One-time crossfire backoff tuning; idempotent, only helps single-core VMs.
1366        crossfire::detect_backoff_cfg();
1367
1368        let lookup_registry = Arc::new(laminar_sql::datafusion::LookupTableRegistry::new());
1369
1370        let ctx = session::create_context(
1371            &config,
1372            Arc::clone(&lookup_registry),
1373            extra_optimizer_rules,
1374            target_partitions,
1375        )?;
1376
1377        let catalog = Arc::new(SourceCatalog::from_config(&config));
1378
1379        let connector_registry = Arc::new(laminar_connectors::registry::ConnectorRegistry::new());
1380        Self::register_builtin_connectors(&connector_registry)?;
1381
1382        Ok(Self {
1383            runtime_mode,
1384            catalog,
1385            planner: parking_lot::Mutex::new(StreamingPlanner::new()),
1386            ctx,
1387            custom_udfs: Vec::new(),
1388            custom_udafs: Vec::new(),
1389            config_vars: Arc::new(config_vars),
1390            shutdown: std::sync::atomic::AtomicBool::new(false),
1391            coordinator: Arc::new(tokio::sync::Mutex::new(None)),
1392            connector_manager: parking_lot::Mutex::new(
1393                crate::connector_manager::ConnectorManager::new(),
1394            ),
1395            connector_registry,
1396            mv_registry: parking_lot::Mutex::new(laminar_core::mv::MvRegistry::new()),
1397            table_store: Arc::new(parking_lot::RwLock::new(
1398                crate::table_store::TableStore::from_config(&config),
1399            )),
1400            mv_store: Arc::new(parking_lot::RwLock::new(
1401                crate::mv_store::MvStore::from_config(&config),
1402            )),
1403            config,
1404            state: Arc::new(std::sync::atomic::AtomicU8::new(DbState::Created as u8)),
1405            last_fault: Arc::new(parking_lot::Mutex::new(None)),
1406            catalog_cleanup_fenced: std::sync::atomic::AtomicBool::new(false),
1407            #[cfg(test)]
1408            catalog_cleanup_deregister_fault: parking_lot::Mutex::new(None),
1409            supervisor_self: Arc::new(parking_lot::Mutex::new(std::sync::Weak::new())),
1410            restart_history: Arc::new(parking_lot::Mutex::new(Vec::new())),
1411            lifecycle_lock: tokio::sync::Mutex::new(()),
1412            control_runtime: DbControlRuntime::new(),
1413            startup_attempt: parking_lot::Mutex::new(None),
1414            topology_ddl_lock: tokio::sync::RwLock::new(()),
1415            #[cfg(feature = "cluster")]
1416            topology_validation_lock: Arc::new(tokio::sync::Mutex::new(())),
1417            #[cfg(feature = "cluster")]
1418            topology_planning_ownership_scope: None,
1419            catalog_namespace: parking_lot::Mutex::new(HashMap::new()),
1420            #[cfg(test)]
1421            topology_planning_gate: parking_lot::Mutex::new(None),
1422            #[cfg(test)]
1423            stop_after_claim_gate: parking_lot::Mutex::new(None),
1424            #[cfg(all(test, feature = "cluster"))]
1425            catalog_seal_gate: parking_lot::Mutex::new(None),
1426            #[cfg(all(test, feature = "cluster"))]
1427            compute_before_ready_panic: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1428            runtime_handle: tokio::sync::Mutex::new(None),
1429            schema_creation_lock: tokio::sync::Mutex::new(()),
1430            checkpoint_namespace_lock: parking_lot::Mutex::new(None),
1431            owned_sink_handles: Arc::new(parking_lot::Mutex::new(Vec::new())),
1432            owned_source_tasks: Arc::new(parking_lot::Mutex::new(Vec::new())),
1433            owned_connector_task_fences: Arc::new(parking_lot::Mutex::new(Vec::new())),
1434            shutdown_signal: Arc::new(tokio::sync::Notify::new()),
1435            runtime_shutdown: parking_lot::RwLock::new(tokio_util::sync::CancellationToken::new()),
1436            #[cfg(feature = "cluster")]
1437            assignment_restore_shutdown: tokio_util::sync::CancellationToken::new(),
1438            engine_metrics: parking_lot::Mutex::new(None),
1439            #[cfg(feature = "cluster")]
1440            checkpoint_barrier_timings: Arc::new(
1441                crate::checkpoint_timing::CheckpointBarrierTimingLedger::new(),
1442            ),
1443            prometheus_registry: parking_lot::Mutex::new(None),
1444            start_time: Instant::now(),
1445            session_properties: parking_lot::Mutex::new(HashMap::new()),
1446            pipeline_watermark: Arc::new(std::sync::atomic::AtomicI64::new(i64::MIN)),
1447            lookup_registry,
1448            ai_runtime: None,
1449            ai_handle: None,
1450            control_tx: parking_lot::Mutex::new(None),
1451            #[cfg(feature = "cluster")]
1452            cluster_controller: parking_lot::Mutex::new(None),
1453            #[cfg(feature = "cluster")]
1454            recover_target_epoch: parking_lot::Mutex::new(None),
1455            #[cfg(feature = "cluster")]
1456            recovery_monitor: parking_lot::Mutex::new(None),
1457            #[cfg(feature = "cluster")]
1458            coordinated_lifecycle_active: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1459            #[cfg(feature = "cluster")]
1460            coordinated_recovery_fenced: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1461            #[cfg(feature = "cluster")]
1462            durable_terminal_recovery_fence: std::sync::atomic::AtomicBool::new(false),
1463            terminal_pipeline_halt: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1464            #[cfg(feature = "cluster")]
1465            pending_recovery_fault: Arc::new(std::sync::atomic::AtomicU64::new(0)),
1466            #[cfg(feature = "cluster")]
1467            startup_checkpoint_artifact_audit: parking_lot::Mutex::new(None),
1468            #[cfg(feature = "cluster")]
1469            last_recovery_epoch: parking_lot::Mutex::new(None),
1470            #[cfg(feature = "cluster")]
1471            source_gate: Arc::new(std::sync::atomic::AtomicBool::new(
1472                runtime_mode.is_cluster(),
1473            )),
1474            #[cfg(feature = "cluster")]
1475            topology_cut_hold: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1476            #[cfg(feature = "cluster")]
1477            installed_topology_runtime: parking_lot::Mutex::new(None),
1478            #[cfg(feature = "cluster")]
1479            cluster_authority_revoked: std::sync::atomic::AtomicBool::new(false),
1480            #[cfg(feature = "cluster")]
1481            cluster_authority_transition: Arc::new(parking_lot::Mutex::new(())),
1482            vnode_registry: parking_lot::Mutex::new((runtime_mode == RuntimeMode::Local).then(
1483                || {
1484                    Arc::new(laminar_core::state::VnodeRegistry::single_owner(
1485                        u32::from(laminar_core::state::DEFAULT_KEY_GROUP_COUNT),
1486                        laminar_core::state::LOCAL_NODE_ID,
1487                    ))
1488                },
1489            )),
1490            physical_optimizer_rules: extra_optimizer_rules.to_vec().into(),
1491            pipeline_target_partitions: target_partitions,
1492            #[cfg(feature = "cluster")]
1493            shuffle_sender: parking_lot::Mutex::new(None),
1494            #[cfg(feature = "cluster")]
1495            shuffle_receiver: Arc::new(parking_lot::Mutex::new(None)),
1496            #[cfg(feature = "cluster")]
1497            decision_store: parking_lot::Mutex::new(None),
1498            #[cfg(feature = "cluster")]
1499            assignment_snapshot_store: parking_lot::Mutex::new(None),
1500            #[cfg(feature = "cluster")]
1501            catalog_manifest_store: parking_lot::Mutex::new(None),
1502            #[cfg(feature = "cluster")]
1503            replayed_topology: parking_lot::Mutex::new(None),
1504            #[cfg(feature = "cluster")]
1505            cluster_checkpoint_object_store: None,
1506            #[cfg(feature = "cluster")]
1507            pending_vnode_transition: Arc::new(parking_lot::Mutex::new(None)),
1508            #[cfg(feature = "cluster")]
1509            installed_vnode_state: Arc::new(parking_lot::Mutex::new(None)),
1510            #[cfg(feature = "cluster")]
1511            assignment_adoption_lock: Arc::new(tokio::sync::Mutex::new(())),
1512            #[cfg(feature = "cluster")]
1513            assignment_authority_revision: std::sync::atomic::AtomicU64::new(0),
1514            #[cfg(feature = "cluster")]
1515            rotation_execution_fence: Arc::new(tokio::sync::RwLock::new(())),
1516            force_ckpt_tx: parking_lot::Mutex::new(None),
1517            subscription_registry: Arc::new(crate::subscription::SubscriptionRegistry::new()),
1518            stream_schemas: parking_lot::RwLock::new(HashMap::new()),
1519        })
1520    }
1521
1522    /// Install the AI subsystem. Called by the builder; `handle` must be the
1523    /// main multi-threaded runtime.
1524    pub(crate) fn set_ai_runtime(
1525        &mut self,
1526        runtime: Arc<crate::ai::AiRuntime>,
1527        handle: tokio::runtime::Handle,
1528    ) {
1529        // Non-fatal: inference still works if catalog view registration fails.
1530        if let Err(e) = crate::ai_catalog::register_ai_catalog(&self.ctx, &runtime) {
1531            tracing::warn!(error = %e, "failed to register laminar.* AI catalog views");
1532        }
1533        self.ai_runtime = Some(runtime);
1534        self.ai_handle = Some(handle);
1535    }
1536
1537    #[cfg(feature = "cluster")]
1538    pub(crate) fn set_shuffle_sender(&self, sender: Arc<laminar_core::shuffle::ShuffleSender>) {
1539        *self.shuffle_sender.lock() = Some(sender);
1540    }
1541
1542    #[cfg(feature = "cluster")]
1543    pub(crate) fn set_shuffle_receiver(
1544        &self,
1545        receiver: Arc<laminar_core::shuffle::ShuffleReceiver>,
1546    ) {
1547        *self.shuffle_receiver.lock() = Some(receiver);
1548    }
1549
1550    #[cfg(feature = "cluster")]
1551    pub(crate) fn invalidate_shuffle_assignment_fence(&self) {
1552        self.assignment_authority_revision
1553            .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
1554        if let Some(receiver) = self.shuffle_receiver.lock().as_ref() {
1555            receiver.invalidate_assignment_fence();
1556        }
1557        if let Some(sender) = self.shuffle_sender.lock().as_ref() {
1558            sender.invalidate_assignment_fence();
1559        }
1560    }
1561
1562    /// Close shuffle admission during temporary assignment-authority uncertainty without
1563    /// destroying the retained certificate's delivery sequence domain.
1564    #[cfg(feature = "cluster")]
1565    pub(crate) fn suspend_shuffle_assignment_fence(&self) {
1566        self.assignment_authority_revision
1567            .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
1568        if let Some(receiver) = self.shuffle_receiver.lock().as_ref() {
1569            receiver.suspend_assignment_fence();
1570        }
1571        if let Some(sender) = self.shuffle_sender.lock().as_ref() {
1572            sender.suspend_assignment_fence();
1573        }
1574    }
1575
1576    #[cfg(feature = "cluster")]
1577    fn withdraw_assignment_authority(
1578        &self,
1579        controller: &laminar_core::cluster::control::ClusterController,
1580    ) {
1581        self.set_source_gate(true);
1582        controller.publish_checkpoint_drain_transition(None);
1583        controller.publish_checkpoint_assignment_fence(None);
1584        self.suspend_shuffle_assignment_fence();
1585    }
1586
1587    #[cfg(feature = "cluster")]
1588    pub(crate) fn install_shuffle_assignment_certificate(
1589        &self,
1590        fence: &laminar_core::checkpoint::CheckpointAssignmentFence,
1591    ) -> Result<(), DbError> {
1592        let _transition = self.cluster_authority_transition.lock();
1593        if self
1594            .cluster_authority_revoked
1595            .load(std::sync::atomic::Ordering::Acquire)
1596        {
1597            return Err(DbError::Checkpoint(
1598                "shuffle assignment install has terminally revoked process authority".into(),
1599            ));
1600        }
1601        let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
1602            DbError::Checkpoint("shuffle assignment install has no cluster controller".into())
1603        })?;
1604        if !controller.process_lease_is_live() {
1605            self.invalidate_shuffle_assignment_fence();
1606            return Err(DbError::Checkpoint(
1607                "shuffle assignment install has no live process lease".into(),
1608            ));
1609        }
1610        let registry = self.vnode_registry.lock().clone().ok_or_else(|| {
1611            DbError::Checkpoint("shuffle assignment install has no vnode registry".into())
1612        })?;
1613        let assignment = registry.versioned_snapshot();
1614        let owners: Vec<u64> = assignment.owners().iter().map(|owner| owner.0).collect();
1615        // The caller installs under the assignment-adoption and execution write fences, then
1616        // publishes this certificate through the controller. Requiring it to be visible first
1617        // exposes authority before the transport has actually switched scopes.
1618        if !fence.is_canonical()
1619            || fence.assignment_version != assignment.version()
1620            || fence.vnode_count != registry.vnode_count()
1621            || !fence.matches_owner_map(&owners)
1622            || fence.participant_incarnation(controller.instance_id().0)
1623                != Some(controller.recovery_incarnation())
1624        {
1625            self.invalidate_shuffle_assignment_fence();
1626            return Err(DbError::Checkpoint(format!(
1627                "shuffle assignment certificate does not match local assignment {} and process {}",
1628                assignment.version(),
1629                controller.instance_id().0
1630            )));
1631        }
1632
1633        let receiver = self.shuffle_receiver.lock().clone();
1634        let sender = self.shuffle_sender.lock().clone();
1635        let expected_digest = fence.digest();
1636        if self.shuffle_assignment_authority_is_exact(fence) {
1637            if !controller.process_lease_is_live() {
1638                self.invalidate_shuffle_assignment_fence();
1639                return Err(DbError::Checkpoint(
1640                    "shuffle assignment install lost its process lease".into(),
1641                ));
1642            }
1643            return Ok(());
1644        }
1645        let endpoint_conflict = receiver.as_ref().is_some_and(|endpoint| {
1646            endpoint.local_id() != controller.instance_id().0
1647                || endpoint.incarnation() != controller.recovery_incarnation()
1648                || endpoint.assignment_version() > fence.assignment_version
1649                || (endpoint.assignment_version() == fence.assignment_version
1650                    && endpoint.active_assignment_digest() != Some(expected_digest))
1651        }) || sender.as_ref().is_some_and(|endpoint| {
1652            endpoint.local_id() != controller.instance_id().0
1653                || endpoint.incarnation() != controller.recovery_incarnation()
1654                || endpoint.assignment_version() > fence.assignment_version
1655                || (endpoint.assignment_version() == fence.assignment_version
1656                    && endpoint.active_assignment_digest() != Some(expected_digest))
1657        });
1658        if endpoint_conflict {
1659            self.invalidate_shuffle_assignment_fence();
1660            return Err(DbError::Checkpoint(
1661                "shuffle endpoint identity conflicts with the certified assignment".into(),
1662            ));
1663        }
1664
1665        // Background and operator shuffle paths hold the execution read fence. Activate inbound
1666        // first, then make outbound admission the local transition's linearization point.
1667        let result = (|| {
1668            if let Some(receiver) = receiver.as_ref() {
1669                receiver
1670                    .install_assignment_fence(fence, &owners)
1671                    .map_err(|error| {
1672                        DbError::Checkpoint(format!(
1673                            "failed to install receiver shuffle assignment certificate: {error}"
1674                        ))
1675                    })?;
1676                if receiver.assignment_version() != fence.assignment_version
1677                    || receiver.active_assignment_digest() != Some(expected_digest)
1678                {
1679                    return Err(DbError::Checkpoint(
1680                        "receiver did not activate the exact shuffle assignment certificate".into(),
1681                    ));
1682                }
1683            }
1684            if let Some(sender) = sender.as_ref() {
1685                sender
1686                    .install_assignment_fence(fence, &owners)
1687                    .map_err(|error| {
1688                        DbError::Checkpoint(format!(
1689                            "failed to install sender shuffle assignment certificate: {error}"
1690                        ))
1691                    })?;
1692                if sender.assignment_version() != fence.assignment_version
1693                    || sender.active_assignment_digest() != Some(expected_digest)
1694                {
1695                    return Err(DbError::Checkpoint(
1696                        "sender did not activate the exact shuffle assignment certificate".into(),
1697                    ));
1698                }
1699            }
1700            Ok(())
1701        })();
1702        if result.is_ok() && !controller.process_lease_is_live() {
1703            self.invalidate_shuffle_assignment_fence();
1704            return Err(DbError::Checkpoint(
1705                "shuffle assignment install lost its process lease".into(),
1706            ));
1707        }
1708        if result.is_err() {
1709            // Keep watcher caches coherent with a partial endpoint install. Direct endpoint
1710            // invalidation would close transport authority without advancing the shared revision.
1711            self.invalidate_shuffle_assignment_fence();
1712        }
1713        result
1714    }
1715
1716    #[cfg(feature = "cluster")]
1717    async fn assignment_recovery_admission(
1718        &self,
1719        controller: &laminar_core::cluster::control::ClusterController,
1720        deadline: tokio::time::Instant,
1721    ) -> Result<Option<laminar_core::cluster::control::RecoveryAdmissionSnapshot>, DbError> {
1722        let snapshot =
1723            tokio::time::timeout_at(deadline, controller.read_recovery_admission_snapshot())
1724                .await
1725                .map_err(|_| {
1726                    DbError::Checkpoint(
1727                        "recovery admission audit timed out before opening intake".into(),
1728                    )
1729                })?
1730                .map_err(|error| {
1731                    DbError::Checkpoint(format!(
1732                        "recovery admission authority is unavailable: {error}"
1733                    ))
1734                })?;
1735        let (committed_generation, committed_epoch) = match snapshot.committed_release() {
1736            None => (0, 0),
1737            Some(release) => match release.phase {
1738                laminar_core::cluster::control::RecoverPhase::ReleaseCommitted { epoch } => {
1739                    (release.round.id.generation, epoch)
1740                }
1741                _ => {
1742                    return Err(DbError::Checkpoint(
1743                        "latest recovery terminal is not a committed Release".into(),
1744                    ));
1745                }
1746            },
1747        };
1748        let local_generation = self.shuffle_recovery_generation()?;
1749        if !snapshot.fault_inventory().faults().is_empty() {
1750            controller.set_recovering(true);
1751            return Ok(None);
1752        }
1753        let transport_is_current =
1754            local_generation.is_none_or(|generation| generation == committed_generation);
1755        let restored_epoch = *self.last_recovery_epoch.lock();
1756        let state_is_current =
1757            committed_epoch == 0 || restored_epoch.is_some_and(|epoch| epoch >= committed_epoch);
1758        if transport_is_current && state_is_current {
1759            return Ok(Some(snapshot));
1760        }
1761
1762        controller.set_recovering(true);
1763        tokio::time::timeout_at(
1764            deadline,
1765            crate::coordinated_recovery::request_local_fault(
1766                controller,
1767                &self.pending_recovery_fault,
1768            ),
1769        )
1770        .await
1771        .map_err(|_| {
1772            DbError::Checkpoint(
1773                "recovery fault publication timed out for stale recovery admission".into(),
1774            )
1775        })?
1776        .map_err(|error| {
1777            DbError::Checkpoint(format!(
1778                "could not publish recovery fault for stale recovery admission: {error}"
1779            ))
1780        })?;
1781        tracing::debug!(
1782            ?local_generation,
1783            committed_generation,
1784            ?restored_epoch,
1785            committed_epoch,
1786            "assignment intake remains fenced for coordinated recovery"
1787        );
1788        Ok(None)
1789    }
1790
1791    /// Install shuffle authority, publish its controller certificate, and conditionally open
1792    /// source intake at one local assignment/execution boundary.
1793    ///
1794    /// `expected_revision` must have been captured before any durable reads used to derive
1795    /// `fence`. A concurrent closure advances the revision and this method leaves that closure in
1796    /// force. The controller certificate is deliberately published only after both endpoints are
1797    /// installed.
1798    #[cfg(feature = "cluster")]
1799    pub(crate) async fn activate_assignment_authority(
1800        &self,
1801        fence: &laminar_core::checkpoint::CheckpointAssignmentFence,
1802        drain_transition: Option<laminar_core::checkpoint::AssignmentDrainTransition>,
1803        expected_revision: u64,
1804        deadline: tokio::time::Instant,
1805    ) -> Result<AssignmentAuthorityActivation, DbError> {
1806        let _adoption = tokio::time::timeout_at(deadline, self.assignment_adoption_lock.lock())
1807            .await
1808            .map_err(|_| {
1809                DbError::Checkpoint("timed out serializing assignment authority activation".into())
1810            })?;
1811        let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
1812            DbError::Checkpoint("assignment activation has no cluster controller".into())
1813        })?;
1814        let installed_transition = controller.checkpoint_drain_transition();
1815        // INVARIANT: an absent drain in an audited snapshot cannot clear a locally active
1816        // transition. A watcher whose read predates leader publication retries from the new head.
1817        if !assignment_transition_allows_activation(
1818            fence,
1819            drain_transition.as_ref(),
1820            installed_transition.as_ref(),
1821        )? {
1822            return Ok(AssignmentAuthorityActivation {
1823                installed: false,
1824                intake_open: false,
1825                revision: self
1826                    .assignment_authority_revision
1827                    .load(std::sync::atomic::Ordering::Acquire),
1828            });
1829        }
1830        let _execution = tokio::time::timeout_at(
1831            deadline,
1832            Arc::clone(&self.rotation_execution_fence).write_owned(),
1833        )
1834        .await
1835        .map_err(|_| {
1836            DbError::Checkpoint("timed out draining the prior assignment execution scope".into())
1837        })?;
1838        let revision = self
1839            .assignment_authority_revision
1840            .load(std::sync::atomic::Ordering::Acquire);
1841        if revision != expected_revision || tokio::time::Instant::now() >= deadline {
1842            return Ok(AssignmentAuthorityActivation {
1843                installed: false,
1844                intake_open: false,
1845                revision,
1846            });
1847        }
1848
1849        // Installing a certificate briefly serializes source and shuffle authority. During a
1850        // drain, preserve an already-open predecessor execution scope: its source tasks are held
1851        // by the drain protocol, while its coordinator must still consume the FIFO boundary and
1852        // checkpoint barrier. Fresh and target-only processes enter with this gate closed and
1853        // remain closed until the terminal assignment is adopted.
1854        let intake_was_closed = self.cluster_intake_fenced();
1855        self.set_source_gate(true);
1856        if !controller.process_lease_is_live() {
1857            self.revoke_cluster_authority();
1858            return Ok(AssignmentAuthorityActivation {
1859                installed: false,
1860                intake_open: false,
1861                revision: self
1862                    .assignment_authority_revision
1863                    .load(std::sync::atomic::Ordering::Acquire),
1864            });
1865        }
1866        let registry = self.vnode_registry.lock().clone().ok_or_else(|| {
1867            DbError::Checkpoint("assignment activation has no vnode registry".into())
1868        })?;
1869        let assignment = registry.versioned_snapshot();
1870        let owners: Vec<u64> = assignment.owners().iter().map(|owner| owner.0).collect();
1871        let local_id = controller.instance_id().0;
1872        let local_incarnation = fence.participant_incarnation(local_id);
1873        if !fence.is_canonical()
1874            || fence.assignment_version != assignment.version()
1875            || fence.vnode_count != registry.vnode_count()
1876            || !fence.matches_owner_map(&owners)
1877            || local_incarnation
1878                .is_some_and(|incarnation| incarnation != controller.recovery_incarnation())
1879            || (local_incarnation.is_none() && owners.contains(&local_id))
1880        {
1881            return Err(DbError::Checkpoint(format!(
1882                "assignment certificate does not match local assignment {} and process {}",
1883                assignment.version(),
1884                local_id
1885            )));
1886        }
1887
1888        // A live process outside the owner roster is control-plane ready, but has no source,
1889        // compute, shuffle, checkpoint, or recovery authority. Retain the audited certificate so
1890        // its watcher can observe a later assignment that grants ownership.
1891        if local_incarnation.is_none() {
1892            let shuffle_active = self
1893                .shuffle_receiver
1894                .lock()
1895                .as_ref()
1896                .is_some_and(|receiver| receiver.assignment_version() != 0)
1897                || self
1898                    .shuffle_sender
1899                    .lock()
1900                    .as_ref()
1901                    .is_some_and(|sender| sender.assignment_version() != 0);
1902            let revision = if shuffle_active {
1903                self.invalidate_shuffle_assignment_fence();
1904                self.assignment_authority_revision
1905                    .load(std::sync::atomic::Ordering::Acquire)
1906            } else {
1907                expected_revision
1908            };
1909            controller.publish_checkpoint_drain_transition(drain_transition);
1910            controller.publish_checkpoint_assignment_fence(Some(fence.clone()));
1911            if !controller.process_lease_is_live()
1912                || self
1913                    .assignment_authority_revision
1914                    .load(std::sync::atomic::Ordering::Acquire)
1915                    != revision
1916            {
1917                if controller.process_lease_is_live() {
1918                    self.withdraw_assignment_authority(&controller);
1919                } else {
1920                    self.revoke_cluster_authority();
1921                }
1922                return Ok(AssignmentAuthorityActivation {
1923                    installed: false,
1924                    intake_open: false,
1925                    revision: self
1926                        .assignment_authority_revision
1927                        .load(std::sync::atomic::Ordering::Acquire),
1928                });
1929            }
1930            return Ok(AssignmentAuthorityActivation {
1931                installed: true,
1932                intake_open: false,
1933                revision,
1934            });
1935        }
1936        if let Err(error) = self.install_shuffle_assignment_fence(fence) {
1937            controller.publish_checkpoint_drain_transition(None);
1938            controller.publish_checkpoint_assignment_fence(None);
1939            return Err(error);
1940        }
1941        if self
1942            .assignment_authority_revision
1943            .load(std::sync::atomic::Ordering::Acquire)
1944            != expected_revision
1945        {
1946            self.withdraw_assignment_authority(&controller);
1947            return Ok(AssignmentAuthorityActivation {
1948                installed: false,
1949                intake_open: false,
1950                revision: self
1951                    .assignment_authority_revision
1952                    .load(std::sync::atomic::Ordering::Acquire),
1953            });
1954        }
1955
1956        let source_drain_active = drain_transition.is_some();
1957        let expected_drain_transition = drain_transition.clone();
1958        controller.publish_checkpoint_drain_transition(drain_transition);
1959        controller.publish_checkpoint_assignment_fence(Some(fence.clone()));
1960        if self
1961            .assignment_authority_revision
1962            .load(std::sync::atomic::Ordering::Acquire)
1963            != expected_revision
1964        {
1965            self.withdraw_assignment_authority(&controller);
1966            return Ok(AssignmentAuthorityActivation {
1967                installed: false,
1968                intake_open: false,
1969                revision: self
1970                    .assignment_authority_revision
1971                    .load(std::sync::atomic::Ordering::Acquire),
1972            });
1973        }
1974        let intake_open = false;
1975        let preserve_predecessor_execution = source_drain_active && !intake_was_closed;
1976        if !controller.is_recovering() && (!source_drain_active || preserve_predecessor_execution) {
1977            // The exact installed certificate remains useful during a held topology cut, but
1978            // ordinary recovery admission cannot authorize its target or clear that hold.
1979            if self
1980                .topology_cut_hold
1981                .load(std::sync::atomic::Ordering::Acquire)
1982            {
1983                return Ok(AssignmentAuthorityActivation {
1984                    installed: true,
1985                    intake_open: false,
1986                    revision: expected_revision,
1987                });
1988            }
1989            match self
1990                .refresh_released_topology_assignment(
1991                    &controller,
1992                    fence,
1993                    expected_revision,
1994                    deadline,
1995                )
1996                .await
1997            {
1998                Ok(Some(activation)) => return Ok(activation),
1999                Ok(None) => {}
2000                Err(error) => {
2001                    self.withdraw_assignment_authority(&controller);
2002                    return Err(error);
2003                }
2004            }
2005            // Recovery authority and active faults must come from one durable view. Reading them
2006            // independently can pair an old terminal with the empty fault set created by a newer
2007            // committed Release.
2008            let recovery_admission = match self
2009                .assignment_recovery_admission(&controller, deadline)
2010                .await
2011            {
2012                Ok(Some(snapshot)) => snapshot,
2013                Ok(None) => {
2014                    return Ok(AssignmentAuthorityActivation {
2015                        installed: true,
2016                        intake_open: false,
2017                        revision: expected_revision,
2018                    });
2019                }
2020                Err(error) => {
2021                    self.withdraw_assignment_authority(&controller);
2022                    return Err(error);
2023                }
2024            };
2025
2026            if controller.is_recovering() {
2027                return Ok(AssignmentAuthorityActivation {
2028                    installed: true,
2029                    intake_open: false,
2030                    revision: expected_revision,
2031                });
2032            }
2033            let shuffle_sender = { self.shuffle_sender.lock().clone() };
2034            if let Some(sender) = shuffle_sender {
2035                let mut retry_delay = Duration::from_millis(25);
2036                let mut last_error = None;
2037                loop {
2038                    if !controller.process_lease_is_live() {
2039                        self.revoke_cluster_authority();
2040                        return Ok(AssignmentAuthorityActivation {
2041                            installed: false,
2042                            intake_open: false,
2043                            revision: self
2044                                .assignment_authority_revision
2045                                .load(std::sync::atomic::Ordering::Acquire),
2046                        });
2047                    }
2048                    if self
2049                        .assignment_authority_revision
2050                        .load(std::sync::atomic::Ordering::Acquire)
2051                        != expected_revision
2052                        || controller
2053                            .checkpoint_assignment_fence(fence.assignment_version)
2054                            .as_ref()
2055                            != Some(fence)
2056                        || controller.checkpoint_drain_transition() != expected_drain_transition
2057                    {
2058                        return Ok(AssignmentAuthorityActivation {
2059                            installed: false,
2060                            intake_open: false,
2061                            revision: self
2062                                .assignment_authority_revision
2063                                .load(std::sync::atomic::Ordering::Acquire),
2064                        });
2065                    }
2066                    if tokio::time::Instant::now() >= deadline {
2067                        self.withdraw_assignment_authority(&controller);
2068                        return Err(DbError::Checkpoint(format!(
2069                            "assignment shuffle mesh did not become ready before the activation deadline{}",
2070                            last_error
2071                                .as_ref()
2072                                .map(|error| format!("; last error: {error}"))
2073                                .unwrap_or_default()
2074                        )));
2075                    }
2076                    match tokio::time::timeout_at(deadline, sender.establish_assignment_mesh(fence))
2077                        .await
2078                    {
2079                        Ok(Ok(())) => break,
2080                        Ok(Err(error)) => last_error = Some(error.to_string()),
2081                        Err(_) => {
2082                            self.withdraw_assignment_authority(&controller);
2083                            return Err(DbError::Checkpoint(format!(
2084                                "assignment shuffle mesh readiness timed out{}",
2085                                last_error
2086                                    .as_ref()
2087                                    .map(|error| format!("; last error: {error}"))
2088                                    .unwrap_or_default()
2089                            )));
2090                        }
2091                    }
2092                    let wake = tokio::time::Instant::now()
2093                        .checked_add(retry_delay)
2094                        .map_or(deadline, |wake| wake.min(deadline));
2095                    tokio::time::sleep_until(wake).await;
2096                    retry_delay = retry_delay
2097                        .checked_mul(2)
2098                        .unwrap_or(Duration::from_millis(250))
2099                        .min(Duration::from_millis(250));
2100                }
2101            }
2102
2103            let expected_leader = expected_drain_transition
2104                .as_ref()
2105                .filter(|_| preserve_predecessor_execution)
2106                .map(|transition| transition.leader.clone());
2107            let audited_leader = match controller
2108                .audit_assignment_leader_authority(fence, expected_leader.as_ref(), deadline)
2109                .await
2110            {
2111                Ok(proof) => proof,
2112                Err(error) => {
2113                    self.withdraw_assignment_authority(&controller);
2114                    return Err(DbError::Checkpoint(format!(
2115                        "assignment leader authority audit failed before opening intake: {error}"
2116                    )));
2117                }
2118            };
2119            let recovery_admission_current = match tokio::time::timeout_at(
2120                deadline,
2121                controller.recovery_admission_is_current(&recovery_admission, &audited_leader),
2122            )
2123            .await
2124            {
2125                Ok(Ok(current)) => current,
2126                Ok(Err(error)) => {
2127                    self.withdraw_assignment_authority(&controller);
2128                    return Err(DbError::Checkpoint(format!(
2129                        "recovery admission revalidation failed before opening intake: {error}"
2130                    )));
2131                }
2132                Err(_) => {
2133                    self.withdraw_assignment_authority(&controller);
2134                    return Err(DbError::Checkpoint(
2135                        "recovery admission revalidation timed out before opening intake".into(),
2136                    ));
2137                }
2138            };
2139            if !recovery_admission_current {
2140                self.withdraw_assignment_authority(&controller);
2141                return Ok(AssignmentAuthorityActivation {
2142                    installed: false,
2143                    intake_open: false,
2144                    revision: self
2145                        .assignment_authority_revision
2146                        .load(std::sync::atomic::Ordering::Acquire),
2147                });
2148            }
2149            return self
2150                .open_assignment_intake_after_audit(
2151                    &controller,
2152                    fence,
2153                    expected_drain_transition.as_ref(),
2154                    audited_leader.owner.node_id,
2155                    expected_revision,
2156                    deadline,
2157                )
2158                .await;
2159        }
2160
2161        Ok(AssignmentAuthorityActivation {
2162            installed: true,
2163            intake_open,
2164            revision: expected_revision,
2165        })
2166    }
2167
2168    /// Drop buffered shuffle slices and stashed barriers before a rewind: their senders
2169    /// rewind and replay them, so folding a buffered copy afterwards double-counts.
2170    #[cfg(feature = "cluster")]
2171    pub(crate) fn purge_shuffle_receiver_buffers(&self) {
2172        let Some(receiver) = self.shuffle_receiver.lock().clone() else {
2173            return;
2174        };
2175        let queued = receiver.drain_available().len();
2176        let staged = receiver.drain_all_staged().len();
2177        let barriers = receiver.drain_staged_barriers().len();
2178        if queued + staged + barriers > 0 {
2179            tracing::info!(
2180                queued,
2181                staged,
2182                barriers,
2183                "purged stale shuffle buffers before coordinated rewind"
2184            );
2185        }
2186    }
2187
2188    #[cfg(feature = "cluster")]
2189    pub(crate) fn set_decision_store(
2190        &self,
2191        store: Arc<laminar_core::cluster::control::CheckpointDecisionStore>,
2192    ) {
2193        *self.decision_store.lock() = Some(store);
2194    }
2195
2196    #[cfg(feature = "cluster")]
2197    pub(crate) fn set_assignment_snapshot_store(
2198        &self,
2199        store: Arc<laminar_core::cluster::control::AssignmentSnapshotStore>,
2200    ) {
2201        *self.assignment_snapshot_store.lock() = Some(store);
2202    }
2203
2204    /// Install the shared catalog manifest store.
2205    #[cfg(feature = "cluster")]
2206    pub(crate) fn set_catalog_manifest_store(
2207        &self,
2208        store: Arc<laminar_core::cluster::control::CatalogManifestStore>,
2209    ) {
2210        *self.catalog_manifest_store.lock() = Some(store);
2211    }
2212
2213    /// Install the shared checkpoint namespace before the database is published behind `Arc`.
2214    #[cfg(feature = "cluster")]
2215    pub(crate) fn set_cluster_checkpoint_object_store(
2216        &mut self,
2217        store: Arc<dyn object_store::ObjectStore>,
2218    ) -> Result<(), DbError> {
2219        if !self.is_cluster_runtime() || self.cluster_controller.lock().is_none() {
2220            return Err(DbError::Config(
2221                "cluster checkpoint object store requires a cluster controller".into(),
2222            ));
2223        }
2224        self.cluster_checkpoint_object_store = Some(store);
2225        Ok(())
2226    }
2227
2228    /// Clone the immutable cluster checkpoint namespace selected at construction.
2229    #[cfg(feature = "cluster")]
2230    pub(crate) fn cluster_checkpoint_object_store(
2231        &self,
2232    ) -> Option<Arc<dyn object_store::ObjectStore>> {
2233        self.cluster_checkpoint_object_store.clone()
2234    }
2235
2236    #[cfg(feature = "cluster")]
2237    fn validate_catalog_seal_authority(
2238        &self,
2239        proof: Option<&laminar_core::cluster::control::LeaderProof>,
2240    ) -> Result<(), DbError> {
2241        let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
2242            DbError::Pipeline(
2243                "[LDB-6043] cluster catalog bootstrap requires a cluster controller".into(),
2244            )
2245        })?;
2246        let Some(proof) = proof else {
2247            return Err(DbError::Pipeline(
2248                "[LDB-6043] cluster catalog bootstrap requires the active durable leader lease"
2249                    .into(),
2250            ));
2251        };
2252        if !controller.catalog_bootstrap_proof_is_live(proof) {
2253            return Err(DbError::Pipeline(
2254                "[LDB-6043] cluster catalog bootstrap lost its durable leader lease before sealing"
2255                    .into(),
2256            ));
2257        }
2258        Ok(())
2259    }
2260
2261    /// Replay catalog DDL from the shared manifest before cluster startup.
2262    ///
2263    /// # Errors
2264    /// Fails closed when the manifest cannot be loaded, a local definition conflicts with it, or
2265    /// any entry cannot be recreated. A node must never start with a partial cluster topology.
2266    #[cfg(feature = "cluster")]
2267    pub(crate) async fn restore_catalog_from_manifest(
2268        &self,
2269    ) -> Result<Option<laminar_core::cluster::control::CatalogManifest>, DbError> {
2270        self.connector_registry.freeze();
2271        let Some(store) = self.catalog_manifest_store.lock().clone() else {
2272            return Ok(None);
2273        };
2274        let Some((manifest, topology)) = futures::FutureExt::boxed(store.load_with_topology())
2275            .await
2276            .map_err(|error| {
2277                DbError::Pipeline(format!(
2278                    "[{}] catalog manifest load failed: {error}",
2279                    laminar_core::error_codes::RECOVERY_FAILED
2280                ))
2281            })?
2282        else {
2283            return Ok(None);
2284        };
2285
2286        let mut replay_guard = CatalogBootstrapGuard {
2287            db: self,
2288            created: Vec::new(),
2289            sealed: false,
2290        };
2291        topology::catalog_changes::validate_manifest_ddl(&manifest)?;
2292        self.reconcile_superseded_catalog_objects(&manifest, &store, &topology)
2293            .await?;
2294
2295        for entry in &manifest.entries {
2296            let local_ddl = self
2297                .connector_manager
2298                .lock()
2299                .get_ddl(&entry.canonical_name)
2300                .map(str::to_owned);
2301            if let Some(local_ddl) = local_ddl {
2302                let local_kind = self
2303                    .catalog_namespace
2304                    .lock()
2305                    .get(&entry.canonical_name)
2306                    .copied();
2307                if local_ddl != entry.ddl || local_kind != Some(entry.kind) {
2308                    return Err(DbError::Pipeline(format!(
2309                        "[{}] local catalog definition for '{}' conflicts with catalog manifest",
2310                        laminar_core::error_codes::RECOVERY_FAILED,
2311                        entry.canonical_name
2312                    )));
2313                }
2314                continue;
2315            }
2316            CATALOG_MANIFEST_REPLAY
2317                .scope(
2318                    (),
2319                    crate::ddl::schema_resolution::RESOLVED_SCHEMA.scope(
2320                        entry.schema_binding.clone(),
2321                        self.execute_single_already_gated(&entry.ddl),
2322                    ),
2323                )
2324                .await
2325                .map_err(|error| {
2326                    DbError::Pipeline(format!(
2327                        "[{}] catalog manifest replay failed for '{}': {error}",
2328                        laminar_core::error_codes::RECOVERY_FAILED,
2329                        entry.canonical_name
2330                    ))
2331                })?;
2332            let replayed_ddl = self
2333                .connector_manager
2334                .lock()
2335                .get_ddl(&entry.canonical_name)
2336                .map(str::to_owned);
2337            let replayed_kind = self
2338                .catalog_namespace
2339                .lock()
2340                .get(&entry.canonical_name)
2341                .copied();
2342            if replayed_ddl.as_deref() != Some(entry.ddl.as_str())
2343                || replayed_kind != Some(entry.kind)
2344            {
2345                return Err(DbError::Pipeline(format!(
2346                    "[{}] catalog manifest entry '{}' completed without installing its exact \
2347                     durable DDL identity",
2348                    laminar_core::error_codes::RECOVERY_FAILED,
2349                    entry.canonical_name
2350                )));
2351            }
2352            replay_guard.record(entry.canonical_name.clone(), entry.kind);
2353            tracing::info!(name = %entry.canonical_name, "replayed catalog DDL from manifest");
2354        }
2355
2356        let local_inventory = self.reconcile_catalog_manifest_inventory(&manifest)?;
2357        if local_inventory.as_slice() != manifest.entries.as_slice() {
2358            return Err(DbError::Pipeline(format!(
2359                "[{}] local catalog DDL inventory does not match the ordered catalog manifest \
2360                 (local entries: {}, manifest entries: {}); refusing cluster startup",
2361                laminar_core::error_codes::RECOVERY_FAILED,
2362                local_inventory.len(),
2363                manifest.entries.len()
2364            )));
2365        }
2366        replay_guard.sealed();
2367        *self.replayed_topology.lock() = topology::ReplayedTopology::from_catalog(&topology)?;
2368        Ok(Some(manifest))
2369    }
2370
2371    /// Adopt one authority-audited assignment, staging exact committed frames for live vnode
2372    /// acquisition. Startup recovery still restores the complete graph before launch.
2373    ///
2374    /// # Errors
2375    /// Returns a checkpoint error when authority, state, or the deadline changes before atomic
2376    /// publication. The assignment is not published in that case.
2377    #[cfg(feature = "cluster")]
2378    pub async fn adopt_assignment_snapshot(
2379        &self,
2380        snapshot: laminar_core::cluster::control::AssignmentSnapshot,
2381        deadline: tokio::time::Instant,
2382    ) -> Result<SnapshotAdoption, DbError> {
2383        let version = snapshot.version;
2384        tokio::time::timeout_at(deadline, async {
2385            let _adoption = self.assignment_adoption_lock.lock().await;
2386            self.adopt_assignment_snapshot_locked(
2387                snapshot,
2388                deadline,
2389                AssignmentAdoptionMode::LiveTransition,
2390            )
2391            .await
2392        })
2393        .await
2394        .unwrap_or_else(|_| {
2395            Err(DbError::Checkpoint(format!(
2396                "assignment {version} adoption exceeded its end-to-end deadline"
2397            )))
2398        })
2399    }
2400
2401    /// Install an authority-sequenced failure-recovery successor. A still-running graph uses the
2402    /// ordinary live transition. Only an exactly faulted, terminally observed and recovery-fenced
2403    /// graph selects cold publication without reusing predecessor heap memory.
2404    ///
2405    /// Waiting for another assignment operation and executing this adoption are independently
2406    /// bounded. A competing observer must not consume the durable re-audit and publication budget
2407    /// after this caller acquires serialization.
2408    #[cfg(feature = "cluster")]
2409    pub(crate) async fn adopt_recovery_assignment_snapshot(
2410        &self,
2411        snapshot: laminar_core::cluster::control::AssignmentSnapshot,
2412        operation_timeout: Duration,
2413    ) -> Result<SnapshotAdoption, DbError> {
2414        let version = snapshot.version;
2415        let _adoption = tokio::time::timeout(operation_timeout, self.assignment_adoption_lock.lock())
2416            .await
2417            .map_err(|_| {
2418                DbError::Checkpoint(format!(
2419                    "recovery assignment {version} adoption timed out waiting for assignment serialization"
2420                ))
2421            })?;
2422        let deadline = tokio::time::Instant::now() + operation_timeout;
2423        let mode = if DbState::load(&self.state) == DbState::Faulted {
2424            AssignmentAdoptionMode::ColdRecovery
2425        } else {
2426            AssignmentAdoptionMode::LiveTransition
2427        };
2428        tokio::time::timeout_at(
2429            deadline,
2430            self.adopt_assignment_snapshot_locked(snapshot, deadline, mode),
2431        )
2432        .await
2433        .unwrap_or_else(|_| {
2434            Err(DbError::Checkpoint(format!(
2435                "recovery assignment {version} adoption exceeded its end-to-end deadline"
2436            )))
2437        })
2438    }
2439
2440    #[cfg(feature = "cluster")]
2441    fn adopt_assignment_snapshot_locked(
2442        &self,
2443        snapshot: laminar_core::cluster::control::AssignmentSnapshot,
2444        deadline: tokio::time::Instant,
2445        mode: AssignmentAdoptionMode,
2446    ) -> futures::future::BoxFuture<'_, Result<SnapshotAdoption, DbError>> {
2447        Box::pin(async move {
2448            if snapshot.draining {
2449                return Err(DbError::Checkpoint(format!(
2450                    "assignment {} is a draining generation and cannot publish ownership",
2451                    snapshot.version
2452                )));
2453            }
2454            if !snapshot.has_canonical_participants() {
2455                return Err(DbError::Checkpoint(format!(
2456                    "assignment {} has no canonical process roster",
2457                    snapshot.version
2458                )));
2459            }
2460            let Some(registry) = self.vnode_registry.lock().clone() else {
2461                return Ok(SnapshotAdoption::default());
2462            };
2463            let vnode_count = registry.vnode_count();
2464            let new_assignment: Arc<[laminar_core::state::NodeId]> = snapshot
2465                .to_vnode_vec(vnode_count)
2466                .map_err(|error| DbError::Checkpoint(error.to_string()))?
2467                .into();
2468            if snapshot.version <= registry.assignment_version() {
2469                return Ok(SnapshotAdoption {
2470                    adopted: false,
2471                    version: snapshot.version,
2472                    recovery_required: false,
2473                });
2474            }
2475            // A successor cannot reinterpret transition bytes staged for the current assignment.
2476            // Reject this local condition before any durable-authority I/O; publication checks it
2477            // again under the execution fence to close the preparation race.
2478            if self.has_unapplied_vnode_transition() {
2479                return Err(DbError::ShuffleNotReady(format!(
2480                    "[LDB-6053] assignment {} cannot overtake an unapplied vnode transition for \
2481                 assignment {}; retry after graph execution completes it or an explicitly fenced \
2482                 replacement graph restores the committed cut",
2483                    snapshot.version,
2484                    registry.assignment_version()
2485                )));
2486            }
2487            let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
2488                DbError::Checkpoint(format!(
2489                    "[LDB-6053] assignment {} has no live local process identity",
2490                    snapshot.version
2491                ))
2492            })?;
2493            let assignment_snapshot_store = self
2494                .assignment_snapshot_store
2495                .lock()
2496                .clone()
2497                .ok_or_else(|| {
2498                    DbError::Checkpoint(format!(
2499                    "[LDB-6053] assignment {} cannot be adopted without durable assignment history",
2500                    snapshot.version
2501                ))
2502                })?;
2503            let durable_target = assignment_snapshot_store
2504                .load_version(snapshot.version)
2505                .await
2506                .map_err(|error| {
2507                    DbError::Checkpoint(format!(
2508                        "[LDB-6053] failed to load durable assignment {}: {error}",
2509                        snapshot.version
2510                    ))
2511                })?
2512                .ok_or_else(|| {
2513                    DbError::Checkpoint(format!(
2514                        "[LDB-6053] assignment {} is absent from durable history",
2515                        snapshot.version
2516                    ))
2517                })?;
2518            if durable_target != snapshot {
2519                return Err(DbError::Checkpoint(format!(
2520                    "[LDB-6053] assignment {} does not match its durable materialization",
2521                    snapshot.version
2522                )));
2523            }
2524            let audited_target = crate::rebalance::audit_assignment_snapshot_authority_outcome(
2525                &assignment_snapshot_store,
2526                Some(controller.as_ref()),
2527                &snapshot,
2528            )
2529            .await
2530            .map_err(|error| {
2531                DbError::Checkpoint(format!(
2532                    "assignment {} authority audit failed: {error}",
2533                    snapshot.version
2534                ))
2535            })?;
2536            let audited_recovery = audited_target.is_recovery();
2537            let audited_predecessor = audited_target.predecessor().cloned();
2538            let committed_handoff_checkpoint = audited_target.handoff_checkpoint().cloned();
2539            let recovery_origin = audited_target.recovery_origin().cloned();
2540            let recovery_proof = audited_target.active_recovery_leader_proof().cloned();
2541            let recovery_was_consumed = audited_target.recovery_checkpoint_was_consumed();
2542            let audited_drain = audited_target.into_terminal();
2543            let terminal_drain_authority = !audited_recovery && audited_drain.is_some();
2544            let aborted_drain_authority = !audited_recovery
2545                && audited_drain
2546                    .as_ref()
2547                    .is_some_and(crate::rebalance::AuditedDrainOutcome::is_abort);
2548            let committed_drain_authority = !audited_recovery
2549                && committed_handoff_checkpoint.is_some()
2550                && audited_drain
2551                    .clone()
2552                    .and_then(crate::rebalance::AuditedDrainOutcome::into_committed_transition)
2553                    .is_some();
2554            // A drain terminal can land while checkpoint-abort cleanup is faulting the exact
2555            // predecessor graph. That graph has already discarded its installed vnode binding,
2556            // so an ordinary live transition must not reinterpret its retained heap. This also
2557            // covers an audited Abort: its target-version rollback retains the predecessor map but
2558            // still needs topology publication before a pristine graph can restore that older cut.
2559            // Reuse the cold-publication path only when the immutable drain audit and exact faulted
2560            // lifecycle below both hold; neither outcome reuses predecessor heap memory.
2561            // The durable audit above can outlive the compute generation on a remote store. Select
2562            // the recovery mode again after that I/O so a graph that faulted meanwhile does not
2563            // pay for a known-invalid live transition and a second complete authority audit.
2564            let (mut mode, faulted_terminal_drain_cold) = mode
2565                .after_authority_audit(self, audited_recovery, terminal_drain_authority, deadline)
2566                .await?;
2567            let target_fence = snapshot
2568                .assignment_fence()
2569                .map_err(|error| DbError::Checkpoint(error.to_string()))?;
2570            let recovery_requires_cold = audited_recovery
2571                && (recovery_was_consumed
2572                    || recovery_origin.as_ref().is_some_and(|origin| {
2573                        audited_predecessor.as_ref().is_some_and(|predecessor| {
2574                            origin.assignment_version < predecessor.assignment_version
2575                        })
2576                    }));
2577            if mode == AssignmentAdoptionMode::ColdRecovery {
2578                if !audited_recovery && !faulted_terminal_drain_cold {
2579                    return Err(DbError::Checkpoint(format!(
2580                        "[LDB-6053] assignment {} is neither an authority-sequenced recovery successor nor an audited terminal drain",
2581                        snapshot.version
2582                    )));
2583                }
2584                if audited_recovery {
2585                    let expected_handoff = committed_handoff_checkpoint.as_ref().ok_or_else(|| {
2586                        DbError::Checkpoint(format!(
2587                            "[LDB-6053] recovery assignment {} has no pinned predecessor checkpoint",
2588                            snapshot.version
2589                        ))
2590                    })?;
2591                    let authority = controller.checkpoint_authority().map_err(|error| {
2592                        DbError::Checkpoint(format!(
2593                            "[LDB-6053] recovery assignment {} has no checkpoint authority: {error}",
2594                            snapshot.version
2595                        ))
2596                    })?;
2597                    if recovery_proof.is_some() {
2598                        let pinned = tokio::time::timeout_at(
2599                            deadline,
2600                            authority.assignment_handoff_checkpoint(&target_fence),
2601                        )
2602                        .await
2603                        .map_err(|_| {
2604                            DbError::Checkpoint(format!(
2605                                "[LDB-6053] recovery assignment {} checkpoint-pin audit timed out",
2606                                snapshot.version
2607                            ))
2608                        })?
2609                        .map_err(|error| {
2610                            DbError::Checkpoint(format!(
2611                                "[LDB-6053] recovery assignment {} checkpoint-pin audit failed: {error}",
2612                                snapshot.version
2613                            ))
2614                        })?;
2615                        if pinned.as_ref() != Some(expected_handoff) {
2616                            return Err(DbError::Checkpoint(format!(
2617                                "[LDB-6053] recovery assignment {} does not retain its exact authorized checkpoint pin",
2618                                snapshot.version
2619                            )));
2620                        }
2621                    } else if recovery_was_consumed {
2622                        let head = tokio::time::timeout_at(
2623                            deadline,
2624                            authority.highest_cluster_committed_outcome(),
2625                        )
2626                        .await
2627                        .map_err(|_| {
2628                            DbError::Checkpoint(format!(
2629                                "[LDB-6053] recovery assignment {} consumed-checkpoint audit timed out",
2630                                snapshot.version
2631                            ))
2632                        })?
2633                        .map_err(|error| {
2634                            DbError::Checkpoint(format!(
2635                                "[LDB-6053] recovery assignment {} consumed-checkpoint audit failed: {error}",
2636                                snapshot.version
2637                            ))
2638                        })?
2639                        .ok_or_else(|| {
2640                            DbError::Checkpoint(format!(
2641                                "[LDB-6053] recovery assignment {} has no committed recovery head",
2642                                snapshot.version
2643                            ))
2644                        })?;
2645                        if head.assignment_fence.as_ref() != Some(&target_fence)
2646                            || head.committed_checkpoint.as_ref() != Some(expected_handoff)
2647                        {
2648                            return Err(DbError::Checkpoint(format!(
2649                                "[LDB-6053] recovery assignment {} no longer owns its consumed committed head",
2650                                snapshot.version
2651                            )));
2652                        }
2653                    } else {
2654                        return Err(DbError::Checkpoint(format!(
2655                            "[LDB-6053] recovery assignment {} has no audited checkpoint lifecycle",
2656                            snapshot.version
2657                        )));
2658                    }
2659                }
2660            }
2661            let self_id = laminar_core::state::NodeId(controller.instance_id().0);
2662            let observed_assignment = registry.versioned_snapshot();
2663            let observed_version = observed_assignment.version();
2664            if observed_version == 0 {
2665                let pristine_boot = observed_assignment
2666                    .owners()
2667                    .iter()
2668                    .all(|owner| *owner == laminar_core::state::NodeId::UNASSIGNED)
2669                    && self.pending_vnode_transition.lock().is_none()
2670                    && self.installed_vnode_state.lock().is_none();
2671                if !pristine_boot {
2672                    return Err(DbError::Checkpoint(
2673                    "[LDB-6053] assignment-zero bootstrap contains retained vnode lifecycle state"
2674                        .into(),
2675                ));
2676                }
2677                let durable_head = assignment_snapshot_store
2678                    .load()
2679                    .await
2680                    .map_err(|error| {
2681                        DbError::Checkpoint(format!(
2682                            "[LDB-6053] failed to load the durable assignment head: {error}"
2683                        ))
2684                    })?
2685                    .ok_or_else(|| {
2686                        DbError::Checkpoint(
2687                            "[LDB-6053] assignment-zero bootstrap has no durable assignment head"
2688                                .into(),
2689                        )
2690                    })?;
2691                let committed = crate::rebalance::startup_committed_assignment(
2692                    assignment_snapshot_store.as_ref(),
2693                    Some(controller.as_ref()),
2694                    durable_head,
2695                )
2696                .await
2697                .map_err(|error| DbError::Checkpoint(format!("[LDB-6053] {error}")))?;
2698                if committed != snapshot {
2699                    return Err(DbError::Checkpoint(format!(
2700                    "[LDB-6053] assignment-zero bootstrap target {} is not the current committed assignment {}",
2701                    snapshot.version, committed.version
2702                )));
2703                }
2704            } else if (mode == AssignmentAdoptionMode::LiveTransition
2705                || faulted_terminal_drain_cold)
2706                && snapshot.version != observed_version.saturating_add(1)
2707            {
2708                return Err(DbError::Checkpoint(format!(
2709                "[LDB-6053] assignment {} is not the exact successor of local assignment {observed_version}; coordinated recovery must install missing generations",
2710                snapshot.version
2711            )));
2712            }
2713            let predecessor_snapshot = if observed_version == 0
2714                && mode == AssignmentAdoptionMode::LiveTransition
2715            {
2716                None
2717            } else {
2718                // The target authority audit above certified the immediate target -> predecessor
2719                // edge. Retention intentionally keeps that predecessor, not its complete ancestry;
2720                // recursively auditing older generations would reject a valid successor after GC.
2721                // A cold recovery may skip locally uninstalled generations, so bind this proof to the
2722                // target's durable immediate predecessor rather than to the stale local registry.
2723                let predecessor_version = if mode == AssignmentAdoptionMode::ColdRecovery {
2724                    snapshot.version.checked_sub(1).ok_or_else(|| {
2725                        DbError::Checkpoint(
2726                            "[LDB-6053] recovery target has no predecessor generation".into(),
2727                        )
2728                    })?
2729                } else {
2730                    observed_version
2731                };
2732                let predecessor = assignment_snapshot_store
2733                .load_version(predecessor_version)
2734                .await
2735                .map_err(|error| {
2736                    DbError::Checkpoint(format!(
2737                        "[LDB-6053] failed to load predecessor assignment {predecessor_version}: {error}"
2738                    ))
2739                })?
2740                .ok_or_else(|| {
2741                    DbError::Checkpoint(format!(
2742                        "[LDB-6053] predecessor assignment {predecessor_version} is unavailable"
2743                    ))
2744                })?;
2745                let predecessor_owners = predecessor
2746                    .to_vnode_vec(vnode_count)
2747                    .map_err(|error| DbError::Checkpoint(error.to_string()))?;
2748                let predecessor_fence = predecessor
2749                    .assignment_fence()
2750                    .map_err(|error| DbError::Checkpoint(error.to_string()))?;
2751                if audited_predecessor.as_ref() != Some(&predecessor_fence) {
2752                    return Err(DbError::Checkpoint(format!(
2753                    "[LDB-6053] predecessor assignment {predecessor_version} does not match the audited target edge"
2754                )));
2755                }
2756                if (mode == AssignmentAdoptionMode::LiveTransition || faulted_terminal_drain_cold)
2757                    && predecessor_owners.as_slice() != observed_assignment.owners()
2758                {
2759                    return Err(DbError::Checkpoint(format!(
2760                    "[LDB-6053] local owner map at assignment {observed_version} does not match durable history"
2761                )));
2762                }
2763                Some(predecessor)
2764            };
2765            let predecessor_fence = predecessor_snapshot
2766                .as_ref()
2767                .map(laminar_core::cluster::control::AssignmentSnapshot::assignment_fence)
2768                .transpose()
2769                .map_err(|error| DbError::Checkpoint(error.to_string()))?;
2770            let current_incarnation = controller.recovery_incarnation();
2771            let local_participant = laminar_core::checkpoint::CheckpointParticipant {
2772                node_id: self_id.0,
2773                boot_incarnation: current_incarnation,
2774            };
2775            crate::vnode_transition_staging::PendingVnodeTransition::validate_target_process(
2776                &target_fence,
2777                &new_assignment,
2778                local_participant,
2779            )?;
2780            let predecessor_process_is_current =
2781                predecessor_fence.as_ref().is_some_and(|predecessor| {
2782                    predecessor.participant_incarnation(self_id.0) == Some(current_incarnation)
2783                });
2784            // A replacement process may retain the stable node id without inheriting any of the old
2785            // process's memory. Use this effective predecessor roster everywhere below so acquisition,
2786            // revoke, and final-owner authority cannot disagree with transition construction.
2787            let effective_predecessor_owned = if predecessor_process_is_current {
2788                owned_vnode_indices(observed_assignment.owners(), self_id)?
2789            } else {
2790                Vec::new()
2791            };
2792            let preflight_new_owned = owned_vnode_indices(&new_assignment, self_id)?;
2793            if mode == AssignmentAdoptionMode::ColdRecovery && preflight_new_owned.is_empty() {
2794                return Err(DbError::Checkpoint(format!(
2795                "[LDB-6053] recovery assignment {} cold publication requires target-bound local vnode ownership",
2796                snapshot.version
2797            )));
2798            }
2799            let final_owner_exit = if mode == AssignmentAdoptionMode::LiveTransition
2800                && !effective_predecessor_owned.is_empty()
2801                && preflight_new_owned.is_empty()
2802            {
2803                Some(audited_drain
2804                .clone()
2805                .and_then(crate::rebalance::AuditedDrainOutcome::into_committed_transition)
2806                .ok_or_else(|| {
2807                    DbError::Checkpoint(format!(
2808                        "[LDB-6053] assignment {} cannot revoke this process's final vnode without an audited committed drain transition",
2809                        snapshot.version
2810                    ))
2811                })?)
2812            } else {
2813                None
2814            };
2815            // Cold recovery must arrive through an already-fenced fault path. Remember this before
2816            // ordinary adoption closes the gate itself, so that mutation cannot manufacture cold
2817            // eligibility.
2818            let cold_source_gate_was_closed =
2819                self.source_gate.load(std::sync::atomic::Ordering::Acquire);
2820            // The target is now durable-authority-audited and still newer. Close every predecessor
2821            // admission path before reading its process roster, source handoff, or vnode state. Scope
2822            // cancellation releases compute cycles blocked in shuffle so the final write fence can
2823            // drain; any later error deliberately leaves this authority closed for a full retry.
2824            self.set_source_gate(true);
2825            controller.publish_checkpoint_assignment_fence(None);
2826            self.invalidate_shuffle_assignment_fence();
2827            let predecessor_drain = Arc::clone(&self.rotation_execution_fence)
2828                .write_owned()
2829                .await;
2830            drop(predecessor_drain);
2831
2832            // Hold the coord mutex so registry + fence updates land between epochs.
2833            let guard = self.coordinator.lock().await;
2834            let prepared_pipeline_identity = guard
2835                .as_ref()
2836                .map(crate::checkpoint_coordinator::CheckpointCoordinator::bound_pipeline_identity)
2837                .transpose()?;
2838            // Re-check under the lock: a concurrent adopt may have advanced the version,
2839            // which we must not regress.
2840            if snapshot.version <= registry.assignment_version() {
2841                return Ok(SnapshotAdoption {
2842                    adopted: false,
2843                    version: snapshot.version,
2844                    recovery_required: false,
2845                });
2846            }
2847
2848            let prepared_assignment = registry.versioned_snapshot();
2849            let prepared_from_version = prepared_assignment.version();
2850            if prepared_from_version != observed_version {
2851                return Err(DbError::Checkpoint(format!(
2852                "[LDB-6053] assignment base advanced from {observed_version} to {prepared_from_version} while preparing target {}",
2853                snapshot.version
2854            )));
2855            }
2856            let prepared_db_state = DbState::load(&self.state);
2857            let prepared_runtime_shutdown = self.runtime_shutdown.read().clone();
2858            let prepared_installed_state = self.installed_vnode_state.lock().clone();
2859            let prepared_recovery_fault = self
2860                .pending_recovery_fault
2861                .load(std::sync::atomic::Ordering::Acquire);
2862            let prepared_startup_attempt = self.startup_attempt.lock().clone();
2863            // A fully stopped recovery generation has no resident vnode heap to transition and its
2864            // cancelled runtime cannot consume staged frames. Topology-only publication admits an
2865            // ownerless Commit acquisition under its active fault, or an exact stopped predecessor
2866            // owner publishing Commit/Abort under the durable Prepare plus its boot-bound stopped
2867            // report, or an authority-audited recovery successor pinned to that same stopped
2868            // predecessor. A healthy stopped participant need not own the triggering fault.
2869            let stopped_recovery_topology_common = mode == AssignmentAdoptionMode::LiveTransition
2870                && prepared_db_state == DbState::Created
2871                && prepared_runtime_shutdown.is_cancelled()
2872                && cold_source_gate_was_closed
2873                && self.source_gate.load(std::sync::atomic::Ordering::Acquire)
2874                && controller.process_lease_is_live()
2875                && !self
2876                    .cluster_authority_revoked
2877                    .load(std::sync::atomic::Ordering::Acquire)
2878                && self
2879                    .coordinated_recovery_fenced
2880                    .load(std::sync::atomic::Ordering::Acquire)
2881                && !self
2882                    .coordinated_lifecycle_active
2883                    .load(std::sync::atomic::Ordering::Acquire)
2884                && !self.assignment_restore_shutdown.is_cancelled()
2885                && !self
2886                    .catalog_cleanup_fenced
2887                    .load(std::sync::atomic::Ordering::Acquire)
2888                && !self.is_closed()
2889                && prepared_startup_attempt
2890                    .as_ref()
2891                    .is_some_and(|attempt| attempt.is_complete())
2892                && guard.is_some()
2893                && prepared_pipeline_identity.is_some()
2894                && self.pending_vnode_transition.lock().is_none()
2895                && prepared_installed_state.is_none()
2896                && self.recover_target_epoch.lock().is_none()
2897                && snapshot.version == observed_version.saturating_add(1);
2898            let stopped_committed_topology_shape = committed_drain_authority
2899                && observed_assignment
2900                    .owners()
2901                    .iter()
2902                    .all(|owner| *owner != self_id)
2903                && effective_predecessor_owned.is_empty()
2904                && predecessor_fence.as_ref().is_some_and(|predecessor| {
2905                    predecessor.participant_incarnation(self_id.0).is_none()
2906                })
2907                && !preflight_new_owned.is_empty()
2908                && target_fence.participant_incarnation(self_id.0) == Some(current_incarnation);
2909            let stopped_committed_owner_topology_shape = committed_drain_authority
2910                && !effective_predecessor_owned.is_empty()
2911                && predecessor_process_is_current
2912                && audited_drain.as_ref().is_some_and(|terminal| {
2913                    terminal.is_commit()
2914                        && predecessor_fence.as_ref().is_some_and(|predecessor| {
2915                            terminal.transition().predecessor == *predecessor
2916                                && terminal.transition().target == target_fence
2917                        })
2918                })
2919                && if preflight_new_owned.is_empty() {
2920                    target_fence.participant_incarnation(self_id.0).is_none()
2921                } else {
2922                    target_fence.participant_incarnation(self_id.0) == Some(current_incarnation)
2923                };
2924            let stopped_aborted_owner_topology_shape = aborted_drain_authority
2925                && !effective_predecessor_owned.is_empty()
2926                && predecessor_process_is_current
2927                && predecessor_snapshot.as_ref().is_some_and(|predecessor| {
2928                    predecessor.vnodes == snapshot.vnodes
2929                        && predecessor.participants == snapshot.participants
2930                })
2931                && new_assignment.as_ref() == observed_assignment.owners()
2932                && preflight_new_owned == effective_predecessor_owned
2933                && predecessor_fence.as_ref().is_some_and(|predecessor| {
2934                    predecessor.participants == target_fence.participants
2935                        && predecessor.assignment_digest == target_fence.assignment_digest
2936                        && predecessor.vnode_count == target_fence.vnode_count
2937                        && predecessor.partitioning_abi_version
2938                            == target_fence.partitioning_abi_version
2939                        && predecessor.assignment_version.checked_add(1)
2940                            == Some(target_fence.assignment_version)
2941                })
2942                && target_fence.participant_incarnation(self_id.0) == Some(current_incarnation);
2943            // A non-owner fault reporter is part of the exact recovery stopped roster but is absent
2944            // from both the rollback predecessor and target assignment rosters. It has no vnode heap
2945            // to retain or restore; after its exact stopped report, it must still advance the local
2946            // topology generation so it can observe the successor owner-complete recovery round.
2947            let stopped_aborted_ownerless_topology_shape = aborted_drain_authority
2948                && effective_predecessor_owned.is_empty()
2949                && !predecessor_process_is_current
2950                && observed_assignment
2951                    .owners()
2952                    .iter()
2953                    .all(|owner| *owner != self_id)
2954                && preflight_new_owned.is_empty()
2955                && predecessor_snapshot.as_ref().is_some_and(|predecessor| {
2956                    predecessor.vnodes == snapshot.vnodes
2957                        && predecessor.participants == snapshot.participants
2958                })
2959                && new_assignment.as_ref() == observed_assignment.owners()
2960                && predecessor_fence.as_ref().is_some_and(|predecessor| {
2961                    predecessor.participant_incarnation(self_id.0).is_none()
2962                        && predecessor.participants == target_fence.participants
2963                        && predecessor.assignment_digest == target_fence.assignment_digest
2964                        && predecessor.vnode_count == target_fence.vnode_count
2965                        && predecessor.partitioning_abi_version
2966                            == target_fence.partitioning_abi_version
2967                        && predecessor.assignment_version.checked_add(1)
2968                            == Some(target_fence.assignment_version)
2969                })
2970                && target_fence.participant_incarnation(self_id.0).is_none();
2971            let stopped_aborted_topology_shape =
2972                stopped_aborted_owner_topology_shape || stopped_aborted_ownerless_topology_shape;
2973            let stopped_recovery_successor_topology_shape = recovery_proof.is_some()
2974                && committed_handoff_checkpoint.is_some()
2975                && predecessor_fence.is_some();
2976            let prepared_recovery_fault_is_active = if stopped_recovery_topology_common
2977                && stopped_committed_topology_shape
2978                && prepared_recovery_fault != 0
2979            {
2980                let request = controller
2981                    .recovery_fault_request(prepared_recovery_fault)
2982                    .map_err(|error| {
2983                        DbError::Checkpoint(format!(
2984                            "[LDB-6053] assignment {} stopped-recovery fault identity is invalid: {error}",
2985                            snapshot.version
2986                        ))
2987                    })?;
2988                let outcome = tokio::time::timeout_at(deadline, controller.report_fault(request))
2989                    .await
2990                    .map_err(|_| {
2991                        DbError::Checkpoint(format!(
2992                            "[LDB-6053] assignment {} stopped-recovery fault audit timed out",
2993                            snapshot.version
2994                        ))
2995                    })?
2996                    .map_err(|error| {
2997                        DbError::Checkpoint(format!(
2998                            "[LDB-6053] assignment {} stopped-recovery fault audit failed: {error}",
2999                            snapshot.version
3000                        ))
3001                    })?;
3002                outcome == laminar_core::cluster::control::RecoveryFaultReportOutcome::Active
3003            } else {
3004                false
3005            };
3006            let stopped_abort_round =
3007                if stopped_recovery_topology_common && stopped_aborted_topology_shape {
3008                    audited_stopped_terminal_round(
3009                        controller.as_ref(),
3010                        predecessor_fence
3011                            .as_ref()
3012                            .expect("abort shape has predecessor"),
3013                        deadline,
3014                    )
3015                    .await?
3016                } else {
3017                    None
3018                };
3019            let stopped_commit_owner_round =
3020                if stopped_recovery_topology_common && stopped_committed_owner_topology_shape {
3021                    audited_stopped_terminal_round(
3022                        controller.as_ref(),
3023                        predecessor_fence
3024                            .as_ref()
3025                            .expect("committed owner shape has predecessor"),
3026                        deadline,
3027                    )
3028                    .await?
3029                } else {
3030                    None
3031                };
3032            let stopped_recovery_successor_round =
3033                if stopped_recovery_topology_common && stopped_recovery_successor_topology_shape {
3034                    audited_stopped_recovery_successor_round(
3035                        controller.as_ref(),
3036                        predecessor_fence
3037                            .as_ref()
3038                            .expect("recovery successor shape has predecessor"),
3039                        recovery_proof.as_ref().expect("audited active proof"),
3040                        deadline,
3041                    )
3042                    .await?
3043                } else {
3044                    None
3045                };
3046            let stopped_commit_owner_pin_is_active =
3047                if stopped_recovery_topology_common && stopped_committed_owner_topology_shape {
3048                    assignment_authority::handoff_pin_is_active(
3049                        &controller,
3050                        &target_fence,
3051                        committed_handoff_checkpoint
3052                            .as_ref()
3053                            .expect("committed drain authority has a handoff checkpoint"),
3054                        &format!(
3055                            "assignment {} stopped-Commit handoff-pin audit",
3056                            snapshot.version
3057                        ),
3058                        deadline,
3059                    )
3060                    .await?
3061                } else {
3062                    false
3063                };
3064            let stopped_recovery_topology = stopped_recovery_topology_common
3065                && ((stopped_committed_topology_shape && prepared_recovery_fault_is_active)
3066                    || (stopped_aborted_topology_shape && stopped_abort_round.is_some())
3067                    || (stopped_committed_owner_topology_shape
3068                        && stopped_commit_owner_round.is_some()
3069                        && stopped_commit_owner_pin_is_active)
3070                    || (stopped_recovery_successor_topology_shape
3071                        && stopped_recovery_successor_round.is_some()));
3072            if stopped_recovery_topology {
3073                mode = AssignmentAdoptionMode::StoppedRecoveryTopology;
3074            }
3075            // A propagated recovery cut cannot seed a resident live-state transition: its archive
3076            // is bound to an older assignment than the target's immediate predecessor. Two local
3077            // shapes carry no predecessor heap across that edge and are therefore safe without
3078            // waiting for the faulted cold path:
3079            //
3080            // * an authority-validated Created process with no coordinator or installed heap
3081            //   defers the complete graph restore to startup; and
3082            // * a zero-owner process publishes topology only, with neither retained nor acquired
3083            //   vnode state and no installed binding.
3084            //
3085            // Keep every stateful live path fail-closed. In particular, merely being a replacement
3086            // process is not enough when the target assigns it vnodes.
3087            let pristine_startup_bootstrap = prepared_db_state == DbState::Created
3088                && prepared_installed_state.is_none()
3089                && guard.is_none()
3090                && effective_predecessor_owned.is_empty()
3091                && cold_source_gate_was_closed
3092                && controller.is_recovering()
3093                && controller.process_lease_is_live()
3094                && (!audited_recovery || prepared_recovery_fault != 0)
3095                && self.pending_vnode_transition.lock().is_none()
3096                && self.recover_target_epoch.lock().is_none()
3097                && !self
3098                    .coordinated_recovery_fenced
3099                    .load(std::sync::atomic::Ordering::Acquire)
3100                && !self
3101                    .catalog_cleanup_fenced
3102                    .load(std::sync::atomic::Ordering::Acquire)
3103                && !self.is_closed();
3104            let metadata_only_zero_owner_transition = effective_predecessor_owned.is_empty()
3105                && preflight_new_owned.is_empty()
3106                && prepared_installed_state.is_none();
3107            if mode == AssignmentAdoptionMode::LiveTransition
3108                && recovery_requires_cold
3109                && !pristine_startup_bootstrap
3110                && !metadata_only_zero_owner_transition
3111            {
3112                return Err(DbError::Checkpoint(format!(
3113                    "[LDB-6053] recovery assignment {} must wait for a faulted cold bootstrap because its committed cut is not the live predecessor edge",
3114                    snapshot.version
3115                )));
3116            }
3117            if mode == AssignmentAdoptionMode::ColdRecovery
3118                && (prepared_db_state != DbState::Faulted
3119                    || !cold_source_gate_was_closed
3120                    || !self.source_gate.load(std::sync::atomic::Ordering::Acquire)
3121                    || !controller.is_recovering()
3122                    || !self
3123                        .coordinated_recovery_fenced
3124                        .load(std::sync::atomic::Ordering::Acquire)
3125                    || prepared_recovery_fault == 0
3126                    || !controller.process_lease_is_live()
3127                    || self
3128                        .catalog_cleanup_fenced
3129                        .load(std::sync::atomic::Ordering::Acquire)
3130                    || self.is_closed()
3131                    || guard.is_none()
3132                    || self.pending_vnode_transition.lock().is_some()
3133                    || prepared_installed_state.is_some())
3134            {
3135                return Err(DbError::Checkpoint(format!(
3136                "[LDB-6053] recovery assignment {} cold publication requires the exact faulted graph, live process lease, closed source gate, recovery lifecycle fence, pending fault, coordinator, and empty pending/installed vnode state",
3137                snapshot.version
3138            )));
3139            }
3140            let predecessor_binding_missing = local_state_lacks_exact_predecessor_binding(
3141                observed_version,
3142                predecessor_fence.as_ref(),
3143                prepared_pipeline_identity.as_ref(),
3144                (prepared_db_state == DbState::Running)
3145                    .then_some(prepared_installed_state.as_ref())
3146                    .flatten(),
3147            );
3148            let old_owned = if matches!(
3149                mode,
3150                AssignmentAdoptionMode::ColdRecovery
3151                    | AssignmentAdoptionMode::StoppedRecoveryTopology
3152            ) {
3153                Vec::new()
3154            } else {
3155                effective_predecessor_owned
3156            };
3157            if mode == AssignmentAdoptionMode::LiveTransition
3158                && predecessor_binding_missing
3159                && !old_owned.is_empty()
3160            {
3161                return Err(DbError::Checkpoint(format!(
3162                "[LDB-6053] assignment {} cannot reuse retained vnode memory without its exact predecessor binding; coordinated recovery is required",
3163                snapshot.version
3164            )));
3165            }
3166            if mode == AssignmentAdoptionMode::LiveTransition
3167                && old_owned.is_empty()
3168                && prepared_installed_state.is_some()
3169            {
3170                return Err(DbError::Checkpoint(format!(
3171                "[LDB-6053] assignment {} found installed vnode state without current-process predecessor ownership; coordinated recovery is required",
3172                snapshot.version
3173            )));
3174            }
3175            let old_set: rustc_hash::FxHashSet<u32> = old_owned.iter().copied().collect();
3176            let new_owned = preflight_new_owned;
3177            // Compute from the new assignment before reading state or publishing ownership.
3178            let vnodes_requiring_restore: Vec<u32> = if matches!(
3179                mode,
3180                AssignmentAdoptionMode::ColdRecovery
3181                    | AssignmentAdoptionMode::StoppedRecoveryTopology
3182            ) {
3183                Vec::new()
3184            } else {
3185                new_owned
3186                    .iter()
3187                    .copied()
3188                    .filter(|vnode| !old_set.contains(vnode))
3189                    .collect()
3190            };
3191            // Startup installs the committed assignment before the lifecycle restores its complete
3192            // graph. A live graph loads only its acquired vnode ranges at this fenced boundary.
3193            let startup_restore_deferred = matches!(
3194                mode,
3195                AssignmentAdoptionMode::ColdRecovery
3196                    | AssignmentAdoptionMode::StoppedRecoveryTopology
3197            ) || pristine_startup_bootstrap;
3198            let prepared_handoff_cut_mismatch = if mode == AssignmentAdoptionMode::LiveTransition
3199                && !vnodes_requiring_restore.is_empty()
3200                && !startup_restore_deferred
3201            {
3202                let handoff = committed_handoff_checkpoint.as_ref().ok_or_else(|| {
3203                DbError::Checkpoint(format!(
3204                    "[LDB-6050] assignment {} has no exact committed checkpoint authority for vnode acquisition",
3205                    snapshot.version
3206                ))
3207            })?;
3208                let coordinator = guard.as_ref().ok_or_else(|| {
3209                    DbError::Checkpoint(format!(
3210                        "[LDB-6050] assignment {} has no checkpoint coordinator for vnode acquisition",
3211                        snapshot.version
3212                    ))
3213                })?;
3214                coordinator.last_committed_ref() != Some(handoff)
3215            } else {
3216                false
3217            };
3218            let state_frames = if mode == AssignmentAdoptionMode::LiveTransition
3219                && !vnodes_requiring_restore.is_empty()
3220                && !startup_restore_deferred
3221                && !prepared_handoff_cut_mismatch
3222            {
3223                let handoff = committed_handoff_checkpoint
3224                    .as_ref()
3225                    .expect("live vnode acquisition checked exact committed checkpoint authority");
3226                let predecessor = predecessor_fence.as_ref().ok_or_else(|| {
3227                    DbError::Checkpoint(format!(
3228                    "[LDB-6050] assignment {} has no predecessor certificate for vnode acquisition",
3229                    snapshot.version
3230                ))
3231                })?;
3232                let coordinator = guard.as_ref().ok_or_else(|| {
3233                    DbError::Checkpoint(format!(
3234                    "[LDB-6050] assignment {} has no checkpoint coordinator for vnode acquisition",
3235                    snapshot.version
3236                ))
3237                })?;
3238                let max_payload_bytes = self
3239                    .config
3240                    .pipeline_max_managed_state_bytes
3241                    .unwrap_or(crate::config::DEFAULT_MAX_MANAGED_STATE_BYTES);
3242                tokio::select! {
3243                    biased;
3244                    () = self.assignment_restore_shutdown.cancelled() => {
3245                        return Err(DbError::Shutdown);
3246                    }
3247                    () = prepared_runtime_shutdown.cancelled() => {
3248                        return Err(DbError::Shutdown);
3249                    }
3250                    result = coordinator.load_handoff_state_frames(
3251                        handoff,
3252                        predecessor,
3253                        observed_assignment.owners(),
3254                        &vnodes_requiring_restore,
3255                        // Portable whole cuts carry donor-global channel/frontier state. They are
3256                        // required even when this process retains other vnodes (notably aggregates).
3257                        true,
3258                        max_payload_bytes,
3259                        deadline,
3260                    ) => result?,
3261                }
3262            } else {
3263                Vec::new()
3264            };
3265            // A survivor adopts the append-view logs of participants that left, whether or not
3266            // it acquires a vnode. Without a handoff cut there is nothing to adopt from, and the
3267            // leader refuses a cut that loses a departed participant's segments.
3268            let adopted_view_logs = match (
3269                committed_handoff_checkpoint.as_ref(),
3270                predecessor_fence.as_ref(),
3271                guard.as_ref(),
3272            ) {
3273                (Some(handoff), Some(predecessor), Some(coordinator))
3274                    if mode == AssignmentAdoptionMode::LiveTransition
3275                        && !startup_restore_deferred
3276                        && self.mv_store.read().has_append_views() =>
3277                {
3278                    coordinator
3279                        .load_handoff_view_log_adoptions(
3280                            handoff,
3281                            predecessor,
3282                            &target_fence,
3283                            deadline,
3284                        )
3285                        .await?
3286                }
3287                _ => Vec::new(),
3288            };
3289
3290            drop(guard);
3291
3292            // Re-acquire the epoch boundary and discard the prepared adoption if another rotation won,
3293            // the bound pipeline changed, or a newer durable cut appeared during the reads.
3294            let _execution_guard = Arc::clone(&self.rotation_execution_fence)
3295                .write_owned()
3296                .await;
3297            let mut guard = self.coordinator.lock().await;
3298            let current_pipeline_identity = guard
3299                .as_ref()
3300                .map(crate::checkpoint_coordinator::CheckpointCoordinator::bound_pipeline_identity)
3301                .transpose()?;
3302            if current_pipeline_identity != prepared_pipeline_identity {
3303                return Err(DbError::Checkpoint(format!(
3304                "[LDB-6053] pipeline identity changed while preparing assignment {}; retrying the complete transition",
3305                snapshot.version
3306                )));
3307            }
3308            let durable_recovery_fault_is_active = if mode
3309                == AssignmentAdoptionMode::StoppedRecoveryTopology
3310                && stopped_committed_topology_shape
3311            {
3312                let request = controller
3313                    .recovery_fault_request(prepared_recovery_fault)
3314                    .map_err(|error| {
3315                        DbError::Checkpoint(format!(
3316                            "[LDB-6053] assignment {} stopped-recovery fault identity changed: {error}",
3317                            snapshot.version
3318                        ))
3319                    })?;
3320                let outcome = tokio::time::timeout_at(deadline, controller.report_fault(request))
3321                    .await
3322                    .map_err(|_| {
3323                        DbError::Checkpoint(format!(
3324                            "[LDB-6053] assignment {} stopped-recovery fault recheck timed out",
3325                            snapshot.version
3326                        ))
3327                    })?
3328                    .map_err(|error| {
3329                        DbError::Checkpoint(format!(
3330                            "[LDB-6053] assignment {} stopped-recovery fault recheck failed: {error}",
3331                            snapshot.version
3332                        ))
3333                    })?;
3334                outcome == laminar_core::cluster::control::RecoveryFaultReportOutcome::Active
3335            } else {
3336                true
3337            };
3338            let durable_stopped_abort_round_is_active = if mode
3339                == AssignmentAdoptionMode::StoppedRecoveryTopology
3340                && stopped_aborted_topology_shape
3341            {
3342                audited_stopped_terminal_round(
3343                    controller.as_ref(),
3344                    predecessor_fence
3345                        .as_ref()
3346                        .expect("abort shape has predecessor"),
3347                    deadline,
3348                )
3349                .await?
3350                    == stopped_abort_round
3351            } else {
3352                true
3353            };
3354            let durable_stopped_commit_owner_round_is_active = if mode
3355                == AssignmentAdoptionMode::StoppedRecoveryTopology
3356                && stopped_committed_owner_topology_shape
3357            {
3358                audited_stopped_terminal_round(
3359                    controller.as_ref(),
3360                    predecessor_fence
3361                        .as_ref()
3362                        .expect("committed owner shape has predecessor"),
3363                    deadline,
3364                )
3365                .await?
3366                    == stopped_commit_owner_round
3367            } else {
3368                true
3369            };
3370            let durable_stopped_recovery_successor_round_is_active = if mode
3371                == AssignmentAdoptionMode::StoppedRecoveryTopology
3372                && stopped_recovery_successor_topology_shape
3373            {
3374                audited_stopped_recovery_successor_round(
3375                    controller.as_ref(),
3376                    predecessor_fence
3377                        .as_ref()
3378                        .expect("recovery successor shape has predecessor"),
3379                    recovery_proof.as_ref().expect("audited active proof"),
3380                    deadline,
3381                )
3382                .await?
3383                    == stopped_recovery_successor_round
3384            } else {
3385                true
3386            };
3387            let durable_stopped_commit_owner_pin_is_active = if mode
3388                == AssignmentAdoptionMode::StoppedRecoveryTopology
3389                && stopped_committed_owner_topology_shape
3390            {
3391                assignment_authority::handoff_pin_is_active(
3392                    &controller,
3393                    &target_fence,
3394                    committed_handoff_checkpoint
3395                        .as_ref()
3396                        .expect("committed drain authority has a handoff checkpoint"),
3397                    &format!(
3398                        "assignment {} stopped-Commit handoff-pin recheck",
3399                        snapshot.version
3400                    ),
3401                    deadline,
3402                )
3403                .await?
3404            } else {
3405                true
3406            };
3407            let durable_stopped_recovery_successor_pin_is_active = if mode
3408                == AssignmentAdoptionMode::StoppedRecoveryTopology
3409                && stopped_recovery_successor_topology_shape
3410            {
3411                assignment_authority::handoff_pin_is_active(
3412                    &controller,
3413                    &target_fence,
3414                    committed_handoff_checkpoint
3415                        .as_ref()
3416                        .expect("recovery successor authority has a handoff checkpoint"),
3417                    &format!(
3418                        "assignment {} stopped-recovery handoff-pin recheck",
3419                        snapshot.version
3420                    ),
3421                    deadline,
3422                )
3423                .await?
3424            } else {
3425                true
3426            };
3427            if tokio::time::Instant::now() >= deadline {
3428                return Err(DbError::Checkpoint(format!(
3429                    "assignment {} adoption reached its deadline before publication",
3430                    snapshot.version
3431                )));
3432            }
3433            let handoff_cut_mismatch = if mode == AssignmentAdoptionMode::LiveTransition
3434                && !vnodes_requiring_restore.is_empty()
3435                && !startup_restore_deferred
3436            {
3437                guard.as_ref().and_then(
3438                    crate::checkpoint_coordinator::CheckpointCoordinator::last_committed_ref,
3439                ) != committed_handoff_checkpoint.as_ref()
3440            } else {
3441                false
3442            };
3443            if prepared_handoff_cut_mismatch && !handoff_cut_mismatch {
3444                // The local checkpoint advanced while the handoff read boundary was released.
3445                // Retry so acquired frames are loaded from the now-coherent exact cut.
3446                return Err(DbError::Checkpoint(format!(
3447                    "[LDB-6050] assignment {} resident checkpoint advanced to its handoff cut during adoption; retrying",
3448                    snapshot.version
3449                )));
3450            }
3451            let current_version = registry.assignment_version();
3452            if snapshot.version <= current_version {
3453                return Ok(SnapshotAdoption {
3454                    adopted: false,
3455                    version: snapshot.version,
3456                    recovery_required: false,
3457                });
3458            }
3459            if current_version != prepared_from_version {
3460                return Err(DbError::Checkpoint(format!(
3461                "[LDB-6053] assignment base advanced from {prepared_from_version} to {current_version} while preparing target {}; retrying from the new owner set",
3462                snapshot.version
3463            )));
3464            }
3465            let adoption = SnapshotAdoption {
3466                adopted: true,
3467                version: snapshot.version,
3468                recovery_required: handoff_cut_mismatch,
3469            };
3470            // A resident graph advances every adjacent topology generation, including zero-owner
3471            // generations, so a later acquisition is based on the exact predecessor assignment.
3472            let has_local_transition = mode == AssignmentAdoptionMode::LiveTransition
3473                && !startup_restore_deferred
3474                && !handoff_cut_mismatch
3475                && (!old_owned.is_empty()
3476                    || !vnodes_requiring_restore.is_empty()
3477                    || (observed_version != 0 && prepared_db_state == DbState::Running));
3478            let pending_transition = if has_local_transition {
3479                guard.as_ref().ok_or_else(|| {
3480                DbError::Checkpoint(format!(
3481                    "[LDB-6053] assignment {} has no checkpoint coordinator for transition identity",
3482                    snapshot.version
3483                ))
3484            })?;
3485                let pipeline_identity = current_pipeline_identity.clone().ok_or_else(|| {
3486                    DbError::Checkpoint(format!(
3487                        "[LDB-6053] assignment {} has no bound pipeline identity",
3488                        snapshot.version
3489                    ))
3490                })?;
3491                let transition =
3492                    crate::vnode_transition_staging::PendingVnodeTransition::assignment_change(
3493                        predecessor_fence.clone().ok_or_else(|| {
3494                            DbError::Checkpoint(format!(
3495                                "[LDB-6053] assignment {} has no predecessor certificate",
3496                                snapshot.version
3497                            ))
3498                        })?,
3499                        observed_assignment.owners(),
3500                        target_fence.clone(),
3501                        &new_assignment,
3502                        local_participant,
3503                        pipeline_identity,
3504                        state_frames,
3505                        adopted_view_logs,
3506                        final_owner_exit,
3507                    )?;
3508                if transition.acquired_vnodes() != vnodes_requiring_restore {
3509                    return Err(DbError::Checkpoint(format!(
3510                    "[LDB-6053] assignment {} transition acquisition roster changed during preparation",
3511                    snapshot.version
3512                )));
3513                }
3514                Some(Arc::new(transition))
3515            } else {
3516                None
3517            };
3518            let installed_after_publication = if mode == AssignmentAdoptionMode::LiveTransition
3519                && !new_owned.is_empty()
3520                && pending_transition.is_none()
3521                && vnodes_requiring_restore.is_empty()
3522            {
3523                current_pipeline_identity
3524                    .clone()
3525                    .map(|pipeline_identity| {
3526                        crate::vnode_transition_staging::InstalledVnodeStateBinding::new(
3527                            target_fence.clone(),
3528                            pipeline_identity,
3529                        )
3530                    })
3531                    .transpose()?
3532            } else {
3533                None
3534            };
3535
3536            // Assignment zero is intentionally noncanonical and can never have published a readiness
3537            // report. Replacement processes therefore stage their first target restore without an
3538            // impossible predecessor withdrawal; the watcher publishes target=false after adoption.
3539            if predecessor_readiness_withdrawal_required(
3540                pending_transition
3541                    .as_ref()
3542                    .is_some_and(|pending| pending.requires_predecessor_binding()),
3543                observed_assignment.version(),
3544            ) {
3545                tokio::time::timeout_at(
3546                    deadline,
3547                    self.publish_local_vnode_state_report(&controller, &observed_assignment, false),
3548                )
3549                .await
3550                .map_err(|_| {
3551                    DbError::Checkpoint(format!(
3552                        "assignment {} timed out withdrawing predecessor vnode-state readiness",
3553                        snapshot.version
3554                    ))
3555                })??;
3556            }
3557
3558            // Publish one complete immutable transition while the compute-cycle write fence is held,
3559            // then publish ownership before releasing its slot. The next cycle therefore observes
3560            // either the old assignment or the exact target-bound transition, never split half-state.
3561            // Close and pipeline stop cancel under the write side of this lock. Holding the read side
3562            // through publication prevents shutdown from landing between this check and the registry.
3563            // Serialize the cold eligibility check with start/stop/recovery-fence lifecycle claims.
3564            // Stop takes this same lock before clearing installed state and changing Faulted, while a
3565            // recovery Release takes it before lifting the persistent lifecycle fence.
3566            let lifecycle_claim = (mode == AssignmentAdoptionMode::ColdRecovery
3567                || mode == AssignmentAdoptionMode::StoppedRecoveryTopology
3568                || startup_restore_deferred
3569                || handoff_cut_mismatch)
3570                .then(|| self.startup_attempt.lock());
3571            let runtime_shutdown = self.runtime_shutdown.read();
3572            let runtime_generation_is_valid =
3573                if mode == AssignmentAdoptionMode::StoppedRecoveryTopology {
3574                    runtime_shutdown.is_cancelled() && prepared_runtime_shutdown.is_cancelled()
3575                } else {
3576                    !runtime_shutdown.is_cancelled() && !prepared_runtime_shutdown.is_cancelled()
3577                };
3578            if self.is_closed()
3579                || self.assignment_restore_shutdown.is_cancelled()
3580                || !runtime_generation_is_valid
3581            {
3582                return Err(DbError::Shutdown);
3583            }
3584            let mut pending_slot = self.pending_vnode_transition.lock();
3585            if pending_slot.is_some() {
3586                return Err(DbError::Checkpoint(format!(
3587                "[LDB-6053] assignment {} reached publication while another vnode transition was pending",
3588                snapshot.version
3589            )));
3590            }
3591            let mut installed_state = self.installed_vnode_state.lock();
3592            if DbState::load(&self.state) != prepared_db_state
3593                || installed_state.as_ref() != prepared_installed_state.as_ref()
3594            {
3595                return Err(DbError::Checkpoint(format!(
3596                "[LDB-6053] local execution state changed while preparing assignment {}; retrying from the current state",
3597                snapshot.version
3598                )));
3599            }
3600            if startup_restore_deferred
3601                && mode == AssignmentAdoptionMode::LiveTransition
3602                && (prepared_db_state != DbState::Created
3603                    || lifecycle_claim
3604                        .as_ref()
3605                        .is_none_or(|attempt| attempt.as_ref().is_some())
3606                    || !self.source_gate.load(std::sync::atomic::Ordering::Acquire)
3607                    || !controller.is_recovering()
3608                    || !controller.process_lease_is_live()
3609                    || (audited_recovery
3610                        && (prepared_recovery_fault == 0
3611                            || self
3612                                .pending_recovery_fault
3613                                .load(std::sync::atomic::Ordering::Acquire)
3614                                != prepared_recovery_fault))
3615                    || guard.is_some()
3616                    || pending_slot.is_some()
3617                    || installed_state.is_some()
3618                    || self.recover_target_epoch.lock().is_some()
3619                    || self
3620                        .coordinated_recovery_fenced
3621                        .load(std::sync::atomic::Ordering::Acquire)
3622                    || self
3623                        .catalog_cleanup_fenced
3624                        .load(std::sync::atomic::Ordering::Acquire))
3625            {
3626                return Err(DbError::Checkpoint(format!(
3627                    "[LDB-6053] assignment {} startup-deferred publication requires the exact Created assignment-zero lifecycle with closed intake, live process authority, and no concurrent startup or recovered state",
3628                    snapshot.version
3629                )));
3630            }
3631            if mode == AssignmentAdoptionMode::ColdRecovery
3632                && (!self.source_gate.load(std::sync::atomic::Ordering::Acquire)
3633                    || !controller.is_recovering()
3634                    || !self
3635                        .coordinated_recovery_fenced
3636                        .load(std::sync::atomic::Ordering::Acquire)
3637                    || self
3638                        .pending_recovery_fault
3639                        .load(std::sync::atomic::Ordering::Acquire)
3640                        != prepared_recovery_fault
3641                    || prepared_recovery_fault == 0
3642                    || !controller.process_lease_is_live()
3643                    || self
3644                        .catalog_cleanup_fenced
3645                        .load(std::sync::atomic::Ordering::Acquire)
3646                    || pending_slot.is_some()
3647                    || installed_state.is_some())
3648            {
3649                return Err(DbError::Checkpoint(format!(
3650                "[LDB-6053] recovery assignment {} cold-publication authority changed before registry commit",
3651                snapshot.version
3652            )));
3653            }
3654            // Startup publishes a fresh attempt before it can replace the checkpoint coordinator.
3655            // Holding this claim and matching the exact completed Arc therefore proves that the
3656            // coordinator whose pipeline identity was rechecked above is still the stopped graph's
3657            // coordinator, not a same-identity successor generation.
3658            let stopped_startup_attempt_is_same = lifecycle_claim.as_ref().is_some_and(|owned| {
3659                let (Some(current), Some(prepared)) =
3660                    (owned.as_ref(), prepared_startup_attempt.as_ref())
3661                else {
3662                    return false;
3663                };
3664                Arc::ptr_eq(current, prepared) && current.is_complete()
3665            });
3666            let stopped_topology_shape_is_same = (stopped_committed_topology_shape
3667                && prepared_recovery_fault != 0
3668                && self
3669                    .pending_recovery_fault
3670                    .load(std::sync::atomic::Ordering::Acquire)
3671                    == prepared_recovery_fault
3672                && durable_recovery_fault_is_active)
3673                || (stopped_aborted_topology_shape
3674                    && stopped_abort_round.is_some()
3675                    && durable_stopped_abort_round_is_active)
3676                || (stopped_committed_owner_topology_shape
3677                    && stopped_commit_owner_round.is_some()
3678                    && stopped_commit_owner_pin_is_active
3679                    && durable_stopped_commit_owner_round_is_active
3680                    && durable_stopped_commit_owner_pin_is_active)
3681                || (stopped_recovery_successor_topology_shape
3682                    && stopped_recovery_successor_round.is_some()
3683                    && durable_stopped_recovery_successor_round_is_active
3684                    && durable_stopped_recovery_successor_pin_is_active);
3685            if mode == AssignmentAdoptionMode::StoppedRecoveryTopology
3686                && (prepared_db_state != DbState::Created
3687                    || !cold_source_gate_was_closed
3688                    || !self.source_gate.load(std::sync::atomic::Ordering::Acquire)
3689                    || !controller.process_lease_is_live()
3690                    || self
3691                        .cluster_authority_revoked
3692                        .load(std::sync::atomic::Ordering::Acquire)
3693                    || !self
3694                        .coordinated_recovery_fenced
3695                        .load(std::sync::atomic::Ordering::Acquire)
3696                    || self
3697                        .coordinated_lifecycle_active
3698                        .load(std::sync::atomic::Ordering::Acquire)
3699                    || self
3700                        .catalog_cleanup_fenced
3701                        .load(std::sync::atomic::Ordering::Acquire)
3702                    || self.recover_target_epoch.lock().is_some()
3703                    || !stopped_startup_attempt_is_same
3704                    || !stopped_topology_shape_is_same
3705                    || guard.is_none()
3706                    || current_pipeline_identity.is_none()
3707                    || pending_slot.is_some()
3708                    || installed_state.is_some()
3709                    || registry.versioned_snapshot().owners() != prepared_assignment.owners()
3710                    || if new_owned.is_empty() {
3711                        target_fence.participant_incarnation(self_id.0).is_some()
3712                    } else {
3713                        target_fence.participant_incarnation(self_id.0) != Some(current_incarnation)
3714                    }
3715                    || snapshot.version != observed_version.saturating_add(1))
3716            {
3717                return Err(DbError::Checkpoint(format!(
3718                    "[LDB-6053] assignment {} stopped-recovery topology authority changed before registry commit",
3719                    snapshot.version
3720                )));
3721            }
3722            if mode == AssignmentAdoptionMode::StoppedRecoveryTopology || handoff_cut_mismatch {
3723                // Readiness remains false for a stopped graph or a live graph whose cut mismatches
3724                // the handoff. Keep recovery asserted before exposing topology so the owner-complete
3725                // certificate can trigger a full restore without opening source intake.
3726                controller.set_recovering(true);
3727                controller.publish_checkpoint_drain_transition(None);
3728                controller.publish_checkpoint_assignment_fence(None);
3729            }
3730            if handoff_cut_mismatch {
3731                // Allocate the durable fault identity before the first target mutation. Once the
3732                // registry advances, publication below is deliberately infallible and atomic with
3733                // the already-held execution/coordinator/startup claims.
3734                self.coordinated_recovery_fenced
3735                    .store(true, std::sync::atomic::Ordering::Release);
3736                crate::coordinated_recovery::queue_local_fault(
3737                    &controller,
3738                    &self.pending_recovery_fault,
3739                )
3740                .map_err(|error| {
3741                    DbError::Checkpoint(format!(
3742                        "[LDB-6050] assignment {} could not queue exact-handoff recovery: {error}",
3743                        snapshot.version
3744                    ))
3745                })?;
3746            }
3747            *pending_slot = pending_transition;
3748            registry.set_assignment_and_version(new_assignment, snapshot.version);
3749            if let Some(coord) = guard.as_mut() {
3750                coord.set_assignment_version(snapshot.version);
3751                coord.set_vnode_set(new_owned.clone());
3752            }
3753            if pending_slot.is_none() {
3754                *installed_state = installed_after_publication;
3755            }
3756            if handoff_cut_mismatch {
3757                // The resident source/runtime cut and imported vnode cut must be one exact
3758                // checkpoint. Publish target topology (with false readiness) so recovery can form
3759                // its owner-complete quorum, but never expose mixed-cut state to graph execution.
3760                *installed_state = None;
3761            }
3762            drop(installed_state);
3763            drop(pending_slot);
3764            drop(guard);
3765            drop(runtime_shutdown);
3766            drop(lifecycle_claim);
3767
3768            tracing::info!(version = snapshot.version, "adopted assignment snapshot",);
3769            Ok(adoption)
3770        })
3771    }
3772
3773    /// Wait for every local source to publish an exact FIFO drain receipt.
3774    #[cfg(feature = "cluster")]
3775    pub(crate) async fn prepare_local_source_drain(
3776        &self,
3777        transition: &laminar_core::checkpoint::AssignmentDrainTransition,
3778        participant: laminar_core::checkpoint::CheckpointParticipant,
3779        deadline: tokio::time::Instant,
3780    ) -> Result<(), DbError> {
3781        crate::pipeline::streaming_coordinator::prepare_owned_source_drain(
3782            &self.owned_source_tasks,
3783            transition,
3784            participant,
3785            deadline,
3786        )
3787        .await
3788        .map_err(|error| DbError::Checkpoint(format!("source drain failed: {error}")))
3789    }
3790
3791    /// Resolve the exact local source drain after target commit or abort.
3792    #[cfg(feature = "cluster")]
3793    pub(crate) async fn resolve_local_source_drain(
3794        &self,
3795        round: laminar_core::checkpoint::AssignmentDrainId,
3796        outcome: laminar_connectors::connector::SourceDrainOutcome,
3797        deadline: tokio::time::Instant,
3798    ) -> Result<(), DbError> {
3799        crate::pipeline::streaming_coordinator::resolve_owned_source_drain(
3800            &self.owned_source_tasks,
3801            laminar_connectors::connector::SourceDrainResolution { round, outcome },
3802            deadline,
3803        )
3804        .await
3805        .map_err(|error| DbError::Checkpoint(format!("source drain resolution failed: {error}")))
3806    }
3807
3808    /// Validate a draining generation without changing local ownership. Source intake is held by
3809    /// the source-task drain protocol until the durable outcome is resolved.
3810    #[cfg(feature = "cluster")]
3811    pub(crate) fn validate_source_drain_snapshot(
3812        &self,
3813        snapshot: &laminar_core::cluster::control::AssignmentSnapshot,
3814    ) -> Result<(), DbError> {
3815        if !snapshot.draining {
3816            return Err(DbError::Checkpoint(format!(
3817                "assignment {} is not a draining generation",
3818                snapshot.version
3819            )));
3820        }
3821        if !snapshot.has_canonical_participants() {
3822            return Err(DbError::Checkpoint(format!(
3823                "draining assignment {} has no canonical process roster",
3824                snapshot.version
3825            )));
3826        }
3827        let registry = self.vnode_registry.lock().clone().ok_or_else(|| {
3828            DbError::Checkpoint(format!(
3829                "draining assignment {} cannot be validated without a vnode registry",
3830                snapshot.version
3831            ))
3832        })?;
3833        let expected_version = registry
3834            .assignment_version()
3835            .checked_add(1)
3836            .ok_or_else(|| DbError::Checkpoint("assignment version overflow".into()))?;
3837        if snapshot.version != expected_version {
3838            return Err(DbError::Checkpoint(format!(
3839                "draining assignment {} is not the exact successor of local assignment {}",
3840                snapshot.version,
3841                registry.assignment_version()
3842            )));
3843        }
3844        snapshot
3845            .to_vnode_vec(registry.vnode_count())
3846            .map_err(|error| DbError::Checkpoint(error.to_string()))?;
3847        tracing::info!(
3848            version = snapshot.version,
3849            "validated global source-drain generation"
3850        );
3851        Ok(())
3852    }
3853
3854    #[cfg(feature = "cluster")]
3855    pub(crate) fn set_cluster_controller(
3856        &self,
3857        controller: Arc<laminar_core::cluster::control::ClusterController>,
3858    ) -> Result<(), DbError> {
3859        if !self.is_cluster_runtime() {
3860            return Err(DbError::Config(
3861                "cluster controller cannot be installed on a local runtime".into(),
3862            ));
3863        }
3864        let mut installed = self.cluster_controller.lock();
3865        match installed.as_ref() {
3866            None => {
3867                *installed = Some(controller);
3868                Ok(())
3869            }
3870            Some(current) if Arc::ptr_eq(current, &controller) => Ok(()),
3871            Some(_) => Err(DbError::Config(
3872                "cluster controller replacement requires a new LaminarDB graph generation".into(),
3873            )),
3874        }
3875    }
3876
3877    pub(crate) fn set_vnode_registry(&self, registry: Arc<laminar_core::state::VnodeRegistry>) {
3878        *self.vnode_registry.lock() = Some(registry);
3879    }
3880
3881    /// Collect this node's physical table slice without invoking distributed fan-out.
3882    ///
3883    /// This is intended for cluster diagnostics and placement validation. It deliberately returns
3884    /// immutable batches rather than exposing the mutable `DataFusion` catalog.
3885    ///
3886    /// # Errors
3887    /// Returns an error when the local table cannot be resolved, planned, or collected.
3888    #[cfg(feature = "cluster")]
3889    pub async fn collect_local_table(&self, name: &str) -> Result<Vec<RecordBatch>, DbError> {
3890        const LOCAL_SCAN_NAME: &str = "__laminar_local_table_scan";
3891
3892        let _catalog_guard = self.topology_ddl_lock.read().await;
3893        let provider = self.ctx.table_provider(exact_table_reference(name)).await?;
3894        let context = self.create_auxiliary_context();
3895        context.register_table(exact_table_reference(LOCAL_SCAN_NAME), provider)?;
3896        Ok(context
3897            .sql(&format!("SELECT * FROM {LOCAL_SCAN_NAME}"))
3898            .await?
3899            .collect()
3900            .await?)
3901    }
3902
3903    /// Returns a fluent builder for constructing a [`LaminarDB`].
3904    #[must_use]
3905    pub fn builder() -> LaminarDbBuilder {
3906        LaminarDbBuilder::new()
3907    }
3908
3909    #[allow(unused_variables)]
3910    pub(crate) fn register_builtin_connectors(
3911        registry: &laminar_connectors::registry::ConnectorRegistry,
3912    ) -> Result<(), laminar_connectors::error::ConnectorError> {
3913        laminar_connectors::generator::register_generator_source(registry)?;
3914        #[cfg(feature = "kafka")]
3915        {
3916            laminar_connectors::kafka::register_kafka_source(registry)?;
3917            laminar_connectors::kafka::register_kafka_sink(registry)?;
3918        }
3919        #[cfg(feature = "postgres-cdc")]
3920        {
3921            laminar_connectors::postgres::register_postgres_cdc_source(registry)?;
3922        }
3923        #[cfg(feature = "postgres-sink")]
3924        {
3925            laminar_connectors::postgres::register_postgres_sink(registry)?;
3926        }
3927        #[cfg(feature = "delta-lake")]
3928        {
3929            laminar_connectors::lakehouse::register_delta_lake_sink(registry)?;
3930            laminar_connectors::lakehouse::register_delta_lake_source(registry)?;
3931        }
3932        #[cfg(any(
3933            feature = "iceberg",
3934            feature = "iceberg-gcs",
3935            feature = "iceberg-azure"
3936        ))]
3937        {
3938            laminar_connectors::lakehouse::register_iceberg_sink(registry)?;
3939            laminar_connectors::lakehouse::register_iceberg_source(registry)?;
3940        }
3941        #[cfg(feature = "websocket")]
3942        {
3943            laminar_connectors::websocket::register_websocket_source(registry)?;
3944            laminar_connectors::websocket::register_websocket_sink(registry)?;
3945        }
3946        #[cfg(feature = "mongodb-cdc")]
3947        {
3948            laminar_connectors::mongodb::register_mongodb_cdc_source(registry)?;
3949            laminar_connectors::mongodb::register_mongodb_sink(registry)?;
3950        }
3951        #[cfg(feature = "files")]
3952        {
3953            laminar_connectors::files::register_file_source(registry)?;
3954            laminar_connectors::files::register_file_sink(registry)?;
3955        }
3956        #[cfg(feature = "otel")]
3957        {
3958            laminar_connectors::otel::register_otel_source(registry)?;
3959        }
3960        #[cfg(feature = "nats")]
3961        {
3962            laminar_connectors::nats::register_nats_source(registry)?;
3963            laminar_connectors::nats::register_nats_sink(registry)?;
3964        }
3965        #[cfg(feature = "rabbitmq")]
3966        {
3967            laminar_connectors::rabbitmq::register_rabbitmq_source(registry)?;
3968            laminar_connectors::rabbitmq::register_rabbitmq_sink(registry)?;
3969        }
3970        #[cfg(feature = "ibmmq")]
3971        {
3972            laminar_connectors::ibmmq::register_ibmmq_source(registry)?;
3973            laminar_connectors::ibmmq::register_ibmmq_sink(registry)?;
3974        }
3975        #[cfg(feature = "fix")]
3976        {
3977            laminar_connectors::fix::register_fix_source(registry)?;
3978        }
3979        Ok(())
3980    }
3981
3982    fn handle_register_lookup_table(
3983        &self,
3984        info: laminar_sql::planner::LookupTableInfo,
3985    ) -> Result<ExecuteResult, DbError> {
3986        use laminar_sql::parser::lookup_table::LookupConnector;
3987
3988        self.preflight_lookup_connector(&info.properties)?;
3989        if info.primary_key.len() != 1 {
3990            return Err(DbError::InvalidOperation(
3991                "Lookup table requires a single-column primary key".into(),
3992            ));
3993        }
3994        let pk = info.primary_key[0].clone();
3995
3996        self.table_store
3997            .write()
3998            .create_table(&info.name, info.arrow_schema.clone(), &pk)?;
3999
4000        if matches!(&info.properties.connector, LookupConnector::External(_)) {
4001            self.register_lookup_connector(&info, &pk)?;
4002        }
4003
4004        {
4005            let provider = crate::table_provider::ReferenceTableProvider::new(
4006                info.name.clone(),
4007                info.arrow_schema.clone(),
4008                self.table_store.clone(),
4009            );
4010            match self
4011                .ctx
4012                .register_table(exact_table_reference(&info.name), Arc::new(provider))
4013            {
4014                Ok(None) => {}
4015                Ok(Some(previous)) => {
4016                    let _ = self
4017                        .ctx
4018                        .register_table(exact_table_reference(&info.name), previous);
4019                    return Err(DbError::InvalidOperation(format!(
4020                        "cannot create lookup table '{}': its provider was claimed concurrently",
4021                        info.name
4022                    )));
4023                }
4024                Err(error) => {
4025                    return Err(DbError::InvalidOperation(format!(
4026                        "failed to register lookup table '{}': {error}",
4027                        info.name
4028                    )));
4029                }
4030            }
4031        }
4032
4033        if let Some(batch) = self.table_store.read().to_record_batch(&info.name)? {
4034            self.lookup_registry.register(
4035                &info.name,
4036                laminar_sql::datafusion::LookupSnapshot { batch },
4037            );
4038        }
4039
4040        self.refresh_lookup_optimizer_rule();
4041
4042        Ok(ExecuteResult::Ddl(DdlInfo {
4043            statement_type: "CREATE LOOKUP TABLE".to_string(),
4044            object_name: info.name,
4045            #[cfg(feature = "cluster")]
4046            topology_operation: None,
4047            applied: true,
4048        }))
4049    }
4050
4051    pub(crate) fn preflight_lookup_connector(
4052        &self,
4053        properties: &laminar_sql::parser::lookup_table::LookupTableProperties,
4054    ) -> Result<(), DbError> {
4055        use laminar_sql::parser::lookup_table::{LookupConnector, LookupStrategy};
4056
4057        if properties.strategy == LookupStrategy::OnDemand
4058            && self.config.delivery_guarantee
4059                == laminar_connectors::connector::DeliveryGuarantee::ExactlyOnce
4060        {
4061            return Err(DbError::InvalidOperation(
4062                "on-demand LOOKUP TABLE is incompatible with exactly-once delivery because \
4063                 external lookup results are not checkpointed"
4064                    .into(),
4065            ));
4066        }
4067        let connector = match &properties.connector {
4068            LookupConnector::Static => {
4069                if properties.strategy == LookupStrategy::OnDemand {
4070                    return Err(DbError::InvalidOperation(
4071                        "static LOOKUP TABLE supports only the replicated strategy".into(),
4072                    ));
4073                }
4074                return Ok(());
4075            }
4076            LookupConnector::External(name) => name,
4077        };
4078        let (available, capability) = match properties.strategy {
4079            LookupStrategy::Replicated => (
4080                self.connector_registry.has_table_source(connector),
4081                "snapshot-capable table source",
4082            ),
4083            LookupStrategy::OnDemand => (
4084                self.connector_registry.has_lookup_source(connector),
4085                "on-demand lookup source",
4086            ),
4087        };
4088        if !available {
4089            return Err(DbError::InvalidOperation(format!(
4090                "LOOKUP TABLE connector '{connector}' has no registered {capability} required \
4091                 by strategy '{}'",
4092                properties.strategy
4093            )));
4094        }
4095        Ok(())
4096    }
4097
4098    /// Rebuild the lookup optimizer rules for the current set of registered tables.
4099    pub(crate) fn refresh_lookup_optimizer_rule(&self) {
4100        use laminar_sql::planner::lookup_join::{LookupColumnPruningRule, LookupJoinRewriteRule};
4101        use laminar_sql::planner::predicate_split::{
4102            PlanPushdownMode, PlanSourceCapabilities, PredicateSplitterRule,
4103            SourceCapabilitiesRegistry,
4104        };
4105
4106        self.ctx.remove_optimizer_rule("lookup_join_rewrite");
4107        self.ctx.remove_optimizer_rule("predicate_splitter");
4108        self.ctx.remove_optimizer_rule("lookup_column_pruning");
4109
4110        let tables = self.planner.lock().lookup_tables_cloned();
4111        if tables.is_empty() {
4112            return;
4113        }
4114
4115        let mut caps_registry = SourceCapabilitiesRegistry::default();
4116        for (name, info) in &tables {
4117            let mode = match info.properties.pushdown_mode {
4118                laminar_sql::parser::lookup_table::PushdownMode::Enabled
4119                | laminar_sql::parser::lookup_table::PushdownMode::Auto => PlanPushdownMode::Full,
4120                laminar_sql::parser::lookup_table::PushdownMode::Disabled => PlanPushdownMode::None,
4121            };
4122            let pk_set: std::collections::HashSet<String> =
4123                info.primary_key.iter().cloned().collect();
4124            caps_registry.register(
4125                name.clone(),
4126                PlanSourceCapabilities {
4127                    pushdown_mode: mode,
4128                    eq_columns: pk_set,
4129                    range_columns: std::collections::HashSet::new(),
4130                    in_columns: std::collections::HashSet::new(),
4131                    supports_null_check: false,
4132                },
4133            );
4134        }
4135
4136        self.ctx
4137            .add_optimizer_rule(Arc::new(LookupJoinRewriteRule::new(tables)));
4138        self.ctx
4139            .add_optimizer_rule(Arc::new(PredicateSplitterRule::new(caps_registry)));
4140        self.ctx
4141            .add_optimizer_rule(Arc::new(LookupColumnPruningRule));
4142    }
4143
4144    /// Returns the frozen connector registry for factory lookup and deployment introspection.
4145    /// Custom factories must be registered through [`LaminarDbBuilder::register_connector`].
4146    #[must_use]
4147    pub fn connector_registry(&self) -> &laminar_connectors::registry::ConnectorRegistry {
4148        &self.connector_registry
4149    }
4150
4151    /// Register a custom scalar UDF. Called by the builder after construction.
4152    pub(crate) fn register_custom_udf(
4153        &mut self,
4154        udf: datafusion_expr::ScalarUDF,
4155    ) -> Result<(), DbError> {
4156        validate_custom_function_name("scalar UDF", udf.name(), udf.aliases())?;
4157        self.ctx.register_udf(udf.clone());
4158        self.custom_udfs.push(udf);
4159        Ok(())
4160    }
4161
4162    /// Register a custom aggregate UDF. Called by the builder after construction.
4163    pub(crate) fn register_custom_udaf(
4164        &mut self,
4165        udaf: datafusion_expr::AggregateUDF,
4166    ) -> Result<(), DbError> {
4167        validate_custom_function_name("aggregate UDF", udaf.name(), udaf.aliases())?;
4168        self.ctx.register_udaf(udaf.clone());
4169        self.custom_udafs.push(udaf);
4170        Ok(())
4171    }
4172
4173    pub(crate) fn register_custom_functions_into(&self, ctx: &SessionContext) {
4174        for udf in &self.custom_udfs {
4175            ctx.register_udf(udf.clone());
4176        }
4177        for udaf in &self.custom_udafs {
4178            ctx.register_udaf(udaf.clone());
4179        }
4180    }
4181
4182    /// Execute a SQL statement.
4183    ///
4184    /// # Errors
4185    ///
4186    /// Returns `DbError` if SQL parsing, planning, or execution fails.
4187    pub async fn execute(&self, sql: &str) -> Result<ExecuteResult, DbError> {
4188        if self.shutdown.load(std::sync::atomic::Ordering::Relaxed) {
4189            return Err(DbError::Shutdown);
4190        }
4191
4192        let stmts = sql_utils::split_statements(sql);
4193        if stmts.is_empty() {
4194            return Err(DbError::InvalidOperation("Empty SQL statement".into()));
4195        }
4196
4197        let mut last_result = None;
4198        for stmt_sql in &stmts {
4199            last_result = Some(self.execute_single(stmt_sql).await?);
4200        }
4201
4202        last_result.ok_or_else(|| DbError::InvalidOperation("Empty SQL statement".into()))
4203    }
4204
4205    /// Apply one startup catalog definition and seal it as the complete inventory.
4206    /// Prefer [`Self::execute_cluster_bootstrap_batch`] for server configuration.
4207    ///
4208    /// # Errors
4209    /// Returns an error when the definition is invalid or the catalog batch cannot be sealed.
4210    #[cfg(feature = "cluster")]
4211    pub async fn execute_cluster_bootstrap(&self, sql: &str) -> Result<ExecuteResult, DbError> {
4212        let entries = vec![sql.to_owned()];
4213        self.execute_cluster_bootstrap_batch(&entries)
4214            .await?
4215            .into_iter()
4216            .last()
4217            .ok_or_else(|| DbError::InvalidOperation("Empty SQL statement".into()))
4218    }
4219
4220    async fn execute_single(&self, sql: &str) -> Result<ExecuteResult, DbError> {
4221        let statements = parse_streaming_sql(sql)?;
4222        if statements.is_empty() {
4223            return Err(DbError::InvalidOperation("Empty SQL statement".into()));
4224        }
4225        let statement = &statements[0];
4226        if mutates_database(statement) {
4227            self.ensure_catalog_cleanup_unfenced("database mutation")?;
4228        }
4229        if is_topology_ddl(statement) {
4230            #[cfg(feature = "cluster")]
4231            if self.is_cluster_runtime()
4232                && DbState::load(&self.state) == DbState::Running
4233                && !catalog_manifest_replay_active()
4234                && !catalog_bootstrap_active()
4235            {
4236                // Admission plans its private catalog under the read lock. Do not take the
4237                // direct-mutation write lock or invoke the startup bootstrap exception here.
4238                return Box::pin(self.submit_cluster_topology_sql(sql, statement)).await;
4239            }
4240            futures::FutureExt::boxed(self.execute_schema_ddl(sql, statement)).await
4241        } else if reads_catalog(statement) {
4242            let _topology_read = self.topology_ddl_lock.read().await;
4243            if mutates_database(statement) {
4244                self.ensure_catalog_cleanup_unfenced("database mutation")?;
4245                #[cfg(feature = "cluster")]
4246                self.ensure_coordinated_recovery_mutation_unfenced("database mutation")?;
4247            }
4248            self.execute_parsed_single(sql, statement).await
4249        } else if mutates_database(statement) {
4250            let _topology_read = self.topology_ddl_lock.read().await;
4251            self.ensure_catalog_cleanup_unfenced("database mutation")?;
4252            #[cfg(feature = "cluster")]
4253            self.ensure_coordinated_recovery_mutation_unfenced("database mutation")?;
4254            self.execute_parsed_single(sql, statement).await
4255        } else {
4256            self.execute_parsed_single(sql, statement).await
4257        }
4258    }
4259
4260    #[cfg(feature = "cluster")]
4261    async fn execute_single_already_gated(&self, sql: &str) -> Result<ExecuteResult, DbError> {
4262        let statements = parse_streaming_sql(sql)?;
4263        if statements.len() != 1 || !is_topology_ddl(&statements[0]) {
4264            return Err(DbError::InvalidOperation(
4265                "catalog manifest entries must contain exactly one topology DDL statement".into(),
4266            ));
4267        }
4268        self.execute_parsed_single(sql, &statements[0]).await
4269    }
4270
4271    pub(crate) async fn execute_parsed_single(
4272        &self,
4273        sql: &str,
4274        statement: &StreamingStatement,
4275    ) -> Result<ExecuteResult, DbError> {
4276        #[cfg(feature = "cluster")]
4277        self.preflight_cluster_catalog_mutation(sql, statement)?;
4278
4279        let result = match statement {
4280            StreamingStatement::CreateSource(create) => {
4281                let result = self.handle_create_source(create).await?;
4282                if let ExecuteResult::Ddl(ref info) = result {
4283                    if info.applied {
4284                        self.connector_manager
4285                            .lock()
4286                            .store_ddl(&info.object_name, sql);
4287                    }
4288                }
4289                Ok(result)
4290            }
4291            StreamingStatement::CreateSink(create) => {
4292                let result = futures::FutureExt::boxed(self.handle_create_sink(create)).await?;
4293                if let ExecuteResult::Ddl(ref info) = result {
4294                    if info.applied {
4295                        self.connector_manager
4296                            .lock()
4297                            .store_ddl(&info.object_name, sql);
4298                    }
4299                }
4300                Ok(result)
4301            }
4302            StreamingStatement::CreateStream {
4303                name,
4304                query,
4305                emit_clause,
4306                if_not_exists,
4307                query_sql,
4308                retention_bytes,
4309                ..
4310            } => {
4311                let result = self
4312                    .handle_create_stream(
4313                        sql,
4314                        name,
4315                        query,
4316                        emit_clause.as_ref(),
4317                        *if_not_exists,
4318                        query_sql,
4319                        *retention_bytes,
4320                    )
4321                    .await?;
4322                if let ExecuteResult::Ddl(ref info) = result {
4323                    if info.applied {
4324                        self.connector_manager
4325                            .lock()
4326                            .store_ddl(&info.object_name, sql);
4327                    }
4328                }
4329                Ok(result)
4330            }
4331            StreamingStatement::CreateContinuousQuery { .. } => Err(DbError::InvalidOperation(
4332                "CREATE CONTINUOUS QUERY has no typed catalog/drop lifecycle; use CREATE STREAM"
4333                    .into(),
4334            )),
4335            StreamingStatement::DropLookupTable { name, if_exists } => {
4336                self.handle_drop_lookup_table(name, *if_exists)
4337            }
4338            StreamingStatement::CreateLookupTable(create) => {
4339                if create.or_replace {
4340                    return Err(DbError::InvalidOperation(
4341                        "CREATE OR REPLACE LOOKUP TABLE is not atomic; use DROP LOOKUP TABLE followed by CREATE LOOKUP TABLE"
4342                            .into(),
4343                    ));
4344                }
4345                let name = canonical_object_name(&create.name)?;
4346                let Some(reservation) = self.reserve_catalog_name(
4347                    &name,
4348                    CatalogObjectKind::LookupTable,
4349                    create.if_not_exists,
4350                )?
4351                else {
4352                    return Ok(ExecuteResult::Ddl(DdlInfo {
4353                        statement_type: "CREATE LOOKUP TABLE".into(),
4354                        object_name: name,
4355                        #[cfg(feature = "cluster")]
4356                        topology_operation: None,
4357                        applied: false,
4358                    }));
4359                };
4360                let result = self.handle_query(sql).await?;
4361                if let ExecuteResult::Ddl(ref info) = result {
4362                    if info.applied {
4363                        self.connector_manager
4364                            .lock()
4365                            .store_ddl(&info.object_name, sql);
4366                    }
4367                }
4368                reservation.commit();
4369                Ok(result)
4370            }
4371            StreamingStatement::Standard(stmt) => {
4372                if let sqlparser::ast::Statement::CreateTable(ct) = stmt.as_ref() {
4373                    let result = Box::pin(self.handle_create_table(ct)).await?;
4374                    if let ExecuteResult::Ddl(ref info) = result {
4375                        if info.applied {
4376                            self.connector_manager
4377                                .lock()
4378                                .store_ddl(&info.object_name, sql);
4379                        }
4380                    }
4381                    Ok(result)
4382                } else if let sqlparser::ast::Statement::Drop {
4383                    object_type: sqlparser::ast::ObjectType::Table,
4384                    names,
4385                    if_exists,
4386                    cascade,
4387                    ..
4388                } = stmt.as_ref()
4389                {
4390                    self.handle_drop_table(names, *if_exists, *cascade)
4391                } else if let sqlparser::ast::Statement::Set(set_stmt) = stmt.as_ref() {
4392                    self.handle_set(set_stmt)
4393                } else if matches!(stmt.as_ref(), sqlparser::ast::Statement::AlterTable { .. }) {
4394                    Err(DbError::InvalidOperation(
4395                        "ALTER TABLE is disabled until catalog/provider changes are transactional"
4396                            .into(),
4397                    ))
4398                } else if matches!(stmt.as_ref(), sqlparser::ast::Statement::Query(_)) {
4399                    if crate::sql_analysis::has_temporal_query(sql) {
4400                        Err(DbError::Unsupported(
4401                            "direct temporal queries require the managed vnode runtime; create the temporal stream while the pipeline is stopped"
4402                                .into(),
4403                        ))
4404                    } else {
4405                        self.handle_query(sql).await
4406                    }
4407                } else {
4408                    Err(DbError::InvalidOperation(format!(
4409                        "unsupported standard SQL statement; catalog mutation is not typed or transactional: {stmt}"
4410                    )))
4411                }
4412            }
4413            StreamingStatement::TemporalProbeQuery { .. } => Err(DbError::Unsupported(
4414                "TEMPORAL PROBE JOIN requires a managed stream created while the pipeline is stopped and cannot execute as an ordinary SQL query"
4415                    .into(),
4416            )),
4417            StreamingStatement::InsertInto {
4418                table_name,
4419                columns,
4420                values,
4421            } => self.handle_insert_into(table_name, columns, values).await,
4422            StreamingStatement::DropSource {
4423                name,
4424                if_exists,
4425                cascade,
4426            } => self.handle_drop_source(name, *if_exists, *cascade),
4427            StreamingStatement::DropSink {
4428                name,
4429                if_exists,
4430                cascade,
4431            } => self.handle_drop_sink(name, *if_exists, *cascade),
4432            StreamingStatement::DropStream {
4433                name,
4434                if_exists,
4435                cascade,
4436            } => self.handle_drop_stream(name, *if_exists, *cascade).await,
4437            StreamingStatement::DropMaterializedView {
4438                name,
4439                if_exists,
4440                cascade,
4441            } => {
4442                self.handle_drop_materialized_view(name, *if_exists, *cascade)
4443                    .await
4444            }
4445            StreamingStatement::Show(cmd) => {
4446                let batch = match cmd {
4447                    ShowCommand::Sources => self.build_show_sources(),
4448                    ShowCommand::Sinks => self.build_show_sinks(),
4449                    ShowCommand::Queries => self.build_show_queries(),
4450                    ShowCommand::MaterializedViews => self.build_show_materialized_views(),
4451                    ShowCommand::Streams => self.build_show_streams(),
4452                    ShowCommand::Tables => self.build_show_tables(),
4453                    ShowCommand::CheckpointStatus => self.build_show_checkpoint_status().await?,
4454                    ShowCommand::CreateSource { name } => {
4455                        self.build_show_create_source(&canonical_object_name(name)?)?
4456                    }
4457                    ShowCommand::CreateSink { name } => {
4458                        self.build_show_create_sink(&canonical_object_name(name)?)?
4459                    }
4460                };
4461                Ok(ExecuteResult::Metadata(batch))
4462            }
4463            StreamingStatement::Checkpoint => {
4464                let result = self.checkpoint().await?;
4465                Ok(ExecuteResult::Ddl(DdlInfo {
4466                    statement_type: "CHECKPOINT".to_string(),
4467                    object_name: format!("checkpoint_{}", result.checkpoint_id),
4468                    #[cfg(feature = "cluster")]
4469                    topology_operation: None,
4470                    applied: true,
4471                }))
4472            }
4473            StreamingStatement::RestoreCheckpoint { checkpoint_id } => {
4474                self.handle_restore_checkpoint(*checkpoint_id)
4475            }
4476            StreamingStatement::Describe { name, .. } => {
4477                let name_str = name.to_string();
4478                let batch = self.build_describe(&name_str)?;
4479                Ok(ExecuteResult::Metadata(batch))
4480            }
4481            StreamingStatement::Explain {
4482                statement, analyze, ..
4483            } => {
4484                if *analyze {
4485                    self.handle_explain_analyze(statement, sql).await
4486                } else {
4487                    self.handle_explain(statement).await
4488                }
4489            }
4490            StreamingStatement::CreateMaterializedView { .. } => {
4491                let result = self
4492                    .handle_create_materialized_view(sql, statement)
4493                    .await?;
4494                if let ExecuteResult::Ddl(ref info) = result {
4495                    if info.applied {
4496                        self.connector_manager
4497                            .lock()
4498                            .store_ddl(&info.object_name, sql);
4499                    }
4500                }
4501                Ok(result)
4502            }
4503            StreamingStatement::AlterSource { .. } => Err(DbError::InvalidOperation(
4504                "ALTER SOURCE is disabled until catalog/provider changes are transactional".into(),
4505            )),
4506            StreamingStatement::Subscribe(_) => Err(DbError::InvalidOperation(
4507                "SUBSCRIBE requires the pgwire endpoint, not HTTP /api/v1/sql".into(),
4508            )),
4509            StreamingStatement::DeclareCursorForSubscribe { .. } => Err(DbError::InvalidOperation(
4510                "DECLARE CURSOR FOR SUBSCRIBE requires the pgwire endpoint, not HTTP /api/v1/sql"
4511                    .into(),
4512            )),
4513        };
4514
4515        result
4516    }
4517
4518    async fn handle_insert_into(
4519        &self,
4520        table_name: &sqlparser::ast::ObjectName,
4521        columns: &[sqlparser::ast::Ident],
4522        values: &[Vec<sqlparser::ast::Expr>],
4523    ) -> Result<ExecuteResult, DbError> {
4524        let name = canonical_object_name(table_name)?;
4525        if !columns.is_empty() {
4526            return Err(DbError::InvalidOperation(
4527                "INSERT column lists are unsupported until projection and default-value semantics are implemented"
4528                    .into(),
4529            ));
4530        }
4531        if values.is_empty() {
4532            return Err(DbError::InvalidOperation(
4533                "INSERT requires at least one VALUES row".into(),
4534            ));
4535        }
4536
4537        if let Some(entry) = self.catalog.get_source(&name) {
4538            let batch = sql_utils::sql_values_to_record_batch(&entry.schema, values)?;
4539            entry
4540                .push_and_buffer(batch)
4541                .map_err(|e| DbError::InsertError(format!("Failed to push to source: {e}")))?;
4542            return Ok(ExecuteResult::RowsAffected(values.len() as u64));
4543        }
4544
4545        if !self.table_store.read().has_table(&name) {
4546            return Err(DbError::InvalidOperation(format!(
4547                "INSERT target '{name}' is not a typed mutable source or table"
4548            )));
4549        }
4550
4551        // Changelog enrichment must replay against the same dimension snapshot that produced the
4552        // original positive row. Linearize reference-table mutation with start/stop and keep it
4553        // offline until table versions are checkpoint-bound into the enrich operator.
4554        let _lifecycle = self.lifecycle_lock.lock().await;
4555        self.ensure_offline_topology_ddl_allowed(&format!("INSERT INTO TABLE '{name}'"))?;
4556        self.ensure_reference_table_insert_is_ephemeral(&name)?;
4557
4558        // Single lock scope avoids TOCTOU between has_table/schema/upsert.
4559        {
4560            let mut ts = self.table_store.write();
4561            if ts.has_table(&name) {
4562                let schema = ts
4563                    .table_schema(&name)
4564                    .ok_or_else(|| DbError::TableNotFound(name.clone()))?;
4565                let batch = sql_utils::sql_values_to_record_batch(&schema, values)?;
4566                ts.upsert(&name, &batch)?;
4567                drop(ts); // release before sync (which may also lock)
4568
4569                self.sync_table_to_datafusion(&name)?;
4570                return Ok(ExecuteResult::RowsAffected(values.len() as u64));
4571            }
4572        }
4573
4574        Err(DbError::TableNotFound(name))
4575    }
4576
4577    fn ensure_reference_table_insert_is_ephemeral(&self, table_name: &str) -> Result<(), DbError> {
4578        if self.is_cluster_runtime() {
4579            return Err(DbError::InvalidOperation(format!(
4580                "[LDB-6043] INSERT INTO TABLE '{table_name}' is not coordinated cluster state; load a versioned reference-table snapshot during deployment bootstrap"
4581            )));
4582        }
4583        if self.is_checkpoint_enabled() {
4584            return Err(DbError::InvalidOperation(format!(
4585                "[LDB-6043] INSERT INTO TABLE '{table_name}' is process-local and cannot be admitted into a recoverable deployment; load a versioned reference-table snapshot through its connector"
4586            )));
4587        }
4588        Ok(())
4589    }
4590
4591    #[allow(clippy::unused_self)] // will use self when implemented
4592    fn handle_restore_checkpoint(&self, _checkpoint_id: u64) -> Result<ExecuteResult, DbError> {
4593        Err(DbError::Unsupported(
4594            "RESTORE FROM CHECKPOINT is not yet implemented — \
4595             requires pipeline stop, state reload from manifest, \
4596             source offset seek, and pipeline restart"
4597                .to_string(),
4598        ))
4599    }
4600
4601    /// Return a session property value.
4602    #[must_use]
4603    pub fn get_session_property(&self, key: &str) -> Option<String> {
4604        self.session_properties
4605            .lock()
4606            .get(&key.to_lowercase())
4607            .cloned()
4608    }
4609
4610    /// Return all session properties.
4611    #[must_use]
4612    pub fn session_properties(&self) -> HashMap<String, String> {
4613        self.session_properties.lock().clone()
4614    }
4615
4616    /// Set a session property (keys are lowercased).
4617    pub fn set_session_property(&self, key: &str, value: &str) {
4618        self.session_properties
4619            .lock()
4620            .insert(key.to_lowercase(), value.to_string());
4621    }
4622
4623    /// Subscribe to a named stream or materialized view.
4624    ///
4625    /// Rows are matched to `T` by column name. A changelog view's frames carry `__weight`
4626    /// (see [`Self::open_subscription`]); a `T` without that field receives retracted rows as
4627    /// if they were inserted.
4628    ///
4629    /// # Errors
4630    ///
4631    /// Returns `DbError::StreamNotFound` if the object or its output schema is unresolved.
4632    pub async fn subscribe<T: crate::handle::FromBatch>(
4633        &self,
4634        name: &str,
4635    ) -> Result<crate::handle::TypedSubscription<T>, DbError> {
4636        let portal = self
4637            .open_subscription(name, None, crate::subscription::SubscribeStart::Tail)
4638            .await?;
4639        Ok(crate::handle::TypedSubscription::new(portal))
4640    }
4641
4642    /// Schema a `SUBSCRIBE` against `name` would emit.
4643    /// A stream is visible here only after its physical output schema has been
4644    /// resolved. Bare sources and unresolved streams are not subscribable.
4645    #[must_use]
4646    pub fn lookup_subscription_schema(&self, name: &str) -> Option<arrow_schema::SchemaRef> {
4647        if crate::sql_analysis::stage_owner(name).is_some() {
4648            return None;
4649        }
4650        if let Some(schema) = self.mv_store.read().subscription_schema(name) {
4651            return Some(schema);
4652        }
4653        if let Some(schema) = self.stream_schemas.read().get(name).cloned() {
4654            return Some(schema);
4655        }
4656        None
4657    }
4658
4659    /// Open a SUBSCRIBE portal against a named MV or resolved stream. A bare
4660    /// SOURCE is not subscribable (surfaced as `StreamNotFound`).
4661    ///
4662    /// A materialized view's portal first delivers the view's rows, then each change to them.
4663    /// A changelog view's rows carry an Int64 `__weight` column: `+1` in the snapshot, and in
4664    /// a change the amount by which the change altered the row's count (a replaced row is
4665    /// retracted with `-1`). An append view delivers plain rows; rows leaving its retention are
4666    /// not changes. Outside a cluster a view subscription starts at the latest cycle and admits
4667    /// only [`SubscribeStart::Tail`](crate::subscription::SubscribeStart::Tail). In a cluster it
4668    /// reads committed checkpoints: the view at the newest one or at
4669    /// [`SubscribeStart::AsOfEpoch`](crate::subscription::SubscribeStart::AsOfEpoch), then each
4670    /// later checkpoint's change, each followed by a progress frame.
4671    ///
4672    /// # Errors
4673    /// `StreamNotFound` for unknown `name`; `Subscription` for unsupported cluster plans or
4674    /// durable replay failures; `Pipeline` for subscriber-cap or filter-compile failures;
4675    /// Local epoch and sequence replay failures return the corresponding structured
4676    /// `SubscriptionEpochNotCommitted`, `SubscriptionReplayPruned`,
4677    /// `SubscriptionSequenceNotPublished`, or `SubscriptionSequencePruned` error.
4678    pub async fn open_subscription(
4679        &self,
4680        name: &str,
4681        filter_sql: Option<&str>,
4682        start: crate::subscription::SubscribeStart,
4683    ) -> Result<crate::subscription::SubscriptionPortal, DbError> {
4684        // Serialize schema resolution, filter compilation, and cursor attachment
4685        // with topology DDL. Otherwise DROP/recreate can leave a portal attached
4686        // to an orphaned log with the previous object's schema.
4687        let _topology = self.topology_ddl_lock.read().await;
4688
4689        let schema = self
4690            .lookup_subscription_schema(name)
4691            .ok_or_else(|| DbError::StreamNotFound(name.to_string()))?;
4692
4693        let filter = match filter_sql {
4694            None => None,
4695            Some(sql) => Some(crate::filter_compile::compile(&self.ctx, sql, &schema).await?),
4696        };
4697
4698        #[cfg(feature = "cluster")]
4699        if self.is_cluster_runtime() {
4700            return self
4701                .open_cluster_subscription(name, schema, filter, start)
4702                .await;
4703        }
4704
4705        if self.mv_store.read().has_mv(name) {
4706            return self
4707                .open_local_view_subscription(name, schema, filter, start)
4708                .await;
4709        }
4710        self.open_local_stream_subscription(name, schema, filter, start)
4711    }
4712
4713    #[allow(clippy::too_many_lines)]
4714    pub(crate) async fn handle_query(&self, sql: &str) -> Result<ExecuteResult, DbError> {
4715        let plan = {
4716            let statements = parse_streaming_sql(sql)?;
4717            if statements.is_empty() {
4718                return Err(DbError::InvalidOperation("Empty SQL statement".into()));
4719            }
4720            match &statements[0] {
4721                StreamingStatement::CreateContinuousQuery { .. } => {
4722                    return Err(DbError::InvalidOperation(
4723                        "CREATE CONTINUOUS QUERY has no typed catalog/drop lifecycle; use CREATE STREAM"
4724                            .into(),
4725                    ));
4726                }
4727                StreamingStatement::CreateLookupTable(statement) => {
4728                    self.ensure_offline_topology_ddl_allowed("CREATE LOOKUP TABLE")?;
4729                    let name = canonical_object_name(&statement.name)?;
4730                    if self.catalog_namespace.lock().get(&name)
4731                        != Some(&CatalogObjectKind::LookupTable)
4732                    {
4733                        return Err(DbError::InvalidOperation(
4734                            "CREATE LOOKUP TABLE must pass typed namespace admission".into(),
4735                        ));
4736                    }
4737                }
4738                StreamingStatement::DropLookupTable { name, if_exists } => {
4739                    return self.handle_drop_lookup_table(name, *if_exists);
4740                }
4741                // An ad-hoc query reads every table as a snapshot, so the streaming planner's
4742                // join and window rules, which govern named streaming queries, do not apply.
4743                StreamingStatement::Standard(statement)
4744                    if matches!(**statement, sqlparser::ast::Statement::Query(_)) =>
4745                {
4746                    let sql_str = statement.to_string();
4747                    let df = crate::mv_store::plan_statement(&self.ctx, &self.mv_store, &sql_str);
4748                    let stream = df.await?.execute_stream().await?;
4749                    return Ok(self.bridge_query_stream(sql, stream));
4750                }
4751                _ => {}
4752            }
4753            let mut planner = self.planner.lock();
4754            let plan = match (
4755                &statements[0],
4756                crate::ddl::schema_resolution::supplied_binding(),
4757            ) {
4758                (StreamingStatement::CreateLookupTable(create), Some(binding)) => {
4759                    planner.plan_lookup_table_with_schema(create, Arc::new(binding.logical))
4760                }
4761                _ => planner.plan(&statements[0]),
4762            };
4763            plan.map_err(laminar_sql::Error::from)?
4764        };
4765
4766        match plan {
4767            laminar_sql::planner::StreamingPlan::RegisterSource(info) => {
4768                Ok(ExecuteResult::Ddl(DdlInfo {
4769                    statement_type: "DDL".to_string(),
4770                    object_name: info.name,
4771                    #[cfg(feature = "cluster")]
4772                    topology_operation: None,
4773                    applied: true,
4774                }))
4775            }
4776            laminar_sql::planner::StreamingPlan::RegisterSink(info) => {
4777                Ok(ExecuteResult::Ddl(DdlInfo {
4778                    statement_type: "DDL".to_string(),
4779                    object_name: info.name,
4780                    #[cfg(feature = "cluster")]
4781                    topology_operation: None,
4782                    applied: true,
4783                }))
4784            }
4785            laminar_sql::planner::StreamingPlan::Query(query_plan) => {
4786                if query_plan.join_config.as_ref().is_some_and(|joins| {
4787                    joins.iter().any(|join| {
4788                        matches!(
4789                            join,
4790                            laminar_sql::translator::JoinOperatorConfig::Temporal(_)
4791                        )
4792                    })
4793                }) {
4794                    return Err(DbError::Unsupported(
4795                        "temporal queries require the managed vnode runtime and cannot execute through DataFusion's ordinary join path"
4796                            .into(),
4797                    ));
4798                }
4799                let plan_sql = query_plan.statement.to_string();
4800                let df = crate::mv_store::plan_statement(&self.ctx, &self.mv_store, &plan_sql);
4801                let stream = df.await?.execute_stream().await?;
4802                Ok(self.bridge_query_stream(sql, stream))
4803            }
4804            laminar_sql::planner::StreamingPlan::Standard(stmt) => {
4805                let sql_str = stmt.to_string();
4806                let df = crate::mv_store::plan_statement(&self.ctx, &self.mv_store, &sql_str);
4807                let stream = df.await?.execute_stream().await?;
4808                Ok(self.bridge_query_stream(sql, stream))
4809            }
4810            laminar_sql::planner::StreamingPlan::RegisterLookupTable(info) => {
4811                self.handle_register_lookup_table(info)
4812            }
4813            laminar_sql::planner::StreamingPlan::DropLookupTable { .. } => Err(
4814                DbError::InvalidOperation("lookup drop bypassed typed catalog admission".into()),
4815            ),
4816        }
4817    }
4818
4819    fn bridge_query_stream(
4820        &self,
4821        sql: &str,
4822        stream: datafusion::physical_plan::SendableRecordBatchStream,
4823    ) -> ExecuteResult {
4824        let query_id = self.catalog.register_query(sql);
4825        let schema = stream.schema();
4826        let cancel_token = tokio_util::sync::CancellationToken::new();
4827        let catalog = Arc::clone(&self.catalog);
4828        let rows = crate::query_rows::QueryRows::spawn(stream, cancel_token.clone(), move || {
4829            catalog.deactivate_query(query_id);
4830        });
4831        ExecuteResult::Query(QueryHandle {
4832            id: query_id,
4833            schema,
4834            sql: sql.to_string(),
4835            rows: Some(rows),
4836            active: true,
4837            cancel_token,
4838        })
4839    }
4840    /// Get a typed handle for pushing data to a registered source.
4841    ///
4842    /// # Errors
4843    ///
4844    /// Returns `DbError::SourceNotFound` if the source is not registered.
4845    /// Returns `DbError::SchemaMismatch` if the Rust type's schema doesn't match.
4846    pub fn source<T: laminar_core::streaming::Record>(
4847        &self,
4848        name: &str,
4849    ) -> Result<SourceHandle<T>, DbError> {
4850        let entry = self
4851            .catalog
4852            .get_source(name)
4853            .ok_or_else(|| DbError::SourceNotFound(name.to_string()))?;
4854        SourceHandle::new(entry)
4855    }
4856
4857    /// Get an untyped source handle for pushing `RecordBatch` data.
4858    ///
4859    /// # Errors
4860    ///
4861    /// Returns `DbError::SourceNotFound` if the source is not registered.
4862    pub fn source_untyped(&self, name: &str) -> Result<UntypedSourceHandle, DbError> {
4863        let entry = self
4864            .catalog
4865            .get_source(name)
4866            .ok_or_else(|| DbError::SourceNotFound(name.to_string()))?;
4867        Ok(UntypedSourceHandle::new(entry))
4868    }
4869
4870    /// List registered sources.
4871    pub fn sources(&self) -> Vec<SourceInfo> {
4872        let names = self.catalog.list_sources();
4873        names
4874            .into_iter()
4875            .filter_map(|name| {
4876                self.catalog.get_source(&name).map(|e| SourceInfo {
4877                    name: e.name.clone(),
4878                    schema: e.schema.clone(),
4879                    watermark_column: e.watermark_column.clone(),
4880                })
4881            })
4882            .collect()
4883    }
4884
4885    /// List registered sinks.
4886    pub fn sinks(&self) -> Vec<SinkInfo> {
4887        self.catalog
4888            .list_sinks()
4889            .into_iter()
4890            .map(|name| SinkInfo { name })
4891            .collect()
4892    }
4893
4894    /// List registered materialized views.
4895    pub fn materialized_views(&self) -> Vec<crate::handle::MaterializedViewInfo> {
4896        let registry = self.mv_registry.lock();
4897        registry
4898            .views()
4899            .map(crate::handle::MaterializedViewInfo::from)
4900            .collect()
4901    }
4902
4903    /// List registered streams.
4904    pub fn streams(&self) -> Vec<crate::handle::StreamInfo> {
4905        let mgr = self.connector_manager.lock();
4906        let mut streams = mgr
4907            .streams()
4908            .iter()
4909            .filter(|(name, _)| crate::sql_analysis::stage_owner(name).is_none())
4910            .map(|(name, reg)| crate::handle::StreamInfo {
4911                name: name.clone(),
4912                sql: Some(reg.query_sql.clone()),
4913            })
4914            .collect::<Vec<_>>();
4915        streams.extend(mgr.process_functions().values().map(|registration| {
4916            crate::handle::StreamInfo {
4917                name: registration.output_name.clone(),
4918                sql: None,
4919            }
4920        }));
4921        streams
4922    }
4923
4924    /// Build the pipeline topology graph (nodes + edges) from registered sources, streams, and sinks.
4925    pub fn pipeline_topology(&self) -> crate::handle::PipelineTopology {
4926        use crate::handle::{PipelineEdge, PipelineNode, PipelineNodeType};
4927
4928        let mut nodes = Vec::new();
4929        let mut edges = Vec::new();
4930
4931        let source_names = self.catalog.list_sources();
4932
4933        for name in &source_names {
4934            let schema = self.catalog.get_source(name).map(|e| e.schema.clone());
4935            nodes.push(PipelineNode {
4936                name: name.clone(),
4937                node_type: PipelineNodeType::Source,
4938                schema,
4939                sql: None,
4940            });
4941        }
4942
4943        let mgr = self.connector_manager.lock();
4944        let stream_names: Vec<String> = mgr.streams().keys().cloned().collect();
4945        for (name, reg) in mgr.streams() {
4946            nodes.push(PipelineNode {
4947                name: name.clone(),
4948                node_type: PipelineNodeType::Stream,
4949                schema: None,
4950                sql: Some(reg.query_sql.clone()),
4951            });
4952
4953            // Lightweight heuristic: substring match instead of a full parse.
4954            let sql_upper = reg.query_sql.to_uppercase();
4955            for src in &source_names {
4956                if sql_upper.contains(&src.to_uppercase()) {
4957                    edges.push(PipelineEdge {
4958                        from: src.clone(),
4959                        to: name.clone(),
4960                    });
4961                }
4962            }
4963            for other in &stream_names {
4964                if other != name && sql_upper.contains(&other.to_uppercase()) {
4965                    edges.push(PipelineEdge {
4966                        from: other.clone(),
4967                        to: name.clone(),
4968                    });
4969                }
4970            }
4971        }
4972
4973        for (name, reg) in mgr.sinks() {
4974            nodes.push(PipelineNode {
4975                name: name.clone(),
4976                node_type: PipelineNodeType::Sink,
4977                schema: None,
4978                sql: None,
4979            });
4980
4981            if !reg.input.is_empty() {
4982                edges.push(PipelineEdge {
4983                    from: reg.input.clone(),
4984                    to: name.clone(),
4985                });
4986            }
4987        }
4988
4989        let cm_sink_names: std::collections::HashSet<&String> = mgr.sinks().keys().collect();
4990        for name in self.catalog.list_sinks() {
4991            if !cm_sink_names.contains(&name) {
4992                if let Some(input) = self.catalog.get_sink_input(&name) {
4993                    nodes.push(PipelineNode {
4994                        name: name.clone(),
4995                        node_type: PipelineNodeType::Sink,
4996                        schema: None,
4997                        sql: None,
4998                    });
4999                    if !input.is_empty() {
5000                        edges.push(PipelineEdge {
5001                            from: input,
5002                            to: name,
5003                        });
5004                    }
5005                }
5006            }
5007        }
5008
5009        drop(mgr);
5010
5011        crate::handle::PipelineTopology { nodes, edges }
5012    }
5013
5014    /// List active queries.
5015    pub fn queries(&self) -> Vec<QueryInfo> {
5016        self.catalog
5017            .list_queries()
5018            .into_iter()
5019            .map(|(id, sql, active)| QueryInfo { id, sql, active })
5020            .collect()
5021    }
5022
5023    /// Returns whether streaming checkpointing is enabled.
5024    #[must_use]
5025    pub fn is_checkpoint_enabled(&self) -> bool {
5026        self.config.checkpoint.is_some()
5027    }
5028
5029    /// Stable node identity used by checkpoint metadata.
5030    ///
5031    /// `None` identifies the local runtime; its checkpoint node id is
5032    /// [`laminar_core::state::LOCAL_NODE_ID`]. Cluster runtimes use the controller's numeric
5033    /// instance id.
5034    pub(crate) fn checkpoint_participant(&self) -> Option<u64> {
5035        checkpoint_participant_for_runtime(self)
5036    }
5037
5038    /// Stable logical partition count used by checkpoint and routing identity.
5039    /// Uses the exact registry topology, or the common deployment default when no registry is
5040    /// installed.
5041    pub(crate) fn checkpoint_key_groups(&self) -> laminar_core::state::KeyGroupCount {
5042        self.vnode_registry.lock().as_ref().map_or(
5043            laminar_core::state::DEFAULT_KEY_GROUP_COUNT,
5044            |registry| {
5045                laminar_core::state::KeyGroupCount::try_from(registry.vnode_count())
5046                    .expect("builder validated the vnode registry key-group count")
5047            },
5048        )
5049    }
5050
5051    pub(crate) fn checkpoint_object_store(
5052        &self,
5053    ) -> Result<Option<Arc<dyn object_store::ObjectStore>>, DbError> {
5054        let Some(cp_config) = self.config.checkpoint.as_ref() else {
5055            return Ok(None);
5056        };
5057        #[cfg(feature = "cluster")]
5058        if let Some(object_store) = self.cluster_checkpoint_object_store() {
5059            return Ok(Some(object_store));
5060        }
5061        let object_store: Arc<dyn object_store::ObjectStore> = if let Some(ref url) =
5062            self.config.object_store_url
5063        {
5064            let owner = self.checkpoint_namespace_lock.lock().clone();
5065            match owner {
5066                Some(owner)
5067                    if laminar_connectors::storage::StorageProvider::detect_uri(url)
5068                        == Some(laminar_connectors::storage::StorageProvider::Local) =>
5069                {
5070                    let root = laminar_core::checkpoint::object_store_builder::file_url_path(url)
5071                        .map_err(|error| DbError::Checkpoint(error.to_string()))?;
5072                    laminar_core::checkpoint::object_store_builder::owned_durable_local_object_store(root, &owner)
5073                        .map_err(|error| DbError::Checkpoint(format!("checkpoint object store: {error}")))?
5074                }
5075                _ => laminar_core::checkpoint::object_store_builder::build_object_store(
5076                    url,
5077                    &self.config.object_store_options,
5078                )
5079                .map_err(|error| {
5080                    DbError::Checkpoint(format!("checkpoint object store: {error}"))
5081                })?,
5082            }
5083        } else {
5084            let data_dir = cp_config
5085                .data_dir
5086                .clone()
5087                .or_else(|| self.config.storage_dir.clone())
5088                .unwrap_or_else(|| std::path::PathBuf::from("./data"));
5089            match self.checkpoint_namespace_lock.lock().clone() {
5090                Some(owner) => laminar_core::checkpoint::object_store_builder::owned_durable_local_object_store(data_dir, &owner),
5091                None => laminar_core::checkpoint::object_store_builder::durable_local_object_store(data_dir),
5092            }
5093                .map_err(|error| DbError::Checkpoint(format!("checkpoint object store: {error}")))?
5094        };
5095        Ok(Some(object_store))
5096    }
5097
5098    /// Trigger a checkpoint that persists source offsets, sink positions, and operator state.
5099    ///
5100    /// # Errors
5101    ///
5102    /// Returns `DbError::Checkpoint` if checkpointing is disabled, no live
5103    /// manual-checkpoint route exists, cluster leadership cannot be resolved,
5104    /// or the checkpoint fails.
5105    pub async fn checkpoint(
5106        &self,
5107    ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
5108        let timeout = self
5109            .config
5110            .checkpoint
5111            .as_ref()
5112            .and_then(|checkpoint| checkpoint.timeout_ms)
5113            .map_or(
5114                crate::checkpoint_coordinator::CheckpointConfig::default().checkpoint_timeout,
5115                std::time::Duration::from_millis,
5116            );
5117        self.checkpoint_with_timeout(timeout).await
5118    }
5119
5120    /// Trigger one manual checkpoint within a caller-provided relative budget.
5121    ///
5122    /// The budget is capped by the configured checkpoint timeout. It bounds admission and attempt
5123    /// work; once an exact attempt is reserved, this call waits for terminal cleanup even if that
5124    /// requires the coordinator's private cleanup budget.
5125    ///
5126    /// # Errors
5127    ///
5128    /// Returns `DbError::Checkpoint` under the same conditions as [`Self::checkpoint`], or when
5129    /// the caller-provided budget cannot be represented as an absolute deadline.
5130    pub async fn checkpoint_with_timeout(
5131        &self,
5132        timeout: std::time::Duration,
5133    ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
5134        let deadline = self.manual_checkpoint_deadline(timeout)?;
5135        self.checkpoint_until(deadline).await
5136    }
5137
5138    /// Execute a forwarded manual checkpoint directly, without another network forwarding hop.
5139    ///
5140    /// This is the server-side entry point for a request already routed by a follower. Refusing a
5141    /// second hop prevents stale, inconsistent leader views from forming an HTTP forwarding loop.
5142    ///
5143    /// # Errors
5144    ///
5145    /// Returns `DbError::Checkpoint` under the same conditions as [`Self::checkpoint`], when the
5146    /// caller-provided budget cannot be represented as an absolute deadline, or when this cluster
5147    /// node is not the leader that can admit the forwarded request.
5148    #[doc(hidden)]
5149    pub async fn checkpoint_forwarded_with_timeout(
5150        &self,
5151        timeout: std::time::Duration,
5152    ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
5153        let deadline = self.manual_checkpoint_deadline(timeout)?;
5154        self.checkpoint_until_inner(deadline, false).await
5155    }
5156
5157    fn manual_checkpoint_deadline(
5158        &self,
5159        timeout: std::time::Duration,
5160    ) -> Result<tokio::time::Instant, DbError> {
5161        let configured_timeout = self
5162            .config
5163            .checkpoint
5164            .as_ref()
5165            .and_then(|checkpoint| checkpoint.timeout_ms)
5166            .map_or(
5167                crate::checkpoint_coordinator::CheckpointConfig::default().checkpoint_timeout,
5168                std::time::Duration::from_millis,
5169            );
5170        let deadline = tokio::time::Instant::now()
5171            .checked_add(timeout.min(configured_timeout))
5172            .ok_or_else(|| DbError::Checkpoint("manual checkpoint deadline overflowed".into()))?;
5173        Ok(deadline)
5174    }
5175
5176    /// Trigger one manual checkpoint under a caller-selected absolute deadline.
5177    ///
5178    /// The deadline bounds admission and every exact-attempt phase. After reservation this method
5179    /// deliberately waits past the deadline for the coordinator's terminal cleanup reply, so a
5180    /// caller cannot begin a conflicting lifecycle transition while the attempt remains live.
5181    pub(crate) async fn checkpoint_until(
5182        &self,
5183        deadline: tokio::time::Instant,
5184    ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
5185        self.checkpoint_until_inner(deadline, true).await
5186    }
5187
5188    async fn checkpoint_until_inner(
5189        &self,
5190        deadline: tokio::time::Instant,
5191        allow_forwarding: bool,
5192    ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
5193        #[cfg(not(feature = "cluster"))]
5194        let _ = allow_forwarding;
5195        self.ensure_catalog_cleanup_unfenced("manual checkpoint")?;
5196        #[cfg(feature = "cluster")]
5197        self.ensure_coordinated_recovery_mutation_unfenced("manual checkpoint")?;
5198        if self.config.checkpoint.is_none() {
5199            return Err(DbError::Checkpoint(
5200                "checkpointing is not enabled".to_string(),
5201            ));
5202        }
5203        if DbState::load(&self.state) != DbState::Running {
5204            return Err(DbError::Checkpoint(
5205                "manual checkpoint coordinator is not running — a fully running pipeline is required"
5206                    .into(),
5207            ));
5208        }
5209
5210        #[cfg(feature = "cluster")]
5211        if self.is_cluster_runtime() {
5212            let leader = self.cluster_leader_http_address().map_err(|reason| {
5213                DbError::Checkpoint(format!("cannot route checkpoint: {reason}"))
5214            })?;
5215            match leader {
5216                None => {}
5217                Some(_) if !allow_forwarding => {
5218                    return Err(DbError::Checkpoint(
5219                        "forwarded checkpoint reached a node that is no longer the cluster leader"
5220                            .into(),
5221                    ))
5222                }
5223                Some(leader_rpc) => {
5224                    tracing::info!(
5225                        "Forwarding checkpoint request to leader node at HTTP address {}",
5226                        leader_rpc
5227                    );
5228                    return self
5229                        .forward_checkpoint_to_leader(&leader_rpc, deadline)
5230                        .await;
5231                }
5232            }
5233        }
5234
5235        // Route through the live streaming coordinator so the manual checkpoint observes the
5236        // same source, operator, and sink cut as a periodic barrier checkpoint.
5237        let tx = self.force_ckpt_tx.lock().clone().ok_or_else(|| {
5238            DbError::Checkpoint(
5239                "manual checkpoint coordinator is not running — call start() first".into(),
5240            )
5241        })?;
5242        let (reply_tx, mut reply_rx) = crossfire::oneshot::oneshot();
5243        let (reservation_claim, mut reservation_wait) = ForceCheckpointReservationClaim::new();
5244        let request = ForceCheckpointRequest {
5245            reply: reply_tx,
5246            reservation_claim: Some(reservation_claim),
5247            deadline,
5248        };
5249        tokio::time::timeout_at(deadline, tx.send(request))
5250            .await
5251            .map_err(|_| {
5252                DbError::Checkpoint(
5253                    "manual checkpoint deadline expired before coordinator admission".into(),
5254                )
5255            })?
5256            .map_err(|_| {
5257                DbError::Checkpoint(
5258                    "manual checkpoint receiver closed — engine may be shutting down".into(),
5259                )
5260            })?;
5261        // Admission into the bounded channel is not exact-attempt ownership: the coordinator can
5262        // be alive but unable to cycle while a prior durable tail is still retiring. Bound that
5263        // pre-reservation interval. Once the coordinator's atomic claim wins (observed either by
5264        // its wake or by losing the deadline CAS), deliberately wait without the caller deadline
5265        // so a lifecycle transition cannot overlap durable reservation or terminal cleanup.
5266        let mut reservation_wake_open = true;
5267        loop {
5268            tokio::select! {
5269                biased;
5270                result = &mut reply_rx => {
5271                    return result.map_err(|_| {
5272                        DbError::Checkpoint(
5273                            "manual checkpoint ended without a terminal reply".into(),
5274                        )
5275                    })?;
5276                }
5277                reservation = reservation_wait.wait(), if reservation_wake_open => {
5278                    if reservation.is_ok() {
5279                        break;
5280                    }
5281                    // A pre-reservation rejection drops the wake after sending its terminal reply.
5282                    // Keep the reply/deadline race live rather than treating channel closure as
5283                    // exact ownership.
5284                    reservation_wake_open = false;
5285                }
5286                () = tokio::time::sleep_until(deadline) => {
5287                    if reservation_wait.cancel_before_reservation() {
5288                        return Err(DbError::Checkpoint(
5289                            "manual checkpoint deadline expired before exact attempt reservation"
5290                                .into(),
5291                        ));
5292                    }
5293                    if reservation_wait.is_claimed() {
5294                        break;
5295                    }
5296                    return Err(DbError::Checkpoint(
5297                        "manual checkpoint deadline expired before exact attempt reservation"
5298                            .into(),
5299                    ));
5300                }
5301            }
5302        }
5303        let result = reply_rx.await.map_err(|_| {
5304            DbError::Checkpoint("manual checkpoint ended without a terminal reply".into())
5305        })?;
5306
5307        result
5308    }
5309
5310    #[cfg(feature = "cluster")]
5311    async fn forward_checkpoint_to_leader(
5312        &self,
5313        addr: &str,
5314        deadline: tokio::time::Instant,
5315    ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
5316        #[derive(serde::Deserialize)]
5317        struct ForwardedCheckpointResponse {
5318            success: bool,
5319            checkpoint_id: u64,
5320            epoch: u64,
5321            duration_ms: u64,
5322            error: Option<String>,
5323            failure_disposition:
5324                Option<crate::checkpoint_coordinator::CheckpointFailureDisposition>,
5325        }
5326
5327        let remaining = deadline
5328            .checked_duration_since(tokio::time::Instant::now())
5329            .filter(|remaining| !remaining.is_zero())
5330            .ok_or_else(|| {
5331                DbError::Checkpoint(
5332                    "manual checkpoint deadline expired before leader forwarding".into(),
5333                )
5334            })?;
5335        let budget_nanos = u64::try_from(remaining.as_nanos()).unwrap_or(u64::MAX);
5336        // Bound connection establishment by the caller's remaining admission window. Once the
5337        // leader accepts the request there is deliberately no client-side total timeout: the
5338        // leader may need its private cleanup budget to make a reserved attempt terminal.
5339        let client = reqwest::Client::builder()
5340            .connect_timeout(remaining)
5341            .build()
5342            .map_err(|error| {
5343                DbError::Checkpoint(format!(
5344                    "failed to build checkpoint forwarding client: {error}"
5345                ))
5346            })?;
5347        let mut req = client
5348            .post(format!("http://{addr}/api/v1/checkpoint"))
5349            .header(
5350                "x-laminar-checkpoint-budget-nanos",
5351                budget_nanos.to_string(),
5352            );
5353        if let Some(token) = &self.config.http_auth_token {
5354            req = req.bearer_auth(token.expose());
5355        }
5356        let resp = req.send().await.map_err(|e| {
5357            DbError::Checkpoint(format!(
5358                "failed to forward checkpoint to leader at {addr}: {e}"
5359            ))
5360        })?;
5361
5362        let status = resp.status();
5363        let body = resp.text().await.map_err(|e| {
5364            DbError::Checkpoint(format!("failed to read leader checkpoint response: {e}"))
5365        })?;
5366
5367        // The leader returns a `CheckpointResponse` body even on failure (HTTP 500 +
5368        // `success: false`), so parse it to relay structured failure. A non-`CheckpointResponse`
5369        // body (e.g. a 401 payload) is an auth/transport failure — surface status and body.
5370        match serde_json::from_str::<ForwardedCheckpointResponse>(&body) {
5371            Ok(response) => Ok(crate::checkpoint_coordinator::CheckpointResult {
5372                success: response.success,
5373                checkpoint_id: response.checkpoint_id,
5374                epoch: response.epoch,
5375                duration: std::time::Duration::from_millis(response.duration_ms),
5376                error: response.error,
5377                failure_disposition: response.failure_disposition,
5378            }),
5379            Err(_) => Err(DbError::Checkpoint(format!(
5380                "leader rejected checkpoint ({status}): {body}"
5381            ))),
5382        }
5383    }
5384
5385    /// Returns checkpoint performance statistics, or `None` if the coordinator is not initialized.
5386    pub async fn checkpoint_stats(&self) -> Option<crate::checkpoint_coordinator::CheckpointStats> {
5387        let guard = self.coordinator.lock().await;
5388        guard
5389            .as_ref()
5390            .map(crate::checkpoint_coordinator::CheckpointCoordinator::stats)
5391    }
5392}
5393
5394impl std::fmt::Debug for LaminarDB {
5395    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5396        f.debug_struct("LaminarDB")
5397            .field("runtime_mode", &self.runtime_mode)
5398            .field("sources", &self.catalog.list_sources().len())
5399            .field("sinks", &self.catalog.list_sinks().len())
5400            .field("materialized_views", &self.mv_registry.lock().len())
5401            .field("checkpoint_enabled", &self.is_checkpoint_enabled())
5402            .field("shutdown", &self.is_closed())
5403            .finish_non_exhaustive()
5404    }
5405}
5406
5407/// `DefaultPhysicalPlanner` with lookup-join extension support.
5408struct LookupQueryPlanner {
5409    extension_planner: Arc<dyn datafusion::physical_planner::ExtensionPlanner + Send + Sync>,
5410}
5411
5412impl std::fmt::Debug for LookupQueryPlanner {
5413    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5414        f.debug_struct("LookupQueryPlanner").finish_non_exhaustive()
5415    }
5416}
5417
5418#[async_trait::async_trait]
5419impl datafusion::execution::context::QueryPlanner for LookupQueryPlanner {
5420    async fn create_physical_plan(
5421        &self,
5422        logical_plan: &datafusion::logical_expr::LogicalPlan,
5423        session_state: &datafusion::execution::SessionState,
5424    ) -> datafusion_common::Result<Arc<dyn datafusion::physical_plan::ExecutionPlan>> {
5425        use datafusion::physical_planner::PhysicalPlanner;
5426        let planner =
5427            datafusion::physical_planner::DefaultPhysicalPlanner::with_extension_planners(vec![
5428                Arc::clone(&self.extension_planner),
5429            ]);
5430        planner
5431            .create_physical_plan(logical_plan, session_state)
5432            .await
5433    }
5434}
5435
5436#[cfg(test)]
5437mod tests;