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