Skip to main content

laminar_db/
db.rs

1//! The main `LaminarDB` database facade.
2#![allow(clippy::disallowed_types)] // cold path
3
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use arrow::array::{RecordBatch, StringArray};
8use arrow::datatypes::{DataType, Field, Schema};
9use datafusion::prelude::SessionContext;
10use laminar_core::catalog::CatalogObjectKind;
11use laminar_core::streaming;
12use laminar_sql::parser::{parse_streaming_sql, ShowCommand, StreamingStatement};
13use laminar_sql::planner::StreamingPlanner;
14use laminar_sql::register_streaming_functions;
15use laminar_sql::translator::{AsofJoinTranslatorConfig, JoinOperatorConfig};
16
17use crate::builder::LaminarDbBuilder;
18use crate::catalog::SourceCatalog;
19use crate::config::LaminarConfig;
20use crate::error::DbError;
21use crate::handle::{
22    DdlInfo, ExecuteResult, QueryHandle, QueryInfo, SinkInfo, SourceHandle, SourceInfo,
23    UntypedSourceHandle,
24};
25use crate::pipeline::ControlMsg;
26use crate::sql_utils;
27
28/// Cloneable async sender for the live-DDL control channel.
29pub(crate) type ControlMsgTx = crossfire::MAsyncTx<crossfire::mpsc::Array<ControlMsg>>;
30
31/// Lifecycle state of a [`LaminarDB`] instance.
32#[repr(u8)]
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub(crate) enum DbState {
35    Created = 0,
36    Starting = 1,
37    Running = 2,
38    ShuttingDown = 3,
39    Stopped = 4,
40    /// Runtime fault. Recoverable faults may restart from the catalog; terminal resource
41    /// exhaustion remains faulted for operator intervention. Reason in `LaminarDB::last_fault`.
42    Faulted = 5,
43}
44
45/// Deployment scope fixed when the database is constructed.
46///
47/// Cluster support being compiled into the process does not select cluster semantics. The
48/// builder resolves this value once and separately verifies that cluster-only handles agree with
49/// it, so admission cannot change because a controller slot happens to be populated later.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub(crate) enum RuntimeMode {
52    Local,
53    Cluster,
54}
55
56impl RuntimeMode {
57    pub(crate) const fn is_cluster(self) -> bool {
58        matches!(self, Self::Cluster)
59    }
60}
61
62impl DbState {
63    pub(crate) fn from_u8(raw: u8) -> Option<Self> {
64        Some(match raw {
65            0 => Self::Created,
66            1 => Self::Starting,
67            2 => Self::Running,
68            3 => Self::ShuttingDown,
69            4 => Self::Stopped,
70            5 => Self::Faulted,
71            _ => return None,
72        })
73    }
74
75    pub(crate) fn load(atomic: &std::sync::atomic::AtomicU8) -> Self {
76        Self::from_u8(atomic.load(std::sync::atomic::Ordering::Acquire)).unwrap_or(Self::Stopped)
77    }
78
79    pub(crate) fn store(self, atomic: &std::sync::atomic::AtomicU8) {
80        atomic.store(self as u8, std::sync::atomic::Ordering::Release);
81    }
82
83    /// Atomically transition `current → new`; returns the observed state on failure.
84    pub(crate) fn compare_exchange(
85        current: Self,
86        new: Self,
87        atomic: &std::sync::atomic::AtomicU8,
88    ) -> Result<Self, Self> {
89        use std::sync::atomic::Ordering;
90        atomic
91            .compare_exchange(
92                current as u8,
93                new as u8,
94                Ordering::AcqRel,
95                Ordering::Acquire,
96            )
97            .map(|v| Self::from_u8(v).unwrap_or(Self::Stopped))
98            .map_err(|v| Self::from_u8(v).unwrap_or(Self::Stopped))
99    }
100}
101
102const DB_IO_WORKER_THREADS: usize = 2;
103// Cluster recovery has a finite, non-recursive async call chain that exceeds Tokio's 2 MiB
104// default worker stack. Keep the larger stack out of embedded and single-node runtimes.
105const CLUSTER_IO_WORKER_STACK_BYTES: usize = 4 * 1024 * 1024;
106
107struct DbControlRuntimeInner {
108    handle: tokio::runtime::Handle,
109    shutdown: tokio::sync::oneshot::Sender<()>,
110}
111
112/// Lazily-created executor for connector I/O and lifecycle ownership.
113///
114/// The runtime itself lives on a detached owner thread, so dropping a DB from async code never
115/// blocks on Tokio shutdown. The fixed two-worker size keeps control and feedback traffic live
116/// while one connector future is temporarily busy without adding a public tuning dimension.
117pub(crate) struct DbControlRuntime {
118    worker_stack_bytes: Option<usize>,
119    inner: parking_lot::Mutex<Option<DbControlRuntimeInner>>,
120}
121
122impl DbControlRuntime {
123    fn new(runtime_mode: RuntimeMode) -> Self {
124        Self {
125            worker_stack_bytes: runtime_mode
126                .is_cluster()
127                .then_some(CLUSTER_IO_WORKER_STACK_BYTES),
128            inner: parking_lot::Mutex::new(None),
129        }
130    }
131
132    pub(crate) fn handle(&self) -> Result<tokio::runtime::Handle, DbError> {
133        let mut runtime_slot = self.inner.lock();
134        if let Some(runtime) = runtime_slot.as_ref() {
135            return Ok(runtime.handle.clone());
136        }
137
138        let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
139        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
140        let worker_stack_bytes = self.worker_stack_bytes;
141        let owner = std::thread::Builder::new()
142            .name("laminar-io-owner".into())
143            .spawn(move || {
144                let mut builder = tokio::runtime::Builder::new_multi_thread();
145                builder
146                    .worker_threads(DB_IO_WORKER_THREADS)
147                    .thread_name("laminar-io")
148                    .enable_all();
149                if let Some(bytes) = worker_stack_bytes {
150                    builder.thread_stack_size(bytes);
151                }
152                let runtime = match builder.build() {
153                    Ok(runtime) => runtime,
154                    Err(error) => {
155                        let _ = ready_tx.send(Err(error.to_string()));
156                        return;
157                    }
158                };
159                if ready_tx.send(Ok(runtime.handle().clone())).is_err() {
160                    return;
161                }
162                runtime.block_on(async {
163                    let _ = shutdown_rx.await;
164                });
165                runtime.shutdown_timeout(std::time::Duration::from_secs(10));
166            })
167            .map_err(|error| {
168                DbError::Pipeline(format!("failed to spawn LaminarDB I/O runtime: {error}"))
169            })?;
170        // The shutdown sender is the nonblocking owner. The OS thread terminates after it fires.
171        drop(owner);
172
173        let handle = ready_rx
174            .recv()
175            .map_err(|_| {
176                DbError::Pipeline("LaminarDB I/O runtime exited during initialization".into())
177            })?
178            .map_err(|error| {
179                DbError::Pipeline(format!(
180                    "failed to initialize LaminarDB I/O runtime: {error}"
181                ))
182            })?;
183        *runtime_slot = Some(DbControlRuntimeInner {
184            handle: handle.clone(),
185            shutdown: shutdown_tx,
186        });
187        Ok(handle)
188    }
189}
190
191impl Drop for DbControlRuntime {
192    fn drop(&mut self) {
193        if let Some(runtime) = self.inner.get_mut().take() {
194            let _ = runtime.shutdown.send(());
195        }
196    }
197}
198
199pub(crate) fn canonical_object_name(name: &sqlparser::ast::ObjectName) -> Result<String, DbError> {
200    let [part] = name.0.as_slice() else {
201        return Err(DbError::InvalidOperation(format!(
202            "catalog identifiers must be unqualified: {name}"
203        )));
204    };
205    let ident = part.as_ident().ok_or_else(|| {
206        DbError::InvalidOperation(format!(
207            "dynamic catalog identifier is not supported: {name}"
208        ))
209    })?;
210    Ok(ident.value.clone())
211}
212
213pub(crate) fn exact_table_reference(name: &str) -> datafusion::common::TableReference {
214    // `Into<TableReference>` reparses `&str` and lowercases it independently of session config.
215    datafusion::common::TableReference::bare(name)
216}
217
218/// The main `LaminarDB` database handle.
219///
220/// Unified interface for SQL execution, data ingestion, and result consumption.
221pub struct LaminarDB {
222    runtime_mode: RuntimeMode,
223    pub(crate) catalog: Arc<SourceCatalog>,
224    pub(crate) planner: parking_lot::Mutex<StreamingPlanner>,
225    pub(crate) ctx: SessionContext,
226    pub(crate) config: LaminarConfig,
227    pub(crate) config_vars: Arc<HashMap<String, String>>,
228    pub(crate) shutdown: std::sync::atomic::AtomicBool,
229    pub(crate) coordinator:
230        Arc<tokio::sync::Mutex<Option<crate::checkpoint_coordinator::CheckpointCoordinator>>>,
231    pub(crate) connector_manager: parking_lot::Mutex<crate::connector_manager::ConnectorManager>,
232    pub(crate) connector_registry: Arc<laminar_connectors::registry::ConnectorRegistry>,
233    pub(crate) mv_registry: parking_lot::Mutex<laminar_core::mv::MvRegistry>,
234    pub(crate) table_store: Arc<parking_lot::RwLock<crate::table_store::TableStore>>,
235    pub(crate) state: Arc<std::sync::atomic::AtomicU8>,
236    /// Last recoverable runtime fault, panic, or terminal resource-exhaustion reason;
237    /// cleared on a clean start. Surfaced via `pipeline_status`/`/ready`.
238    pub(crate) last_fault: Arc<parking_lot::Mutex<Option<String>>>,
239    /// Permanent per-instance fence set when catalog cleanup cannot prove that every backing
240    /// registration was removed. Unlike a normal runtime fault, this is never restartable.
241    pub(crate) catalog_cleanup_fenced: std::sync::atomic::AtomicBool,
242    /// Deterministic provider-deregistration failure used to exercise the terminal cleanup fence.
243    #[cfg(test)]
244    pub(crate) catalog_cleanup_deregister_fault: parking_lot::Mutex<Option<String>>,
245    /// Self-ref set by `enable_supervision`; empty `Weak` (default) disables auto-restart.
246    pub(crate) supervisor_self: Arc<parking_lot::Mutex<std::sync::Weak<LaminarDB>>>,
247    /// Auto-restart timestamps within the sliding window; bounds restart storms.
248    pub(crate) restart_history: Arc<parking_lot::Mutex<Vec<std::time::Instant>>>,
249    /// Serializes start/stop/shutdown ownership across awaits. Runtime-handle `None` means an
250    /// owner is currently joining only while this lock is held; another caller must never treat
251    /// it as completed teardown.
252    pub(crate) lifecycle_lock: tokio::sync::Mutex<()>,
253    pub(crate) control_runtime: DbControlRuntime,
254    pub(crate) startup_attempt:
255        parking_lot::Mutex<Option<Arc<crate::pipeline_lifecycle::StartupAttempt>>>,
256    /// Serializes topology DDL through manifest persistence; catalog reads take a shared guard so
257    /// they cannot observe a tentative create that may still roll back.
258    pub(crate) topology_ddl_lock: tokio::sync::RwLock<()>,
259    /// Typed ownership for every user-visible catalog identifier.
260    pub(crate) catalog_namespace: parking_lot::Mutex<HashMap<String, CatalogObjectKind>>,
261    #[cfg(test)]
262    pub(crate) topology_planning_gate:
263        parking_lot::Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
264    #[cfg(test)]
265    pub(crate) stop_after_claim_gate:
266        parking_lot::Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
267    #[cfg(all(test, feature = "cluster"))]
268    pub(crate) catalog_seal_gate:
269        parking_lot::Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
270    /// Kept inside an async mutex while joined. A cancelled stop future drops only the guard,
271    /// never the sole watcher handle, so a retry cannot publish a false terminal state.
272    pub(crate) runtime_handle: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
273    /// Decoupled coordinated-commit committer task. Awaited in place so cancellation cannot lose
274    /// the sole handle while an issued external commit is still completing.
275    pub(crate) committer_handle: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
276    /// OS-released exclusive lock for a local checkpoint namespace. Deployment
277    /// identity prevents reuse after reset; this lock prevents two live processes from writing
278    /// divergent cuts into the same deployment.
279    pub(crate) checkpoint_namespace_lock: parking_lot::Mutex<Option<std::fs::File>>,
280    /// Every sink actor in the active generation, retained from spawn until terminal observation.
281    /// A replacement cannot start while any prior actor can still mutate an external system.
282    pub(crate) owned_sink_handles: Arc<parking_lot::Mutex<Vec<crate::sink_task::SinkTaskHandle>>>,
283    /// Every source task in the active or draining generation. A replacement cannot start until
284    /// the stable supervisor has observed actual task exit, including after Tokio cancellation.
285    pub(crate) owned_source_tasks: crate::pipeline::streaming_coordinator::OwnedSourceTasks,
286    /// Connector children admitted before an actor existed remain fenced across startup failure.
287    pub(crate) owned_connector_task_fences: crate::connector_task_fence::OwnedConnectorTaskFences,
288    pub(crate) shutdown_signal: Arc<tokio::sync::Notify>,
289    /// Persistent terminal cancellation for the currently installed compute runtime. Unlike a
290    /// notification permit, cancellation cannot be lost while the coordinator is between awaits.
291    pub(crate) runtime_shutdown: parking_lot::RwLock<tokio_util::sync::CancellationToken>,
292    pub(crate) engine_metrics:
293        parking_lot::Mutex<Option<Arc<crate::engine_metrics::EngineMetrics>>>,
294    pub(crate) prometheus_registry: parking_lot::Mutex<Option<Arc<prometheus::Registry>>>,
295    pub(crate) start_time: std::time::Instant,
296    pub(crate) session_properties: parking_lot::Mutex<HashMap<String, String>>,
297    /// Min of all source watermarks.
298    pub(crate) pipeline_watermark: Arc<std::sync::atomic::AtomicI64>,
299    pub(crate) lookup_registry: Arc<laminar_sql::datafusion::LookupTableRegistry>,
300    /// `None` unless `[ai]`/`[models]` are configured.
301    pub(crate) ai_runtime: Option<Arc<crate::ai::AiRuntime>>,
302    /// Runtime the AI inference workers spawn on; set alongside `ai_runtime`.
303    pub(crate) ai_handle: Option<tokio::runtime::Handle>,
304    /// Live-DDL channel; `None` outside `start..shutdown`.
305    pub(crate) control_tx: parking_lot::Mutex<Option<ControlMsgTx>>,
306    pub(crate) mv_store: Arc<parking_lot::RwLock<crate::mv_store::MvStore>>,
307    /// `None` in embedded mode.
308    #[cfg(feature = "cluster")]
309    pub(crate) cluster_controller:
310        parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::ClusterController>>>,
311    /// When set, the next start restores to this cluster-agreed epoch instead of the
312    /// local latest (taken in `start_inner`).
313    #[cfg(feature = "cluster")]
314    pub(crate) recover_target_epoch: parking_lot::Mutex<Option<u64>>,
315    /// Database-owned recovery supervisor. It outlives restartable pipeline generations and is
316    /// aborted if the facade is dropped without a graceful terminal shutdown.
317    #[cfg(feature = "cluster")]
318    pub(crate) recovery_monitor: parking_lot::Mutex<Option<tokio::task::JoinHandle<()>>>,
319    /// Prevents a timed-out coordinated stop/start from overlapping a later lifecycle attempt.
320    /// Cleared by the owning lifecycle thread only after its future actually finishes.
321    #[cfg(feature = "cluster")]
322    pub(crate) coordinated_lifecycle_active: Arc<std::sync::atomic::AtomicBool>,
323    /// Persistent ownership fence from the first observed recovery fault through the committed
324    /// recovery release. Unlike `coordinated_lifecycle_active`, this spans the gap between a
325    /// completed stop and its durable stopped report, so an operator start cannot invalidate the
326    /// report's quiescence claim.
327    #[cfg(feature = "cluster")]
328    pub(crate) coordinated_recovery_fenced: Arc<std::sync::atomic::AtomicBool>,
329    /// Opaque local fault request retained through durable publication and cleared only while an
330    /// authorized committed recovery Release is consumed. A newer fault atomically replaces it.
331    #[cfg(feature = "cluster")]
332    pub(crate) pending_recovery_fault: Arc<std::sync::atomic::AtomicU64>,
333    /// Epoch the most recent start restored from (`None` = started fresh). A rejoin fault
334    /// is reported only when prior local state existed — a fresh joiner lost no window.
335    #[cfg(feature = "cluster")]
336    pub(crate) last_recovery_epoch: parking_lot::Mutex<Option<u64>>,
337    /// Holds source intake closed during a coordinated round until every node has restarted
338    /// and rebound its shuffle receiver (the restore quorum). Sources re-read + re-shuffle the
339    /// replay window on restart; without this gate a node that restarts first shuffles into a
340    /// peer whose receiver isn't up yet and the fire-and-forget frames are lost.
341    #[cfg(feature = "cluster")]
342    pub(crate) source_gate: Arc<std::sync::atomic::AtomicBool>,
343    /// One-way local data-plane fence after stable process-lease loss.
344    #[cfg(feature = "cluster")]
345    pub(crate) cluster_authority_revoked: std::sync::atomic::AtomicBool,
346    /// Serializes source/shuffle authority grants with terminal revocation.
347    #[cfg(feature = "cluster")]
348    pub(crate) cluster_authority_transition: parking_lot::Mutex<()>,
349    /// Paired with `vnode_registry`; the coordinator gates commits when both are installed.
350    pub(crate) state_backend:
351        parking_lot::Mutex<Option<Arc<dyn laminar_core::state::StateBackend>>>,
352    pub(crate) vnode_registry: parking_lot::Mutex<Option<Arc<laminar_core::state::VnodeRegistry>>>,
353    pub(crate) physical_optimizer_rules:
354        Arc<[Arc<dyn datafusion::physical_optimizer::PhysicalOptimizerRule + Send + Sync>]>,
355    /// `target_partitions` override. Streaming plans currently require one reusable partition.
356    pub(crate) pipeline_target_partitions: Option<usize>,
357    #[cfg(feature = "cluster")]
358    pub(crate) shuffle_sender:
359        parking_lot::Mutex<Option<Arc<laminar_core::shuffle::ShuffleSender>>>,
360    #[cfg(feature = "cluster")]
361    pub(crate) shuffle_receiver:
362        Arc<parking_lot::Mutex<Option<Arc<laminar_core::shuffle::ShuffleReceiver>>>>,
363    #[cfg(feature = "cluster")]
364    pub(crate) decision_store:
365        parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::CheckpointDecisionStore>>>,
366    #[cfg(feature = "cluster")]
367    pub(crate) assignment_snapshot_store:
368        parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::AssignmentSnapshotStore>>>,
369    /// Create-once cluster catalog inventory, sealed at bootstrap and replayed at boot.
370    #[cfg(feature = "cluster")]
371    pub(crate) catalog_manifest_store:
372        parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::CatalogManifestStore>>>,
373    /// Pre-built shared checkpoint namespace installed during cluster construction.
374    #[cfg(feature = "cluster")]
375    cluster_checkpoint_object_store: Option<Arc<dyn object_store::ObjectStore>>,
376    /// Vnode state staged during rebalance adoption; operators drain it each cycle to resume
377    /// from the last committed epoch. Shared with `OperatorGraph` via `ClusterShuffleConfig`.
378    #[cfg(feature = "cluster")]
379    pub(crate) rehydrated_vnode_state: Arc<parking_lot::Mutex<HashMap<u32, RehydratedVnode>>>,
380    /// Vnodes lost on rebalance adoption; operators drain this each cycle and drop the stale
381    /// in-memory state so a later re-acquire merges into empty state (no additive double-count).
382    #[cfg(feature = "cluster")]
383    pub(crate) pending_revoke_vnodes: Arc<parking_lot::Mutex<rustc_hash::FxHashSet<u32>>>,
384    /// Process incarnation for which local vnode state has been restored or initialized.
385    #[cfg(feature = "cluster")]
386    local_state_incarnation: parking_lot::Mutex<Option<uuid::Uuid>>,
387    /// Serializes successor preparation with assignment-certificate publication. Remote state
388    /// reads do not hold the compute-cycle fence, but an old certificate must never be re-opened
389    /// while a newer audited assignment is being prepared.
390    #[cfg(feature = "cluster")]
391    pub(crate) assignment_adoption_lock: tokio::sync::Mutex<()>,
392    /// Changes whenever local assignment authority is suspended or invalidated. The snapshot
393    /// watcher uses it to reject a certificate computed from a head read before that closure.
394    #[cfg(feature = "cluster")]
395    pub(crate) assignment_authority_revision: std::sync::atomic::AtomicU64,
396    /// Linearizes assignment publication with compute-cycle entry so staged
397    /// revoke/rehydration state is applied before any row observes new ownership.
398    #[cfg(feature = "cluster")]
399    pub(crate) rotation_execution_fence: Arc<tokio::sync::RwLock<()>>,
400    /// Routes `db.checkpoint()` requests to the streaming coordinator for exact-attempt
401    /// barrier admission. `None` means no running pipeline can produce a valid cut.
402    pub(crate) force_ckpt_tx: parking_lot::Mutex<Option<ForceCheckpointTx>>,
403    pub(crate) subscription_registry: Arc<crate::subscription::SubscriptionRegistry>,
404    /// Resolved at `start()`; consulted by SUBSCRIBE WHERE.
405    pub(crate) stream_schemas:
406        parking_lot::RwLock<std::collections::HashMap<String, arrow_schema::SchemaRef>>,
407}
408
409impl Drop for LaminarDB {
410    fn drop(&mut self) {
411        #[cfg(feature = "cluster")]
412        if let Some(monitor) = self.recovery_monitor.get_mut().take() {
413            monitor.abort();
414        }
415        if matches!(
416            DbState::load(&self.state),
417            DbState::Starting | DbState::Running | DbState::ShuttingDown | DbState::Faulted
418        ) {
419            // Dropping Tokio handles detaches their tasks. A local decision hard-link already in
420            // a blocking filesystem worker could therefore outlive this facade. Keep the OS lock
421            // until process exit rather than let another in-process deployment acquire the same
422            // checkpoint namespace while old work can still publish. Graceful stop/shutdown
423            // clears the lock before reaching a terminal state and does not leak it.
424            if let Some(lock) = self.checkpoint_namespace_lock.get_mut().take() {
425                tracing::error!(
426                    "dropping an active checkpoint writer; retaining its namespace lock until process exit"
427                );
428                std::mem::forget(lock);
429            }
430        }
431    }
432}
433
434#[cfg(feature = "cluster")]
435fn checkpoint_participant_for_runtime(db: &LaminarDB) -> Option<u64> {
436    if !db.is_cluster_runtime() {
437        return None;
438    }
439    db.cluster_controller
440        .lock()
441        .as_ref()
442        .map(|controller| controller.instance_id().0)
443}
444
445#[cfg(feature = "cluster")]
446tokio::task_local! {
447    static CATALOG_MANIFEST_REPLAY: ();
448}
449
450#[cfg(feature = "cluster")]
451tokio::task_local! {
452    static CATALOG_BOOTSTRAP: ();
453}
454
455#[cfg(feature = "cluster")]
456struct CatalogBootstrapGuard<'a> {
457    db: &'a LaminarDB,
458    created: Vec<(String, CatalogObjectKind)>,
459    sealed: bool,
460}
461
462#[cfg(feature = "cluster")]
463impl CatalogBootstrapGuard<'_> {
464    fn record(&mut self, name: String, kind: CatalogObjectKind) {
465        self.created.push((name, kind));
466    }
467
468    fn sealed(mut self) {
469        self.sealed = true;
470    }
471}
472
473#[cfg(feature = "cluster")]
474impl Drop for CatalogBootstrapGuard<'_> {
475    fn drop(&mut self) {
476        if !self.sealed {
477            for (name, kind) in self.created.iter().rev() {
478                self.db
479                    .rollback_catalog_create_or_fence(name, *kind, "catalog bootstrap rollback");
480            }
481        }
482    }
483}
484
485#[cfg(feature = "cluster")]
486pub(crate) fn catalog_manifest_replay_active() -> bool {
487    CATALOG_MANIFEST_REPLAY.try_with(|()| ()).is_ok()
488}
489
490#[cfg(feature = "cluster")]
491fn catalog_bootstrap_active() -> bool {
492    CATALOG_BOOTSTRAP.try_with(|()| ()).is_ok()
493}
494
495#[cfg(not(feature = "cluster"))]
496pub(crate) const fn catalog_manifest_replay_active() -> bool {
497    false
498}
499
500fn is_topology_ddl(statement: &StreamingStatement) -> bool {
501    match statement {
502        StreamingStatement::CreateSource(_)
503        | StreamingStatement::CreateSink(_)
504        | StreamingStatement::CreateContinuousQuery { .. }
505        | StreamingStatement::DropSource { .. }
506        | StreamingStatement::DropSink { .. }
507        | StreamingStatement::DropMaterializedView { .. }
508        | StreamingStatement::CreateMaterializedView { .. }
509        | StreamingStatement::CreateStream { .. }
510        | StreamingStatement::DropStream { .. }
511        | StreamingStatement::AlterSource { .. }
512        | StreamingStatement::CreateLookupTable(_)
513        | StreamingStatement::DropLookupTable { .. } => true,
514        StreamingStatement::Standard(statement) => matches!(
515            statement.as_ref(),
516            sqlparser::ast::Statement::CreateTable(_)
517                | sqlparser::ast::Statement::Drop {
518                    object_type: sqlparser::ast::ObjectType::Table,
519                    ..
520                }
521                | sqlparser::ast::Statement::AlterTable { .. }
522        ),
523        _ => false,
524    }
525}
526
527fn mutates_database(statement: &StreamingStatement) -> bool {
528    is_topology_ddl(statement)
529        || matches!(
530            statement,
531            StreamingStatement::InsertInto { .. }
532                | StreamingStatement::Checkpoint
533                | StreamingStatement::RestoreCheckpoint { .. }
534        )
535        || matches!(
536            statement,
537            StreamingStatement::Standard(statement)
538                if matches!(statement.as_ref(), sqlparser::ast::Statement::Set(_))
539        )
540}
541
542fn reads_catalog(statement: &StreamingStatement) -> bool {
543    matches!(
544        statement,
545        StreamingStatement::Standard(statement)
546            if matches!(statement.as_ref(), sqlparser::ast::Statement::Query(_))
547    ) || matches!(
548        statement,
549        StreamingStatement::InsertInto { .. }
550            | StreamingStatement::Show(_)
551            | StreamingStatement::Describe { .. }
552            | StreamingStatement::Explain { .. }
553    )
554}
555
556#[cfg(feature = "cluster")]
557fn catalog_create_identity(
558    statement: &StreamingStatement,
559) -> Result<Option<(String, CatalogObjectKind, &'static str)>, DbError> {
560    let identity = match statement {
561        StreamingStatement::CreateSource(create) => (
562            canonical_object_name(&create.name)?,
563            CatalogObjectKind::Source,
564            "CREATE SOURCE",
565        ),
566        StreamingStatement::CreateSink(create) => (
567            canonical_object_name(&create.name)?,
568            CatalogObjectKind::Sink,
569            "CREATE SINK",
570        ),
571        StreamingStatement::CreateMaterializedView { name, .. } => (
572            canonical_object_name(name)?,
573            CatalogObjectKind::MaterializedView,
574            "CREATE MATERIALIZED VIEW",
575        ),
576        StreamingStatement::CreateStream { name, .. } => (
577            canonical_object_name(name)?,
578            CatalogObjectKind::Stream,
579            "CREATE STREAM",
580        ),
581        StreamingStatement::CreateLookupTable(create) => (
582            canonical_object_name(&create.name)?,
583            CatalogObjectKind::LookupTable,
584            "CREATE LOOKUP TABLE",
585        ),
586        StreamingStatement::Standard(statement) => {
587            let sqlparser::ast::Statement::CreateTable(create) = statement.as_ref() else {
588                return Ok(None);
589            };
590            (
591                canonical_object_name(&create.name)?,
592                CatalogObjectKind::Table,
593                "CREATE TABLE",
594            )
595        }
596        _ => return Ok(None),
597    };
598    Ok(Some(identity))
599}
600
601#[cfg(feature = "cluster")]
602fn validate_cluster_catalog_create(
603    sql: &str,
604    statement: &StreamingStatement,
605) -> Result<(String, CatalogObjectKind, &'static str), DbError> {
606    if matches!(
607        statement,
608        StreamingStatement::CreateStream {
609            retention_bytes: Some(_),
610            ..
611        }
612    ) {
613        return Err(DbError::Unsupported(
614            "CREATE STREAM RETAIN HISTORY is not supported in cluster runtime until replay is globally ordered and checkpoint-aligned"
615                .into(),
616        ));
617    }
618    if catalog_ddl_contains_comment(sql)? {
619        return Err(DbError::InvalidOperation(
620            "cluster catalog DDL cannot persist SQL comments; submit one canonical typed definition"
621                .into(),
622        ));
623    }
624    if let Some(key) = sensitive_catalog_property(statement) {
625        return Err(DbError::InvalidOperation(format!(
626            "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"
627        )));
628    }
629    if connector_source_requires_schema_discovery(statement) {
630        return Err(DbError::InvalidOperation(
631            "cluster connector sources require an explicit column schema; runtime schema discovery is not a durable catalog identity"
632                .into(),
633        ));
634    }
635    let identity = catalog_create_identity(statement)?.ok_or_else(|| {
636        DbError::InvalidOperation(
637            "cluster catalog bootstrap accepts only reversible typed CREATE statements".into(),
638        )
639    })?;
640    if matches!(
641        identity.1,
642        CatalogObjectKind::Table | CatalogObjectKind::LookupTable
643    ) {
644        return Err(DbError::InvalidOperation(format!(
645            "{} is not supported in cluster mode until reference-table rows and source positions are atomically replicated through distributed state",
646            identity.2
647        )));
648    }
649    Ok(identity)
650}
651
652#[cfg(feature = "cluster")]
653fn uri_contains_unsupported_secret(value: &str, allow_reference: bool) -> bool {
654    if value.contains("://") {
655        return laminar_connectors::security::value_contains_uri_secret(value, allow_reference);
656    }
657    {
658        let lower = value.to_ascii_lowercase();
659        lower.split_whitespace().any(|part| {
660            part.strip_prefix("password=")
661                .or_else(|| part.strip_prefix("pwd="))
662                .is_some_and(|password| {
663                    let password = password.trim_matches(|ch| ch == '\'' || ch == '"');
664                    !(password.is_empty()
665                        || allow_reference
666                            && laminar_connectors::security::is_env_reference(password))
667                })
668        })
669    }
670}
671
672#[cfg(feature = "cluster")]
673fn catalog_property_contains_unsupported_secret(
674    key: &str,
675    value: &str,
676    allow_reference: bool,
677) -> bool {
678    let lower = key.to_ascii_lowercase();
679    let normalized = lower.replace(['.', '-'], "_");
680    let secret_key = laminar_connectors::security::is_secret_option_key(key);
681    if secret_key {
682        return !(allow_reference && laminar_connectors::security::is_env_reference(value));
683    }
684    if value.contains("://")
685        && laminar_connectors::security::value_contains_uri_secret(value, allow_reference)
686    {
687        return true;
688    }
689    (normalized.contains("connection")
690        || normalized == "uri"
691        || normalized.ends_with("_uri")
692        || normalized == "url"
693        || normalized.ends_with("_url")
694        || normalized == "dsn")
695        && uri_contains_unsupported_secret(value, allow_reference)
696}
697
698#[cfg(feature = "cluster")]
699fn catalog_ddl_contains_comment(sql: &str) -> Result<bool, DbError> {
700    use sqlparser::tokenizer::{Token, Tokenizer, Whitespace};
701
702    let tokens = Tokenizer::new(&sqlparser::dialect::GenericDialect {}, sql)
703        .tokenize()
704        .map_err(|error| {
705            DbError::InvalidOperation(format!("catalog DDL tokenization failed: {error}"))
706        })?;
707    Ok(tokens.into_iter().any(|token| {
708        matches!(
709            token,
710            Token::Whitespace(
711                Whitespace::SingleLineComment { .. } | Whitespace::MultiLineComment(_)
712            )
713        )
714    }))
715}
716
717#[cfg(feature = "cluster")]
718fn sensitive_catalog_property(statement: &StreamingStatement) -> Option<String> {
719    fn find<'a>(
720        options: impl IntoIterator<Item = (&'a String, &'a String)>,
721        allow_reference: bool,
722    ) -> Option<String> {
723        options
724            .into_iter()
725            .find(|(key, value)| {
726                catalog_property_contains_unsupported_secret(key, value, allow_reference)
727            })
728            .map(|(key, _)| key.clone())
729    }
730
731    match statement {
732        StreamingStatement::CreateSource(create) => find(create.with_options.iter(), true)
733            .or_else(|| find(create.connector_options.iter(), true))
734            .or_else(|| {
735                create
736                    .format
737                    .as_ref()
738                    .and_then(|format| find(format.options.iter(), true))
739            }),
740        StreamingStatement::CreateSink(create) => find(create.with_options.iter(), true)
741            .or_else(|| find(create.connector_options.iter(), true))
742            .or_else(|| find(create.output_options.iter(), true))
743            .or_else(|| {
744                create
745                    .format
746                    .as_ref()
747                    .and_then(|format| find(format.options.iter(), true))
748            }),
749        StreamingStatement::CreateLookupTable(create) => find(create.with_options.iter(), false),
750        StreamingStatement::Standard(statement) => {
751            let sqlparser::ast::Statement::CreateTable(create) = statement.as_ref() else {
752                return None;
753            };
754            let sqlparser::ast::CreateTableOptions::With(options) = &create.table_options else {
755                return None;
756            };
757            options.iter().find_map(|option| {
758                let sqlparser::ast::SqlOption::KeyValue { key, value } = option else {
759                    return None;
760                };
761                let key = key.to_string();
762                let value = value.to_string();
763                catalog_property_contains_unsupported_secret(&key, value.trim_matches('\''), false)
764                    .then_some(key)
765            })
766        }
767        _ => None,
768    }
769}
770
771#[cfg(feature = "cluster")]
772fn connector_source_requires_schema_discovery(statement: &StreamingStatement) -> bool {
773    let StreamingStatement::CreateSource(create) = statement else {
774        return false;
775    };
776    let has_connector = create.connector_type.is_some()
777        || create
778            .with_options
779            .keys()
780            .any(|key| key.eq_ignore_ascii_case("connector"));
781    has_connector && (create.columns.is_empty() || create.has_wildcard)
782}
783
784#[cfg(not(feature = "cluster"))]
785const fn checkpoint_participant_for_runtime(_db: &LaminarDB) -> Option<u64> {
786    None
787}
788
789/// Reply channel for a single `db.checkpoint()` request.
790pub(crate) type ForceCheckpointReply =
791    crossfire::oneshot::TxOneshot<Result<crate::checkpoint_coordinator::CheckpointResult, DbError>>;
792
793pub(crate) type ForceCheckpointTx =
794    crossfire::MAsyncTx<crossfire::mpsc::Array<ForceCheckpointReply>>;
795
796pub(crate) type ForceCheckpointRx =
797    crossfire::AsyncRx<crossfire::mpsc::Array<ForceCheckpointReply>>;
798
799pub(crate) const FORCE_CHECKPOINT_CHANNEL_CAPACITY: usize = 64;
800
801pub(crate) struct SourceWatermarkState {
802    pub(crate) extractor: laminar_core::time::EventTimeExtractor,
803    pub(crate) generator: Box<dyn laminar_core::time::WatermarkGenerator>,
804    pub(crate) column: String,
805}
806
807/// Keep rows at/after the watermark. `Ok(None)` = all rows late;
808/// `Err` = schema drift (missing/non-timestamp column).
809pub(crate) fn filter_late_rows(
810    batch: &RecordBatch,
811    column: &str,
812    watermark: i64,
813) -> Result<Option<RecordBatch>, laminar_core::time::FilterError> {
814    laminar_core::time::filter_batch_by_timestamp(
815        batch,
816        column,
817        watermark,
818        laminar_core::time::ThresholdOp::GreaterEq,
819    )
820}
821
822pub(crate) use laminar_core::time::parse_duration_str;
823
824/// Committed vnode state staged during rebalance adoption for deferred apply.
825#[cfg(feature = "cluster")]
826#[derive(Debug, Clone)]
827pub struct RehydratedVnode {
828    /// Committed epoch the chain head was read from.
829    pub epoch: u64,
830    /// Recovery chain (oldest→newest decoded-as-bytes partials): a FULL base plus any delta partials.
831    pub chain: Vec<bytes::Bytes>,
832}
833
834/// Summary of a single [`LaminarDB::adopt_assignment_snapshot`] call.
835#[cfg(feature = "cluster")]
836#[derive(Debug, Default)]
837pub struct SnapshotAdoption {
838    /// `false` when the snapshot was stale or no registry was installed.
839    pub adopted: bool,
840    /// The snapshot version considered.
841    pub version: u64,
842    /// Vnodes this node gained in this rotation.
843    pub newly_acquired: Vec<u32>,
844    /// How many of `newly_acquired` had committed state read back.
845    pub rehydrated: usize,
846    /// Committed epoch the rehydration read from, if any.
847    pub rehydration_epoch: Option<u64>,
848}
849
850/// Result of certifying a clustered process at startup.
851#[cfg(feature = "cluster")]
852#[derive(Debug, Clone, Copy, PartialEq, Eq)]
853pub enum ClusterStartupDisposition {
854    /// This process owns vnodes and may serve its certified assignment.
855    Serving,
856    /// This process owns no vnodes and remains data-plane fenced while watching assignments.
857    Idle,
858    /// A coordinated recovery must release this process before it may serve data.
859    RecoveryFenced,
860}
861
862/// Result of publishing one locally serialized assignment-authority certificate.
863#[cfg(feature = "cluster")]
864#[derive(Debug, Clone, Copy, PartialEq, Eq)]
865pub(crate) struct AssignmentAuthorityActivation {
866    /// The exact certificate was installed and published.
867    pub(crate) installed: bool,
868    /// Source intake was opened; recovery keeps it closed even after installation.
869    pub(crate) intake_open: bool,
870    /// Authority revision observed at the local publication point.
871    pub(crate) revision: u64,
872}
873
874#[cfg(feature = "cluster")]
875fn owned_vnode_indices(
876    assignment: &[laminar_core::state::NodeId],
877    self_id: laminar_core::state::NodeId,
878) -> Result<Vec<u32>, DbError> {
879    assignment
880        .iter()
881        .enumerate()
882        .filter(|(_, owner)| **owner == self_id)
883        .map(|(vnode, _)| {
884            u32::try_from(vnode).map_err(|_| {
885                DbError::Checkpoint(
886                    "vnode assignment is too large to encode a u32 vnode identifier".into(),
887                )
888            })
889        })
890        .collect()
891}
892
893impl LaminarDB {
894    pub(crate) const fn runtime_mode(&self) -> RuntimeMode {
895        self.runtime_mode
896    }
897
898    /// Whether this instance was constructed with distributed runtime semantics.
899    pub(crate) const fn is_cluster_runtime(&self) -> bool {
900        self.runtime_mode().is_cluster()
901    }
902
903    /// Vnodes revoked by a rebalance and staged for operator-state drop on the next cycle; drained
904    /// once the compute thread applies the revoke. Observability/test hook.
905    #[cfg(feature = "cluster")]
906    #[doc(hidden)]
907    #[must_use]
908    pub fn pending_revoke_vnode_count(&self) -> usize {
909        self.pending_revoke_vnodes.lock().len()
910    }
911
912    /// Create an embedded in-memory database with default settings.
913    ///
914    /// # Errors
915    ///
916    /// Returns `DbError` if `DataFusion` context creation fails.
917    pub fn open() -> Result<Arc<Self>, DbError> {
918        Self::open_with_config(LaminarConfig::default())
919    }
920
921    /// Create with custom configuration.
922    ///
923    /// # Errors
924    ///
925    /// Returns `DbError` if `DataFusion` context creation fails.
926    pub fn open_with_config(config: LaminarConfig) -> Result<Arc<Self>, DbError> {
927        let db = Self::open_with_config_and_vars(config, HashMap::new())?;
928        db.connector_registry.freeze();
929        Ok(Arc::new(db))
930    }
931
932    /// Create with custom configuration and config variables for SQL substitution.
933    ///
934    /// # Errors
935    ///
936    /// Returns `DbError` if `DataFusion` context creation fails.
937    #[allow(clippy::unnecessary_wraps)]
938    pub(crate) fn open_with_config_and_vars(
939        config: LaminarConfig,
940        config_vars: HashMap<String, String>,
941    ) -> Result<Self, DbError> {
942        Self::open_with_config_and_vars_and_rules(
943            config,
944            config_vars,
945            &[],
946            None,
947            RuntimeMode::Local,
948        )
949    }
950
951    /// Same as [`Self::open_with_config_and_vars`] but also installs
952    /// the given physical-optimizer rules on the `DataFusion` session.
953    #[allow(clippy::unnecessary_wraps)]
954    #[allow(clippy::too_many_lines)] // flat field-init of a large struct
955    pub(crate) fn open_with_config_and_vars_and_rules(
956        mut config: LaminarConfig,
957        config_vars: HashMap<String, String>,
958        extra_optimizer_rules: &[Arc<
959            dyn datafusion::physical_optimizer::PhysicalOptimizerRule + Send + Sync,
960        >],
961        target_partitions: Option<usize>,
962        runtime_mode: RuntimeMode,
963    ) -> Result<Self, DbError> {
964        if let Some(checkpoint) = config.checkpoint.as_mut() {
965            let max_state_data_bytes = checkpoint.max_staged_bytes.unwrap_or(
966                laminar_core::storage::checkpoint_store::DEFAULT_MAX_CHECKPOINT_STATE_BYTES,
967            );
968            laminar_core::storage::checkpoint_store::validate_max_checkpoint_state_bytes(
969                max_state_data_bytes,
970            )
971            .map_err(|error| DbError::Config(format!("checkpoint.max_staged_bytes: {error}")))?;
972            checkpoint.max_staged_bytes = Some(max_state_data_bytes);
973        }
974
975        // One-time crossfire backoff tuning; idempotent, only helps single-core VMs.
976        crossfire::detect_backoff_cfg();
977
978        let lookup_registry = Arc::new(laminar_sql::datafusion::LookupTableRegistry::new());
979
980        // Wire the LookupJoinExtensionPlanner so LookupJoinNode → LookupJoinExec.
981        let ctx = {
982            let mut session_config = laminar_sql::datafusion::base_session_config();
983            if let Some(n) = target_partitions {
984                session_config = session_config.with_target_partitions(n);
985            }
986            let extension_planner: Arc<
987                dyn datafusion::physical_planner::ExtensionPlanner + Send + Sync,
988            > = Arc::new(laminar_sql::datafusion::LookupJoinExtensionPlanner::new(
989                Arc::clone(&lookup_registry),
990            ));
991            let query_planner: Arc<dyn datafusion::execution::context::QueryPlanner + Send + Sync> =
992                Arc::new(LookupQueryPlanner { extension_planner });
993            let mut state_builder = datafusion::execution::SessionStateBuilder::new()
994                .with_config(session_config)
995                .with_default_features()
996                .with_query_planner(query_planner);
997            for rule in extra_optimizer_rules {
998                state_builder = state_builder.with_physical_optimizer_rule(Arc::clone(rule));
999            }
1000            SessionContext::new_with_state(state_builder.build())
1001        };
1002        register_streaming_functions(&ctx);
1003
1004        let catalog = Arc::new(SourceCatalog::new(
1005            config.default_buffer_size,
1006            config.default_backpressure,
1007        ));
1008
1009        let connector_registry = Arc::new(laminar_connectors::registry::ConnectorRegistry::new());
1010        Self::register_builtin_connectors(&connector_registry)?;
1011        let physical_rules = extra_optimizer_rules.to_vec();
1012
1013        Ok(Self {
1014            runtime_mode,
1015            catalog,
1016            planner: parking_lot::Mutex::new(StreamingPlanner::new()),
1017            ctx,
1018            config,
1019            config_vars: Arc::new(config_vars),
1020            shutdown: std::sync::atomic::AtomicBool::new(false),
1021            coordinator: Arc::new(tokio::sync::Mutex::new(None)),
1022            connector_manager: parking_lot::Mutex::new(
1023                crate::connector_manager::ConnectorManager::new(),
1024            ),
1025            connector_registry,
1026            mv_registry: parking_lot::Mutex::new(laminar_core::mv::MvRegistry::new()),
1027            table_store: Arc::new(parking_lot::RwLock::new(
1028                crate::table_store::TableStore::new(),
1029            )),
1030            state: Arc::new(std::sync::atomic::AtomicU8::new(DbState::Created as u8)),
1031            last_fault: Arc::new(parking_lot::Mutex::new(None)),
1032            catalog_cleanup_fenced: std::sync::atomic::AtomicBool::new(false),
1033            #[cfg(test)]
1034            catalog_cleanup_deregister_fault: parking_lot::Mutex::new(None),
1035            supervisor_self: Arc::new(parking_lot::Mutex::new(std::sync::Weak::new())),
1036            restart_history: Arc::new(parking_lot::Mutex::new(Vec::new())),
1037            lifecycle_lock: tokio::sync::Mutex::new(()),
1038            control_runtime: DbControlRuntime::new(runtime_mode),
1039            startup_attempt: parking_lot::Mutex::new(None),
1040            topology_ddl_lock: tokio::sync::RwLock::new(()),
1041            catalog_namespace: parking_lot::Mutex::new(HashMap::new()),
1042            #[cfg(test)]
1043            topology_planning_gate: parking_lot::Mutex::new(None),
1044            #[cfg(test)]
1045            stop_after_claim_gate: parking_lot::Mutex::new(None),
1046            #[cfg(all(test, feature = "cluster"))]
1047            catalog_seal_gate: parking_lot::Mutex::new(None),
1048            runtime_handle: tokio::sync::Mutex::new(None),
1049            committer_handle: tokio::sync::Mutex::new(None),
1050            checkpoint_namespace_lock: parking_lot::Mutex::new(None),
1051            owned_sink_handles: Arc::new(parking_lot::Mutex::new(Vec::new())),
1052            owned_source_tasks: Arc::new(parking_lot::Mutex::new(Vec::new())),
1053            owned_connector_task_fences: Arc::new(parking_lot::Mutex::new(Vec::new())),
1054            shutdown_signal: Arc::new(tokio::sync::Notify::new()),
1055            runtime_shutdown: parking_lot::RwLock::new(tokio_util::sync::CancellationToken::new()),
1056            engine_metrics: parking_lot::Mutex::new(None),
1057            prometheus_registry: parking_lot::Mutex::new(None),
1058            start_time: std::time::Instant::now(),
1059            session_properties: parking_lot::Mutex::new(HashMap::new()),
1060            pipeline_watermark: Arc::new(std::sync::atomic::AtomicI64::new(i64::MIN)),
1061            lookup_registry,
1062            ai_runtime: None,
1063            ai_handle: None,
1064            control_tx: parking_lot::Mutex::new(None),
1065            mv_store: Arc::new(parking_lot::RwLock::new(crate::mv_store::MvStore::new())),
1066            #[cfg(feature = "cluster")]
1067            cluster_controller: parking_lot::Mutex::new(None),
1068            #[cfg(feature = "cluster")]
1069            recover_target_epoch: parking_lot::Mutex::new(None),
1070            #[cfg(feature = "cluster")]
1071            recovery_monitor: parking_lot::Mutex::new(None),
1072            #[cfg(feature = "cluster")]
1073            coordinated_lifecycle_active: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1074            #[cfg(feature = "cluster")]
1075            coordinated_recovery_fenced: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1076            #[cfg(feature = "cluster")]
1077            pending_recovery_fault: Arc::new(std::sync::atomic::AtomicU64::new(0)),
1078            #[cfg(feature = "cluster")]
1079            last_recovery_epoch: parking_lot::Mutex::new(None),
1080            #[cfg(feature = "cluster")]
1081            source_gate: Arc::new(std::sync::atomic::AtomicBool::new(
1082                runtime_mode.is_cluster(),
1083            )),
1084            #[cfg(feature = "cluster")]
1085            cluster_authority_revoked: std::sync::atomic::AtomicBool::new(false),
1086            #[cfg(feature = "cluster")]
1087            cluster_authority_transition: parking_lot::Mutex::new(()),
1088            state_backend: parking_lot::Mutex::new(None),
1089            vnode_registry: parking_lot::Mutex::new(None),
1090            physical_optimizer_rules: physical_rules.into(),
1091            pipeline_target_partitions: target_partitions,
1092            #[cfg(feature = "cluster")]
1093            shuffle_sender: parking_lot::Mutex::new(None),
1094            #[cfg(feature = "cluster")]
1095            shuffle_receiver: Arc::new(parking_lot::Mutex::new(None)),
1096            #[cfg(feature = "cluster")]
1097            decision_store: parking_lot::Mutex::new(None),
1098            #[cfg(feature = "cluster")]
1099            assignment_snapshot_store: parking_lot::Mutex::new(None),
1100            #[cfg(feature = "cluster")]
1101            catalog_manifest_store: parking_lot::Mutex::new(None),
1102            #[cfg(feature = "cluster")]
1103            cluster_checkpoint_object_store: None,
1104            #[cfg(feature = "cluster")]
1105            rehydrated_vnode_state: Arc::new(parking_lot::Mutex::new(HashMap::new())),
1106            #[cfg(feature = "cluster")]
1107            pending_revoke_vnodes: Arc::new(parking_lot::Mutex::new(
1108                rustc_hash::FxHashSet::default(),
1109            )),
1110            #[cfg(feature = "cluster")]
1111            local_state_incarnation: parking_lot::Mutex::new(None),
1112            #[cfg(feature = "cluster")]
1113            assignment_adoption_lock: tokio::sync::Mutex::new(()),
1114            #[cfg(feature = "cluster")]
1115            assignment_authority_revision: std::sync::atomic::AtomicU64::new(0),
1116            #[cfg(feature = "cluster")]
1117            rotation_execution_fence: Arc::new(tokio::sync::RwLock::new(())),
1118            force_ckpt_tx: parking_lot::Mutex::new(None),
1119            subscription_registry: Arc::new(crate::subscription::SubscriptionRegistry::new()),
1120            stream_schemas: parking_lot::RwLock::new(std::collections::HashMap::new()),
1121        })
1122    }
1123
1124    /// Install the AI subsystem. Called by the builder; `handle` must be the
1125    /// main multi-threaded runtime.
1126    pub(crate) fn set_ai_runtime(
1127        &mut self,
1128        runtime: Arc<crate::ai::AiRuntime>,
1129        handle: tokio::runtime::Handle,
1130    ) {
1131        // Non-fatal: inference still works if catalog view registration fails.
1132        if let Err(e) = crate::ai_catalog::register_ai_catalog(&self.ctx, &runtime) {
1133            tracing::warn!(error = %e, "failed to register laminar.* AI catalog views");
1134        }
1135        self.ai_runtime = Some(runtime);
1136        self.ai_handle = Some(handle);
1137    }
1138
1139    #[cfg(feature = "cluster")]
1140    pub(crate) fn set_shuffle_sender(&self, sender: Arc<laminar_core::shuffle::ShuffleSender>) {
1141        *self.shuffle_sender.lock() = Some(sender);
1142    }
1143
1144    #[cfg(feature = "cluster")]
1145    pub(crate) fn set_shuffle_receiver(
1146        &self,
1147        receiver: Arc<laminar_core::shuffle::ShuffleReceiver>,
1148    ) {
1149        *self.shuffle_receiver.lock() = Some(receiver);
1150    }
1151
1152    #[cfg(feature = "cluster")]
1153    pub(crate) fn invalidate_shuffle_assignment_fence(&self) {
1154        self.assignment_authority_revision
1155            .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
1156        if let Some(receiver) = self.shuffle_receiver.lock().as_ref() {
1157            receiver.invalidate_assignment_fence();
1158        }
1159        if let Some(sender) = self.shuffle_sender.lock().as_ref() {
1160            sender.invalidate_assignment_fence();
1161        }
1162    }
1163
1164    /// Close shuffle admission during temporary assignment-authority uncertainty without
1165    /// destroying the retained certificate's delivery sequence domain.
1166    #[cfg(feature = "cluster")]
1167    pub(crate) fn suspend_shuffle_assignment_fence(&self) {
1168        self.assignment_authority_revision
1169            .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
1170        if let Some(receiver) = self.shuffle_receiver.lock().as_ref() {
1171            receiver.suspend_assignment_fence();
1172        }
1173        if let Some(sender) = self.shuffle_sender.lock().as_ref() {
1174            sender.suspend_assignment_fence();
1175        }
1176    }
1177
1178    #[cfg(feature = "cluster")]
1179    fn withdraw_assignment_authority(
1180        &self,
1181        controller: &laminar_core::cluster::control::ClusterController,
1182    ) {
1183        self.set_source_gate(true);
1184        controller.publish_checkpoint_drain_transition(None);
1185        controller.publish_checkpoint_assignment_fence(None);
1186        self.suspend_shuffle_assignment_fence();
1187    }
1188
1189    #[cfg(feature = "cluster")]
1190    pub(crate) fn install_shuffle_assignment_fence(
1191        &self,
1192        fence: &laminar_core::checkpoint::CheckpointAssignmentFence,
1193    ) -> Result<(), DbError> {
1194        let _transition = self.cluster_authority_transition.lock();
1195        if self
1196            .cluster_authority_revoked
1197            .load(std::sync::atomic::Ordering::Acquire)
1198        {
1199            return Err(DbError::Checkpoint(
1200                "shuffle assignment install has terminally revoked process authority".into(),
1201            ));
1202        }
1203        let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
1204            DbError::Checkpoint("shuffle assignment install has no cluster controller".into())
1205        })?;
1206        if !controller.process_lease_is_live() {
1207            self.invalidate_shuffle_assignment_fence();
1208            return Err(DbError::Checkpoint(
1209                "shuffle assignment install has no live process lease".into(),
1210            ));
1211        }
1212        let registry = self.vnode_registry.lock().clone().ok_or_else(|| {
1213            DbError::Checkpoint("shuffle assignment install has no vnode registry".into())
1214        })?;
1215        let assignment = registry.versioned_snapshot();
1216        let owners: Vec<u64> = assignment.owners().iter().map(|owner| owner.0).collect();
1217        // The caller installs under the assignment-adoption and execution write fences, then
1218        // publishes this certificate through the controller. Requiring it to be visible first
1219        // exposes authority before the transport has actually switched scopes.
1220        if !fence.is_canonical()
1221            || fence.assignment_version != assignment.version()
1222            || fence.vnode_count != registry.vnode_count()
1223            || !fence.matches_owner_map(&owners)
1224            || fence.participant_incarnation(controller.instance_id().0)
1225                != Some(controller.recovery_incarnation())
1226        {
1227            self.invalidate_shuffle_assignment_fence();
1228            return Err(DbError::Checkpoint(format!(
1229                "shuffle assignment certificate does not match local assignment {} and process {}",
1230                assignment.version(),
1231                controller.instance_id().0
1232            )));
1233        }
1234
1235        let receiver = self.shuffle_receiver.lock().clone();
1236        let sender = self.shuffle_sender.lock().clone();
1237        let expected_digest = fence.digest();
1238        let receiver_exact = receiver.as_ref().is_none_or(|endpoint| {
1239            endpoint.assignment_version() == fence.assignment_version
1240                && endpoint.active_assignment_digest() == Some(expected_digest)
1241        });
1242        let sender_exact = sender.as_ref().is_none_or(|endpoint| {
1243            endpoint.assignment_version() == fence.assignment_version
1244                && endpoint.active_assignment_digest() == Some(expected_digest)
1245        });
1246        if receiver_exact && sender_exact {
1247            if !controller.process_lease_is_live() {
1248                self.invalidate_shuffle_assignment_fence();
1249                return Err(DbError::Checkpoint(
1250                    "shuffle assignment install lost its process lease".into(),
1251                ));
1252            }
1253            return Ok(());
1254        }
1255        let endpoint_conflict = receiver.as_ref().is_some_and(|endpoint| {
1256            endpoint.local_id() != controller.instance_id().0
1257                || endpoint.incarnation() != controller.recovery_incarnation()
1258                || endpoint.assignment_version() > fence.assignment_version
1259                || (endpoint.assignment_version() == fence.assignment_version
1260                    && endpoint.active_assignment_digest() != Some(expected_digest))
1261        }) || sender.as_ref().is_some_and(|endpoint| {
1262            endpoint.local_id() != controller.instance_id().0
1263                || endpoint.incarnation() != controller.recovery_incarnation()
1264                || endpoint.assignment_version() > fence.assignment_version
1265                || (endpoint.assignment_version() == fence.assignment_version
1266                    && endpoint.active_assignment_digest() != Some(expected_digest))
1267        });
1268        if endpoint_conflict {
1269            self.invalidate_shuffle_assignment_fence();
1270            return Err(DbError::Checkpoint(
1271                "shuffle endpoint identity conflicts with the certified assignment".into(),
1272            ));
1273        }
1274
1275        // Background and operator shuffle paths hold the execution read fence. Activate inbound
1276        // first, then make outbound admission the local transition's linearization point.
1277        let result = (|| {
1278            if let Some(receiver) = receiver.as_ref() {
1279                receiver
1280                    .install_assignment_fence(fence, &owners)
1281                    .map_err(|error| {
1282                        DbError::Checkpoint(format!(
1283                            "failed to install receiver shuffle assignment certificate: {error}"
1284                        ))
1285                    })?;
1286                if receiver.assignment_version() != fence.assignment_version
1287                    || receiver.active_assignment_digest() != Some(expected_digest)
1288                {
1289                    return Err(DbError::Checkpoint(
1290                        "receiver did not activate the exact shuffle assignment certificate".into(),
1291                    ));
1292                }
1293            }
1294            if let Some(sender) = sender.as_ref() {
1295                sender
1296                    .install_assignment_fence(fence, &owners)
1297                    .map_err(|error| {
1298                        DbError::Checkpoint(format!(
1299                            "failed to install sender shuffle assignment certificate: {error}"
1300                        ))
1301                    })?;
1302                if sender.assignment_version() != fence.assignment_version
1303                    || sender.active_assignment_digest() != Some(expected_digest)
1304                {
1305                    return Err(DbError::Checkpoint(
1306                        "sender did not activate the exact shuffle assignment certificate".into(),
1307                    ));
1308                }
1309            }
1310            Ok(())
1311        })();
1312        if result.is_ok() && !controller.process_lease_is_live() {
1313            self.invalidate_shuffle_assignment_fence();
1314            return Err(DbError::Checkpoint(
1315                "shuffle assignment install lost its process lease".into(),
1316            ));
1317        }
1318        if result.is_err() {
1319            // Keep watcher caches coherent with a partial endpoint install. Direct endpoint
1320            // invalidation would close transport authority without advancing the shared revision.
1321            self.invalidate_shuffle_assignment_fence();
1322        }
1323        result
1324    }
1325
1326    #[cfg(feature = "cluster")]
1327    async fn assignment_recovery_admission(
1328        &self,
1329        controller: &laminar_core::cluster::control::ClusterController,
1330        deadline: tokio::time::Instant,
1331    ) -> Result<Option<laminar_core::cluster::control::RecoveryAdmissionSnapshot>, DbError> {
1332        let snapshot =
1333            tokio::time::timeout_at(deadline, controller.read_recovery_admission_snapshot())
1334                .await
1335                .map_err(|_| {
1336                    DbError::Checkpoint(
1337                        "recovery admission audit timed out before opening intake".into(),
1338                    )
1339                })?
1340                .map_err(|error| {
1341                    DbError::Checkpoint(format!(
1342                        "recovery admission authority is unavailable: {error}"
1343                    ))
1344                })?;
1345        let (committed_generation, committed_epoch) = match snapshot.committed_release() {
1346            None => (0, 0),
1347            Some(release) => match release.phase {
1348                laminar_core::cluster::control::RecoverPhase::ReleaseCommitted { epoch } => {
1349                    (release.round.id.generation, epoch)
1350                }
1351                _ => {
1352                    return Err(DbError::Checkpoint(
1353                        "latest recovery terminal is not a committed Release".into(),
1354                    ));
1355                }
1356            },
1357        };
1358        let local_generation = self.shuffle_recovery_generation()?;
1359        if !snapshot.fault_inventory().faults().is_empty() {
1360            controller.set_recovering(true);
1361            return Ok(None);
1362        }
1363        let transport_is_current =
1364            local_generation.is_none_or(|generation| generation == committed_generation);
1365        let restored_epoch = *self.last_recovery_epoch.lock();
1366        let state_is_current =
1367            committed_epoch == 0 || restored_epoch.is_some_and(|epoch| epoch >= committed_epoch);
1368        if transport_is_current && state_is_current {
1369            return Ok(Some(snapshot));
1370        }
1371
1372        controller.set_recovering(true);
1373        tokio::time::timeout_at(
1374            deadline,
1375            crate::coordinated_recovery::request_local_fault(
1376                controller,
1377                &self.pending_recovery_fault,
1378            ),
1379        )
1380        .await
1381        .map_err(|_| {
1382            DbError::Checkpoint(
1383                "recovery fault publication timed out for stale recovery admission".into(),
1384            )
1385        })?
1386        .map_err(|error| {
1387            DbError::Checkpoint(format!(
1388                "could not publish recovery fault for stale recovery admission: {error}"
1389            ))
1390        })?;
1391        tracing::debug!(
1392            ?local_generation,
1393            committed_generation,
1394            ?restored_epoch,
1395            committed_epoch,
1396            "assignment intake remains fenced for coordinated recovery"
1397        );
1398        Ok(None)
1399    }
1400
1401    /// Install shuffle authority, publish its controller certificate, and conditionally open
1402    /// source intake at one local assignment/execution boundary.
1403    ///
1404    /// `expected_revision` must have been captured before any durable reads used to derive
1405    /// `fence`. A concurrent closure advances the revision and this method leaves that closure in
1406    /// force. The controller certificate is deliberately published only after both endpoints are
1407    /// installed.
1408    #[cfg(feature = "cluster")]
1409    pub(crate) async fn activate_assignment_authority(
1410        &self,
1411        fence: &laminar_core::checkpoint::CheckpointAssignmentFence,
1412        drain_transition: Option<laminar_core::checkpoint::AssignmentDrainTransition>,
1413        expected_revision: u64,
1414        deadline: tokio::time::Instant,
1415    ) -> Result<AssignmentAuthorityActivation, DbError> {
1416        if drain_transition
1417            .as_ref()
1418            .is_some_and(|transition| transition.predecessor != *fence)
1419        {
1420            return Err(DbError::Checkpoint(
1421                "assignment drain transition does not bind the installed predecessor certificate"
1422                    .into(),
1423            ));
1424        }
1425        let _adoption = tokio::time::timeout_at(deadline, self.assignment_adoption_lock.lock())
1426            .await
1427            .map_err(|_| {
1428                DbError::Checkpoint("timed out serializing assignment authority activation".into())
1429            })?;
1430        let _execution = tokio::time::timeout_at(
1431            deadline,
1432            Arc::clone(&self.rotation_execution_fence).write_owned(),
1433        )
1434        .await
1435        .map_err(|_| {
1436            DbError::Checkpoint("timed out draining the prior assignment execution scope".into())
1437        })?;
1438        let revision = self
1439            .assignment_authority_revision
1440            .load(std::sync::atomic::Ordering::Acquire);
1441        if revision != expected_revision || tokio::time::Instant::now() >= deadline {
1442            return Ok(AssignmentAuthorityActivation {
1443                installed: false,
1444                intake_open: false,
1445                revision,
1446            });
1447        }
1448
1449        // Installing a certificate briefly serializes source and shuffle authority. During a
1450        // drain, preserve an already-open predecessor execution scope: its source tasks are held
1451        // by the drain protocol, while its coordinator must still consume the FIFO boundary and
1452        // checkpoint barrier. Fresh and target-only processes enter with this gate closed and
1453        // remain closed until the terminal assignment is adopted.
1454        let intake_was_closed = self.cluster_intake_fenced();
1455        self.set_source_gate(true);
1456        let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
1457            DbError::Checkpoint("assignment activation has no cluster controller".into())
1458        })?;
1459        if !controller.process_lease_is_live() {
1460            self.revoke_cluster_authority();
1461            return Ok(AssignmentAuthorityActivation {
1462                installed: false,
1463                intake_open: false,
1464                revision: self
1465                    .assignment_authority_revision
1466                    .load(std::sync::atomic::Ordering::Acquire),
1467            });
1468        }
1469        let registry = self.vnode_registry.lock().clone().ok_or_else(|| {
1470            DbError::Checkpoint("assignment activation has no vnode registry".into())
1471        })?;
1472        let assignment = registry.versioned_snapshot();
1473        let owners: Vec<u64> = assignment.owners().iter().map(|owner| owner.0).collect();
1474        let local_id = controller.instance_id().0;
1475        let local_incarnation = fence.participant_incarnation(local_id);
1476        if !fence.is_canonical()
1477            || fence.assignment_version != assignment.version()
1478            || fence.vnode_count != registry.vnode_count()
1479            || !fence.matches_owner_map(&owners)
1480            || local_incarnation
1481                .is_some_and(|incarnation| incarnation != controller.recovery_incarnation())
1482            || (local_incarnation.is_none() && owners.contains(&local_id))
1483        {
1484            return Err(DbError::Checkpoint(format!(
1485                "assignment certificate does not match local assignment {} and process {}",
1486                assignment.version(),
1487                local_id
1488            )));
1489        }
1490
1491        // A live process outside the owner roster is control-plane ready, but has no source,
1492        // compute, shuffle, checkpoint, or recovery authority. Retain the audited certificate so
1493        // its watcher can observe a later assignment that grants ownership.
1494        if local_incarnation.is_none() {
1495            let shuffle_active = self
1496                .shuffle_receiver
1497                .lock()
1498                .as_ref()
1499                .is_some_and(|receiver| receiver.assignment_version() != 0)
1500                || self
1501                    .shuffle_sender
1502                    .lock()
1503                    .as_ref()
1504                    .is_some_and(|sender| sender.assignment_version() != 0);
1505            let revision = if shuffle_active {
1506                self.invalidate_shuffle_assignment_fence();
1507                self.assignment_authority_revision
1508                    .load(std::sync::atomic::Ordering::Acquire)
1509            } else {
1510                expected_revision
1511            };
1512            controller.publish_checkpoint_drain_transition(drain_transition);
1513            controller.publish_checkpoint_assignment_fence(Some(fence.clone()));
1514            if !controller.process_lease_is_live()
1515                || self
1516                    .assignment_authority_revision
1517                    .load(std::sync::atomic::Ordering::Acquire)
1518                    != revision
1519            {
1520                if controller.process_lease_is_live() {
1521                    self.withdraw_assignment_authority(&controller);
1522                } else {
1523                    self.revoke_cluster_authority();
1524                }
1525                return Ok(AssignmentAuthorityActivation {
1526                    installed: false,
1527                    intake_open: false,
1528                    revision: self
1529                        .assignment_authority_revision
1530                        .load(std::sync::atomic::Ordering::Acquire),
1531                });
1532            }
1533            return Ok(AssignmentAuthorityActivation {
1534                installed: true,
1535                intake_open: false,
1536                revision,
1537            });
1538        }
1539        if let Err(error) = self.install_shuffle_assignment_fence(fence) {
1540            controller.publish_checkpoint_drain_transition(None);
1541            controller.publish_checkpoint_assignment_fence(None);
1542            return Err(error);
1543        }
1544        if self
1545            .assignment_authority_revision
1546            .load(std::sync::atomic::Ordering::Acquire)
1547            != expected_revision
1548        {
1549            self.withdraw_assignment_authority(&controller);
1550            return Ok(AssignmentAuthorityActivation {
1551                installed: false,
1552                intake_open: false,
1553                revision: self
1554                    .assignment_authority_revision
1555                    .load(std::sync::atomic::Ordering::Acquire),
1556            });
1557        }
1558
1559        let source_drain_active = drain_transition.is_some();
1560        let expected_drain_transition = drain_transition.clone();
1561        controller.publish_checkpoint_drain_transition(drain_transition);
1562        controller.publish_checkpoint_assignment_fence(Some(fence.clone()));
1563        if self
1564            .assignment_authority_revision
1565            .load(std::sync::atomic::Ordering::Acquire)
1566            != expected_revision
1567        {
1568            self.withdraw_assignment_authority(&controller);
1569            return Ok(AssignmentAuthorityActivation {
1570                installed: false,
1571                intake_open: false,
1572                revision: self
1573                    .assignment_authority_revision
1574                    .load(std::sync::atomic::Ordering::Acquire),
1575            });
1576        }
1577        let mut intake_open = false;
1578        let preserve_predecessor_execution = source_drain_active && !intake_was_closed;
1579        if !controller.is_recovering() && (!source_drain_active || preserve_predecessor_execution) {
1580            // Recovery authority and active faults must come from one durable view. Reading them
1581            // independently can pair an old terminal with the empty fault set created by a newer
1582            // committed Release.
1583            let recovery_admission = match self
1584                .assignment_recovery_admission(&controller, deadline)
1585                .await
1586            {
1587                Ok(Some(snapshot)) => snapshot,
1588                Ok(None) => {
1589                    return Ok(AssignmentAuthorityActivation {
1590                        installed: true,
1591                        intake_open: false,
1592                        revision: expected_revision,
1593                    });
1594                }
1595                Err(error) => {
1596                    self.withdraw_assignment_authority(&controller);
1597                    return Err(error);
1598                }
1599            };
1600
1601            if controller.is_recovering() {
1602                return Ok(AssignmentAuthorityActivation {
1603                    installed: true,
1604                    intake_open: false,
1605                    revision: expected_revision,
1606                });
1607            }
1608            let shuffle_sender = { self.shuffle_sender.lock().clone() };
1609            if let Some(sender) = shuffle_sender {
1610                let mut retry_delay = std::time::Duration::from_millis(25);
1611                let mut last_error = None;
1612                loop {
1613                    if !controller.process_lease_is_live() {
1614                        self.revoke_cluster_authority();
1615                        return Ok(AssignmentAuthorityActivation {
1616                            installed: false,
1617                            intake_open: false,
1618                            revision: self
1619                                .assignment_authority_revision
1620                                .load(std::sync::atomic::Ordering::Acquire),
1621                        });
1622                    }
1623                    if self
1624                        .assignment_authority_revision
1625                        .load(std::sync::atomic::Ordering::Acquire)
1626                        != expected_revision
1627                        || controller
1628                            .checkpoint_assignment_fence(fence.assignment_version)
1629                            .as_ref()
1630                            != Some(fence)
1631                        || controller.checkpoint_drain_transition() != expected_drain_transition
1632                    {
1633                        return Ok(AssignmentAuthorityActivation {
1634                            installed: false,
1635                            intake_open: false,
1636                            revision: self
1637                                .assignment_authority_revision
1638                                .load(std::sync::atomic::Ordering::Acquire),
1639                        });
1640                    }
1641                    if tokio::time::Instant::now() >= deadline {
1642                        self.withdraw_assignment_authority(&controller);
1643                        return Err(DbError::Checkpoint(format!(
1644                            "assignment shuffle mesh did not become ready before the activation deadline{}",
1645                            last_error
1646                                .as_ref()
1647                                .map(|error| format!("; last error: {error}"))
1648                                .unwrap_or_default()
1649                        )));
1650                    }
1651                    match tokio::time::timeout_at(deadline, sender.establish_assignment_mesh(fence))
1652                        .await
1653                    {
1654                        Ok(Ok(())) => break,
1655                        Ok(Err(error)) => last_error = Some(error.to_string()),
1656                        Err(_) => {
1657                            self.withdraw_assignment_authority(&controller);
1658                            return Err(DbError::Checkpoint(format!(
1659                                "assignment shuffle mesh readiness timed out{}",
1660                                last_error
1661                                    .as_ref()
1662                                    .map(|error| format!("; last error: {error}"))
1663                                    .unwrap_or_default()
1664                            )));
1665                        }
1666                    }
1667                    let wake = tokio::time::Instant::now()
1668                        .checked_add(retry_delay)
1669                        .map_or(deadline, |wake| wake.min(deadline));
1670                    tokio::time::sleep_until(wake).await;
1671                    retry_delay = retry_delay
1672                        .checked_mul(2)
1673                        .unwrap_or(std::time::Duration::from_millis(250))
1674                        .min(std::time::Duration::from_millis(250));
1675                }
1676            }
1677
1678            let expected_leader = expected_drain_transition
1679                .as_ref()
1680                .filter(|_| preserve_predecessor_execution)
1681                .map(|transition| transition.leader.clone());
1682            let audited_leader = match controller
1683                .audit_assignment_leader_authority(fence, expected_leader.as_ref(), deadline)
1684                .await
1685            {
1686                Ok(proof) => proof,
1687                Err(error) => {
1688                    self.withdraw_assignment_authority(&controller);
1689                    return Err(DbError::Checkpoint(format!(
1690                        "assignment leader authority audit failed before opening intake: {error}"
1691                    )));
1692                }
1693            };
1694            let recovery_admission_current = match tokio::time::timeout_at(
1695                deadline,
1696                controller.recovery_admission_is_current(&recovery_admission, &audited_leader),
1697            )
1698            .await
1699            {
1700                Ok(Ok(current)) => current,
1701                Ok(Err(error)) => {
1702                    self.withdraw_assignment_authority(&controller);
1703                    return Err(DbError::Checkpoint(format!(
1704                        "recovery admission revalidation failed before opening intake: {error}"
1705                    )));
1706                }
1707                Err(_) => {
1708                    self.withdraw_assignment_authority(&controller);
1709                    return Err(DbError::Checkpoint(
1710                        "recovery admission revalidation timed out before opening intake".into(),
1711                    ));
1712                }
1713            };
1714            if !recovery_admission_current {
1715                self.withdraw_assignment_authority(&controller);
1716                return Ok(AssignmentAuthorityActivation {
1717                    installed: false,
1718                    intake_open: false,
1719                    revision: self
1720                        .assignment_authority_revision
1721                        .load(std::sync::atomic::Ordering::Acquire),
1722                });
1723            }
1724            let authority_unchanged = || {
1725                tokio::time::Instant::now() < deadline
1726                    && self
1727                        .assignment_authority_revision
1728                        .load(std::sync::atomic::Ordering::Acquire)
1729                        == expected_revision
1730                    && !controller.is_recovering()
1731                    && controller.process_lease_is_live()
1732                    && controller.current_leader().map(|leader| leader.0)
1733                        == Some(audited_leader.owner.node_id)
1734                    && controller
1735                        .checkpoint_assignment_fence(fence.assignment_version)
1736                        .as_ref()
1737                        == Some(fence)
1738                    && controller.checkpoint_drain_transition() == expected_drain_transition
1739            };
1740            if !authority_unchanged() {
1741                self.withdraw_assignment_authority(&controller);
1742                return Ok(AssignmentAuthorityActivation {
1743                    installed: false,
1744                    intake_open: false,
1745                    revision: self
1746                        .assignment_authority_revision
1747                        .load(std::sync::atomic::Ordering::Acquire),
1748                });
1749            }
1750            self.set_source_gate(false);
1751            if authority_unchanged() {
1752                intake_open = true;
1753            } else {
1754                self.withdraw_assignment_authority(&controller);
1755                return Ok(AssignmentAuthorityActivation {
1756                    installed: false,
1757                    intake_open: false,
1758                    revision: self
1759                        .assignment_authority_revision
1760                        .load(std::sync::atomic::Ordering::Acquire),
1761                });
1762            }
1763        }
1764
1765        Ok(AssignmentAuthorityActivation {
1766            installed: true,
1767            intake_open,
1768            revision: expected_revision,
1769        })
1770    }
1771
1772    /// Drop buffered shuffle slices and stashed barriers before a rewind: their senders
1773    /// rewind and replay them, so folding a buffered copy afterwards double-counts.
1774    #[cfg(feature = "cluster")]
1775    pub(crate) fn purge_shuffle_receiver_buffers(&self) {
1776        let Some(receiver) = self.shuffle_receiver.lock().clone() else {
1777            return;
1778        };
1779        let queued = receiver.drain_available().len();
1780        let staged = receiver.drain_all_staged().len();
1781        let barriers = receiver.drain_staged_barriers().len();
1782        if queued + staged + barriers > 0 {
1783            tracing::info!(
1784                queued,
1785                staged,
1786                barriers,
1787                "purged stale shuffle buffers before coordinated rewind"
1788            );
1789        }
1790    }
1791
1792    #[cfg(feature = "cluster")]
1793    pub(crate) fn set_decision_store(
1794        &self,
1795        store: Arc<laminar_core::cluster::control::CheckpointDecisionStore>,
1796    ) {
1797        *self.decision_store.lock() = Some(store);
1798    }
1799
1800    #[cfg(feature = "cluster")]
1801    pub(crate) fn set_assignment_snapshot_store(
1802        &self,
1803        store: Arc<laminar_core::cluster::control::AssignmentSnapshotStore>,
1804    ) {
1805        *self.assignment_snapshot_store.lock() = Some(store);
1806    }
1807
1808    /// Install the shared catalog manifest store.
1809    #[cfg(feature = "cluster")]
1810    pub(crate) fn set_catalog_manifest_store(
1811        &self,
1812        store: Arc<laminar_core::cluster::control::CatalogManifestStore>,
1813    ) {
1814        *self.catalog_manifest_store.lock() = Some(store);
1815    }
1816
1817    /// Install the shared checkpoint namespace before the database is published behind `Arc`.
1818    #[cfg(feature = "cluster")]
1819    pub(crate) fn set_cluster_checkpoint_object_store(
1820        &mut self,
1821        store: Arc<dyn object_store::ObjectStore>,
1822    ) -> Result<(), DbError> {
1823        if !self.is_cluster_runtime() || self.cluster_controller.lock().is_none() {
1824            return Err(DbError::Config(
1825                "cluster checkpoint object store requires a cluster controller".into(),
1826            ));
1827        }
1828        self.cluster_checkpoint_object_store = Some(store);
1829        Ok(())
1830    }
1831
1832    /// Clone the immutable cluster checkpoint namespace selected at construction.
1833    #[cfg(feature = "cluster")]
1834    pub(crate) fn cluster_checkpoint_object_store(
1835        &self,
1836    ) -> Option<Arc<dyn object_store::ObjectStore>> {
1837        self.cluster_checkpoint_object_store.clone()
1838    }
1839
1840    #[cfg(feature = "cluster")]
1841    fn catalog_manifest_inventory(
1842        &self,
1843    ) -> Result<Vec<laminar_core::cluster::control::CatalogManifestEntry>, DbError> {
1844        let ordered = self.connector_manager.lock().ordered_ddl();
1845        let namespace = self.catalog_namespace.lock();
1846        if ordered.len() != namespace.len() {
1847            return Err(DbError::Pipeline(format!(
1848                "typed catalog has {} objects but the durable DDL inventory has {}",
1849                namespace.len(),
1850                ordered.len()
1851            )));
1852        }
1853        ordered
1854            .into_iter()
1855            .map(|(canonical_name, ddl)| {
1856                let kind = namespace.get(&canonical_name).copied().ok_or_else(|| {
1857                    DbError::Pipeline(format!(
1858                        "DDL inventory entry '{canonical_name}' has no typed catalog owner"
1859                    ))
1860                })?;
1861                Ok(laminar_core::cluster::control::CatalogManifestEntry {
1862                    canonical_name,
1863                    kind,
1864                    ddl,
1865                })
1866            })
1867            .collect()
1868    }
1869
1870    #[cfg(feature = "cluster")]
1871    fn exact_bootstrap_noop(
1872        &self,
1873        sql: &str,
1874        statement: &StreamingStatement,
1875    ) -> Result<Option<ExecuteResult>, DbError> {
1876        let Some((name, kind, statement_type)) = catalog_create_identity(statement)? else {
1877            return Ok(None);
1878        };
1879        let local_ddl = self
1880            .connector_manager
1881            .lock()
1882            .get_ddl(&name)
1883            .map(str::to_owned);
1884        let local_kind = self.catalog_namespace.lock().get(&name).copied();
1885        if local_ddl.is_none() && local_kind.is_none() {
1886            return Ok(None);
1887        }
1888        if local_ddl.as_deref() != Some(sql) || local_kind != Some(kind) {
1889            return Err(DbError::Pipeline(format!(
1890                "cluster bootstrap definition for '{name}' differs from the durable typed catalog"
1891            )));
1892        }
1893        Ok(Some(ExecuteResult::Ddl(DdlInfo {
1894            statement_type: statement_type.to_string(),
1895            object_name: name,
1896            applied: false,
1897        })))
1898    }
1899
1900    #[cfg(feature = "cluster")]
1901    fn validate_catalog_seal_authority(
1902        &self,
1903        proof: Option<&laminar_core::cluster::control::LeaderProof>,
1904    ) -> Result<(), DbError> {
1905        let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
1906            DbError::Pipeline(
1907                "[LDB-6043] cluster catalog bootstrap requires a cluster controller".into(),
1908            )
1909        })?;
1910        let Some(proof) = proof else {
1911            return Err(DbError::Pipeline(
1912                "[LDB-6043] cluster catalog bootstrap requires the active durable leader lease"
1913                    .into(),
1914            ));
1915        };
1916        if !controller.catalog_bootstrap_proof_is_live(proof) {
1917            return Err(DbError::Pipeline(
1918                "[LDB-6043] cluster catalog bootstrap lost its durable leader lease before sealing"
1919                    .into(),
1920            ));
1921        }
1922        Ok(())
1923    }
1924
1925    /// Replay catalog DDL from the shared manifest before cluster startup.
1926    ///
1927    /// # Errors
1928    /// Fails closed when the manifest cannot be loaded, a local definition conflicts with it, or
1929    /// any entry cannot be recreated. A node must never start with a partial cluster topology.
1930    #[cfg(feature = "cluster")]
1931    pub(crate) async fn restore_catalog_from_manifest(
1932        &self,
1933    ) -> Result<Option<laminar_core::cluster::control::CatalogManifest>, DbError> {
1934        self.connector_registry.freeze();
1935        let Some(store) = self.catalog_manifest_store.lock().clone() else {
1936            return Ok(None);
1937        };
1938        let Some(manifest) = store.load().await.map_err(|error| {
1939            DbError::Pipeline(format!(
1940                "[{}] catalog manifest load failed: {error}",
1941                laminar_core::error_codes::RECOVERY_FAILED
1942            ))
1943        })?
1944        else {
1945            return Ok(None);
1946        };
1947
1948        let mut replay_guard = CatalogBootstrapGuard {
1949            db: self,
1950            created: Vec::new(),
1951            sealed: false,
1952        };
1953        for entry in &manifest.entries {
1954            if catalog_ddl_contains_comment(&entry.ddl)? {
1955                return Err(DbError::Pipeline(format!(
1956                    "[{}] catalog manifest entry '{}' contains SQL comments rather than one canonical typed definition",
1957                    laminar_core::error_codes::RECOVERY_FAILED,
1958                    entry.canonical_name
1959                )));
1960            }
1961            let statements = parse_streaming_sql(&entry.ddl).map_err(|error| {
1962                DbError::Pipeline(format!(
1963                    "[{}] catalog manifest entry '{}' is not valid topology DDL: {error}",
1964                    laminar_core::error_codes::RECOVERY_FAILED,
1965                    entry.canonical_name
1966                ))
1967            })?;
1968            if statements.len() != 1 {
1969                return Err(DbError::Pipeline(format!(
1970                    "[{}] catalog manifest entry '{}' must contain exactly one typed CREATE statement",
1971                    laminar_core::error_codes::RECOVERY_FAILED,
1972                    entry.canonical_name
1973                )));
1974            }
1975            let Some((name, kind, _)) = catalog_create_identity(&statements[0])? else {
1976                return Err(DbError::Pipeline(format!(
1977                    "[{}] catalog manifest entry '{}' must contain exactly one typed CREATE statement",
1978                    laminar_core::error_codes::RECOVERY_FAILED,
1979                    entry.canonical_name
1980                )));
1981            };
1982            if name != entry.canonical_name || kind != entry.kind {
1983                return Err(DbError::Pipeline(format!(
1984                    "[{}] catalog manifest entry '{}' does not match its typed DDL identity",
1985                    laminar_core::error_codes::RECOVERY_FAILED,
1986                    entry.canonical_name
1987                )));
1988            }
1989            if connector_source_requires_schema_discovery(&statements[0]) {
1990                return Err(DbError::Pipeline(format!(
1991                    "[{}] catalog manifest source '{}' lacks an explicit durable schema",
1992                    laminar_core::error_codes::RECOVERY_FAILED,
1993                    entry.canonical_name
1994                )));
1995            }
1996            if let Some(key) = sensitive_catalog_property(&statements[0]) {
1997                return Err(DbError::Pipeline(format!(
1998                    "[{}] catalog manifest entry '{}' contains secret property '{key}'",
1999                    laminar_core::error_codes::RECOVERY_FAILED,
2000                    entry.canonical_name
2001                )));
2002            }
2003        }
2004
2005        for entry in &manifest.entries {
2006            let local_ddl = self
2007                .connector_manager
2008                .lock()
2009                .get_ddl(&entry.canonical_name)
2010                .map(str::to_owned);
2011            if let Some(local_ddl) = local_ddl {
2012                let local_kind = self
2013                    .catalog_namespace
2014                    .lock()
2015                    .get(&entry.canonical_name)
2016                    .copied();
2017                if local_ddl != entry.ddl || local_kind != Some(entry.kind) {
2018                    return Err(DbError::Pipeline(format!(
2019                        "[{}] local catalog definition for '{}' conflicts with catalog manifest",
2020                        laminar_core::error_codes::RECOVERY_FAILED,
2021                        entry.canonical_name
2022                    )));
2023                }
2024                continue;
2025            }
2026            CATALOG_MANIFEST_REPLAY
2027                .scope((), self.execute_single_already_gated(&entry.ddl))
2028                .await
2029                .map_err(|error| {
2030                    DbError::Pipeline(format!(
2031                        "[{}] catalog manifest replay failed for '{}': {error}",
2032                        laminar_core::error_codes::RECOVERY_FAILED,
2033                        entry.canonical_name
2034                    ))
2035                })?;
2036            let replayed_ddl = self
2037                .connector_manager
2038                .lock()
2039                .get_ddl(&entry.canonical_name)
2040                .map(str::to_owned);
2041            let replayed_kind = self
2042                .catalog_namespace
2043                .lock()
2044                .get(&entry.canonical_name)
2045                .copied();
2046            if replayed_ddl.as_deref() != Some(entry.ddl.as_str())
2047                || replayed_kind != Some(entry.kind)
2048            {
2049                return Err(DbError::Pipeline(format!(
2050                    "[{}] catalog manifest entry '{}' completed without installing its exact \
2051                     durable DDL identity",
2052                    laminar_core::error_codes::RECOVERY_FAILED,
2053                    entry.canonical_name
2054                )));
2055            }
2056            replay_guard.record(entry.canonical_name.clone(), entry.kind);
2057            tracing::info!(name = %entry.canonical_name, "replayed catalog DDL from manifest");
2058        }
2059
2060        let local_inventory = self.catalog_manifest_inventory()?;
2061        if local_inventory.as_slice() != manifest.entries.as_slice() {
2062            return Err(DbError::Pipeline(format!(
2063                "[{}] local catalog DDL inventory does not match the ordered catalog manifest \
2064                 (local entries: {}, manifest entries: {}); refusing cluster startup",
2065                laminar_core::error_codes::RECOVERY_FAILED,
2066                local_inventory.len(),
2067                manifest.entries.len()
2068            )));
2069        }
2070        replay_guard.sealed();
2071        Ok(Some(manifest))
2072    }
2073
2074    /// Atomically adopt a new vnode assignment across the registry, state-backend
2075    /// fence, and coordinator, then rehydrate committed state for newly-acquired
2076    /// vnodes. Idempotent for versions ≤ the current registry version.
2077    ///
2078    /// Rehydration runs after the coordinator lock is released so a slow
2079    /// object-store read can't stall the checkpoint cadence.
2080    ///
2081    /// # Errors
2082    /// Returns a checkpoint error when the end-to-end deadline expires or source-offset handoff
2083    /// or vnode-state rehydration fails. The assignment is not published in that case, so the
2084    /// same snapshot remains retryable.
2085    #[cfg(feature = "cluster")]
2086    pub async fn adopt_assignment_snapshot(
2087        &self,
2088        snapshot: laminar_core::cluster::control::AssignmentSnapshot,
2089        deadline: tokio::time::Instant,
2090    ) -> Result<SnapshotAdoption, DbError> {
2091        let version = snapshot.version;
2092        tokio::time::timeout_at(deadline, async {
2093            let _adoption = self.assignment_adoption_lock.lock().await;
2094            self.adopt_assignment_snapshot_locked(snapshot, deadline)
2095                .await
2096        })
2097        .await
2098        .unwrap_or_else(|_| {
2099            Err(DbError::Checkpoint(format!(
2100                "assignment {version} adoption exceeded its end-to-end deadline"
2101            )))
2102        })
2103    }
2104
2105    #[cfg(feature = "cluster")]
2106    async fn adopt_assignment_snapshot_locked(
2107        &self,
2108        snapshot: laminar_core::cluster::control::AssignmentSnapshot,
2109        deadline: tokio::time::Instant,
2110    ) -> Result<SnapshotAdoption, DbError> {
2111        if snapshot.draining {
2112            return Err(DbError::Checkpoint(format!(
2113                "assignment {} is a draining generation and cannot publish ownership",
2114                snapshot.version
2115            )));
2116        }
2117        if !snapshot.has_canonical_participants() {
2118            return Err(DbError::Checkpoint(format!(
2119                "assignment {} has no canonical process roster",
2120                snapshot.version
2121            )));
2122        }
2123        let Some(registry) = self.vnode_registry.lock().clone() else {
2124            return Ok(SnapshotAdoption::default());
2125        };
2126        let vnode_count = registry.vnode_count();
2127        let new_assignment: Arc<[laminar_core::state::NodeId]> = snapshot
2128            .to_vnode_vec(vnode_count)
2129            .map_err(|error| DbError::Checkpoint(error.to_string()))?
2130            .into();
2131        let controller = self.cluster_controller.lock().clone();
2132        let assignment_snapshot_store = self.assignment_snapshot_store.lock().clone();
2133        if let Some(store) = assignment_snapshot_store.as_ref() {
2134            crate::rebalance::audit_assignment_snapshot_authority(
2135                store,
2136                controller.as_deref(),
2137                &snapshot,
2138            )
2139            .await
2140            .map_err(|error| {
2141                DbError::Checkpoint(format!(
2142                    "assignment {} authority audit failed: {error}",
2143                    snapshot.version
2144                ))
2145            })?;
2146        }
2147        if snapshot.version <= registry.assignment_version() {
2148            return Ok(SnapshotAdoption {
2149                adopted: false,
2150                version: snapshot.version,
2151                ..SnapshotAdoption::default()
2152            });
2153        }
2154        // The target is now durable-authority-audited and still newer. Close every predecessor
2155        // admission path before reading its process roster, source handoff, or vnode state. Scope
2156        // cancellation releases compute cycles blocked in shuffle so the final write fence can
2157        // drain; any later error deliberately leaves this authority closed for a full retry.
2158        self.set_source_gate(true);
2159        if let Some(controller) = controller.as_ref() {
2160            controller.publish_checkpoint_assignment_fence(None);
2161        }
2162        self.invalidate_shuffle_assignment_fence();
2163        let predecessor_drain = Arc::clone(&self.rotation_execution_fence)
2164            .write_owned()
2165            .await;
2166        drop(predecessor_drain);
2167
2168        let self_id = controller
2169            .as_ref()
2170            .map_or(laminar_core::state::NodeId(0), |controller| {
2171                laminar_core::state::NodeId(controller.instance_id().0)
2172            });
2173        let observed_assignment = registry.versioned_snapshot();
2174        let observed_version = observed_assignment.version();
2175        let current_incarnation = controller
2176            .as_ref()
2177            .map(|controller| controller.recovery_incarnation());
2178        let local_state_is_current = current_incarnation
2179            .is_some_and(|incarnation| *self.local_state_incarnation.lock() == Some(incarnation));
2180        let force_local_restore = if observed_version == 0 || local_state_is_current {
2181            false
2182        } else if let (Some(store), Some(incarnation)) =
2183            (assignment_snapshot_store, current_incarnation)
2184        {
2185            let prior = store
2186                .load_version(observed_version)
2187                .await
2188                .map_err(|error| {
2189                    DbError::Checkpoint(format!(
2190                        "failed to load assignment {observed_version} process roster: {error}"
2191                    ))
2192                })?
2193                .ok_or_else(|| {
2194                    DbError::Checkpoint(format!(
2195                        "assignment {observed_version} process roster is unavailable"
2196                    ))
2197                })?;
2198            crate::rebalance::audit_assignment_snapshot_authority(
2199                &store,
2200                controller.as_deref(),
2201                &prior,
2202            )
2203            .await
2204            .map_err(|error| {
2205                DbError::Checkpoint(format!(
2206                    "assignment {observed_version} authority audit failed: {error}"
2207                ))
2208            })?;
2209            prior
2210                .to_vnode_vec(vnode_count)
2211                .map_err(|error| DbError::Checkpoint(error.to_string()))?;
2212            prior
2213                .participants
2214                .binary_search_by_key(&self_id.0, |participant| participant.node_id)
2215                .ok()
2216                .and_then(|index| prior.participants.get(index))
2217                .map(|participant| participant.boot_incarnation)
2218                != Some(incarnation)
2219        } else if current_incarnation.is_some() {
2220            return Err(DbError::Checkpoint(format!(
2221                "cannot verify local state incarnation for assignment {observed_version} without durable assignment history"
2222            )));
2223        } else {
2224            false
2225        };
2226        // Hold the coord mutex so registry + fence updates land between epochs.
2227        let guard = self.coordinator.lock().await;
2228        // Re-check under the lock: a concurrent adopt may have advanced the version,
2229        // which we must not regress.
2230        if snapshot.version <= registry.assignment_version() {
2231            return Ok(SnapshotAdoption {
2232                adopted: false,
2233                version: snapshot.version,
2234                ..SnapshotAdoption::default()
2235            });
2236        }
2237
2238        let prepared_assignment = registry.versioned_snapshot();
2239        let prepared_from_version = prepared_assignment.version();
2240        if prepared_from_version != observed_version {
2241            return Err(DbError::Checkpoint(format!(
2242                "[LDB-6053] assignment base advanced from {observed_version} to {prepared_from_version} while preparing target {}",
2243                snapshot.version
2244            )));
2245        }
2246        let old_owned = owned_vnode_indices(prepared_assignment.owners(), self_id)?;
2247        let old_set: std::collections::HashSet<u32> = old_owned.iter().copied().collect();
2248        let new_owned = owned_vnode_indices(&new_assignment, self_id)?;
2249        let skipped_assignment_generation =
2250            snapshot.version > prepared_from_version.saturating_add(1);
2251        // Compute from the new assignment before publishing it, so the Restoring marks
2252        // below land before the ownership flip.
2253        let newly_acquired: Vec<u32> = (0..vnode_count)
2254            .filter(|&v| {
2255                new_assignment.get(v as usize).copied() == Some(self_id)
2256                    && (force_local_restore
2257                        || skipped_assignment_generation
2258                        || !old_set.contains(&v))
2259            })
2260            .collect();
2261        let source_handoff_required = !newly_acquired.is_empty();
2262
2263        // Snapshot immutable recovery handles at the epoch boundary, then release the coordinator
2264        // mutex before decision-store, seal, readiness, and vnode reads. Remote object-store
2265        // latency must not stall checkpoint admission.
2266        let handoff_reader = if !source_handoff_required {
2267            None
2268        } else if let Some(coord) = guard.as_ref() {
2269            Some(coord.cluster_handoff_reader()?.ok_or_else(|| {
2270                DbError::Checkpoint(format!(
2271                    "[LDB-6052] assignment {} requires an active cluster recovery namespace",
2272                    snapshot.version
2273                ))
2274            })?)
2275        } else {
2276            return Err(DbError::Checkpoint(format!(
2277                "[LDB-6052] cannot acquire {} vnodes for assignment {} without a live checkpoint coordinator",
2278                newly_acquired.len(), snapshot.version
2279            )));
2280        };
2281
2282        drop(guard);
2283        // Stage the sealed source offsets and read all newly-owned state before publishing the
2284        // assignment. Any failure leaves the current version intact and retryable.
2285        let source_handoff = if let Some(reader) = handoff_reader.as_ref() {
2286            reader.acquired_source_handoff().await.map_err(|e| {
2287                DbError::Checkpoint(format!(
2288                    "[LDB-6052] source-offset handoff read failed for assignment {}: {e}",
2289                    snapshot.version
2290                ))
2291            })?
2292        } else {
2293            None
2294        };
2295        let prepared_outcome = source_handoff
2296            .as_ref()
2297            .map(|handoff| handoff.outcome.clone());
2298        if let Some(outcome) = prepared_outcome.as_ref() {
2299            let outcome_fence = outcome.assignment_fence.as_ref().ok_or_else(|| {
2300                DbError::Checkpoint(
2301                    "[LDB-6054] cluster source handoff Commit outcome has no assignment certificate"
2302                        .into(),
2303                )
2304            })?;
2305            if outcome_fence.assignment_version > snapshot.version {
2306                return Err(DbError::Checkpoint(format!(
2307                    "[LDB-6054] assignment {} is older than durable checkpoint fence {}; refresh the assignment snapshot before adoption",
2308                    snapshot.version, outcome_fence.assignment_version
2309                )));
2310            }
2311        }
2312        let mut rehydration = if let Some(handoff) = source_handoff
2313            .as_ref()
2314            .filter(|_| !newly_acquired.is_empty())
2315        {
2316            let backend = self.state_backend.lock().clone().ok_or_else(|| {
2317                DbError::Checkpoint(
2318                    "[LDB-6050] cluster assignment adoption requires a state backend".into(),
2319                )
2320            })?;
2321            let attempt = laminar_core::state::CheckpointAttempt::new(
2322                handoff.outcome.epoch,
2323                handoff.outcome.checkpoint_id,
2324            );
2325            crate::recovery_manager::VnodeRehydrator::new(backend.as_ref())
2326                .rehydrate_at(&newly_acquired, attempt)
2327                .await?
2328        } else {
2329            crate::recovery_manager::VnodeRehydration::default()
2330        };
2331
2332        let observed_outcome = if let Some(reader) = handoff_reader.as_ref() {
2333            reader.highest_commit_outcome().await?
2334        } else {
2335            None
2336        };
2337
2338        // Re-acquire the epoch boundary and discard the prepared adoption if another rotation won,
2339        // the coordinator namespace changed, or a newer durable cut appeared during the reads.
2340        let _execution_guard = Arc::clone(&self.rotation_execution_fence)
2341            .write_owned()
2342            .await;
2343        let mut guard = self.coordinator.lock().await;
2344        if tokio::time::Instant::now() >= deadline {
2345            return Err(DbError::Checkpoint(format!(
2346                "assignment {} adoption reached its deadline before publication",
2347                snapshot.version
2348            )));
2349        }
2350        let current_version = registry.assignment_version();
2351        if snapshot.version <= current_version {
2352            return Ok(SnapshotAdoption {
2353                adopted: false,
2354                version: snapshot.version,
2355                ..SnapshotAdoption::default()
2356            });
2357        }
2358        if current_version != prepared_from_version {
2359            return Err(DbError::Checkpoint(format!(
2360                "[LDB-6053] assignment base advanced from {prepared_from_version} to {current_version} while preparing target {}; retrying from the new owner set",
2361                snapshot.version
2362            )));
2363        }
2364        if source_handoff_required {
2365            let current_reader = guard
2366                .as_ref()
2367                .ok_or_else(|| {
2368                    DbError::Checkpoint(format!(
2369                        "[LDB-6052] checkpoint coordinator disappeared while preparing assignment {}",
2370                        snapshot.version
2371                    ))
2372                })?
2373                .cluster_handoff_reader()?
2374                .ok_or_else(|| {
2375                    DbError::Checkpoint(format!(
2376                        "[LDB-6052] cluster recovery namespace disappeared while preparing assignment {}",
2377                        snapshot.version
2378                    ))
2379                })?;
2380            let prepared_reader = handoff_reader.as_ref().ok_or_else(|| {
2381                DbError::Checkpoint(format!(
2382                    "[LDB-6052] assignment {} lost its prepared cluster recovery namespace",
2383                    snapshot.version
2384                ))
2385            })?;
2386            if !prepared_reader.same_namespace(&current_reader) {
2387                return Err(DbError::Checkpoint(format!(
2388                    "[LDB-6053] checkpoint recovery namespace changed while preparing assignment {}; retrying the complete source/state handoff",
2389                    snapshot.version
2390                )));
2391            }
2392            if observed_outcome != prepared_outcome {
2393                return Err(DbError::Checkpoint(format!(
2394                    "[LDB-6053] durable checkpoint Commit outcome advanced while preparing assignment {}; retrying the complete source/state handoff",
2395                    snapshot.version
2396                )));
2397            }
2398        }
2399        if let Some(handoff) = source_handoff.as_ref() {
2400            let expected_attempt = laminar_core::state::CheckpointAttempt::new(
2401                handoff.outcome.epoch,
2402                handoff.outcome.checkpoint_id,
2403            );
2404            if handoff.sources.attempt() != expected_attempt {
2405                return Err(DbError::Checkpoint(format!(
2406                    "[LDB-6054] committed source handoff {:?} does not match durable outcome {expected_attempt:?}",
2407                    handoff.sources.attempt()
2408                )));
2409            }
2410            for (source_name, _) in handoff.sources.sources() {
2411                if self.catalog.get_source(source_name).is_none() {
2412                    return Err(DbError::Checkpoint(format!(
2413                        "[LDB-6054] committed source handoff names unknown catalog source '{source_name}'"
2414                    )));
2415                }
2416            }
2417            let controller = controller.as_ref().ok_or_else(|| {
2418                DbError::Checkpoint(
2419                    "[LDB-6054] committed source handoff has no live cluster controller".into(),
2420                )
2421            })?;
2422            match (
2423                controller.cluster_min_watermark(),
2424                handoff.sources.recovery_watermark_frontier(),
2425            ) {
2426                (Some(current), Some(recovered)) if current > recovered => {
2427                    return Err(DbError::Checkpoint(format!(
2428                        "[LDB-6054] live committed cluster watermark {current} is ahead of source handoff frontier {recovered}"
2429                    )));
2430                }
2431                (Some(current), None) => {
2432                    return Err(DbError::Checkpoint(format!(
2433                        "[LDB-6054] {:?} source handoff without a numeric frontier cannot replace committed cluster watermark {current}",
2434                        handoff.sources.cluster_watermark()
2435                    )));
2436                }
2437                _ => {}
2438            }
2439        }
2440        let new_set: std::collections::HashSet<u32> = new_owned.iter().copied().collect();
2441        let revoked: Vec<u32> = old_set.difference(&new_set).copied().collect();
2442        let rehydration_attempt = rehydration.attempt;
2443        let adoption = SnapshotAdoption {
2444            adopted: true,
2445            version: snapshot.version,
2446            newly_acquired: newly_acquired.clone(),
2447            rehydrated: rehydration.restored.len(),
2448            rehydration_epoch: rehydration_attempt.map(|attempt| attempt.epoch),
2449        };
2450
2451        // Stage revoke and rehydration work while the compute-cycle write fence is held, then
2452        // publish ownership before releasing either staging mutex. The next cycle must therefore
2453        // apply both maps before any local or shuffled row can observe the new owner set.
2454        let mut pending_revoke = self.pending_revoke_vnodes.lock();
2455        let mut staged_rehydration = self.rehydrated_vnode_state.lock();
2456        pending_revoke.extend(revoked);
2457        staged_rehydration.retain(|vnode, _| new_set.contains(vnode));
2458        if let Some(attempt) = rehydration_attempt {
2459            for vnode in &newly_acquired {
2460                staged_rehydration.insert(
2461                    *vnode,
2462                    RehydratedVnode {
2463                        epoch: attempt.epoch,
2464                        chain: rehydration.restored.remove(vnode).unwrap_or_default(),
2465                    },
2466                );
2467            }
2468            registry.mark_restoring(&newly_acquired);
2469        }
2470
2471        if let Some(watermark) = source_handoff
2472            .as_ref()
2473            .and_then(|handoff| handoff.sources.recovery_watermark_frontier())
2474        {
2475            controller
2476                .as_ref()
2477                .expect("validated source handoff has a cluster controller")
2478                .publish_cluster_min_watermark(watermark);
2479        }
2480        if let Some(handoff) = source_handoff {
2481            registry.set_assignment_and_version_with_source_handoff(
2482                new_assignment,
2483                snapshot.version,
2484                handoff.sources,
2485            );
2486        } else if source_handoff_required {
2487            // Genesis has no committed cut. Keep that distinct from a committed
2488            // empty cut so sources may use their start-captured numeric baseline.
2489            registry.set_assignment_and_version(new_assignment, snapshot.version);
2490            registry.mark_active(&newly_acquired);
2491        } else {
2492            registry.set_assignment_and_version_carrying_source_handoff(
2493                new_assignment,
2494                snapshot.version,
2495            );
2496        }
2497        if let Some(backend) = self.state_backend.lock().clone() {
2498            backend.set_authoritative_version(snapshot.version);
2499        }
2500        if let Some(coord) = guard.as_mut() {
2501            coord.set_assignment_version(snapshot.version);
2502            coord.set_vnode_set(new_owned.clone());
2503            coord.set_gate_vnode_set((0..vnode_count).collect());
2504        }
2505        if let Some(incarnation) = current_incarnation {
2506            *self.local_state_incarnation.lock() = Some(incarnation);
2507        }
2508        drop(staged_rehydration);
2509        drop(pending_revoke);
2510        drop(guard);
2511
2512        tracing::info!(
2513            version = snapshot.version,
2514            newly_acquired = adoption.newly_acquired.len(),
2515            rehydrated = adoption.rehydrated,
2516            rehydration_epoch = ?adoption.rehydration_epoch,
2517            "adopted assignment snapshot",
2518        );
2519        Ok(adoption)
2520    }
2521
2522    /// Wait for every local source to publish an exact FIFO drain receipt.
2523    #[cfg(feature = "cluster")]
2524    pub(crate) async fn prepare_local_source_drain(
2525        &self,
2526        transition: &laminar_core::checkpoint::AssignmentDrainTransition,
2527        participant: laminar_core::checkpoint::CheckpointParticipant,
2528        deadline: tokio::time::Instant,
2529    ) -> Result<(), DbError> {
2530        crate::pipeline::streaming_coordinator::prepare_owned_source_drain(
2531            &self.owned_source_tasks,
2532            transition,
2533            participant,
2534            deadline,
2535        )
2536        .await
2537        .map_err(|error| DbError::Checkpoint(format!("source drain failed: {error}")))
2538    }
2539
2540    /// Resolve the exact local source drain after target commit or abort.
2541    #[cfg(feature = "cluster")]
2542    pub(crate) async fn resolve_local_source_drain(
2543        &self,
2544        round: laminar_core::checkpoint::AssignmentDrainId,
2545        outcome: laminar_connectors::connector::SourceDrainOutcome,
2546        deadline: tokio::time::Instant,
2547    ) -> Result<(), DbError> {
2548        crate::pipeline::streaming_coordinator::resolve_owned_source_drain(
2549            &self.owned_source_tasks,
2550            laminar_connectors::connector::SourceDrainResolution { round, outcome },
2551            deadline,
2552        )
2553        .await
2554        .map_err(|error| DbError::Checkpoint(format!("source drain resolution failed: {error}")))
2555    }
2556
2557    /// Validate a draining generation without changing local ownership. Source intake is held by
2558    /// the source-task drain protocol until the durable outcome is resolved.
2559    #[cfg(feature = "cluster")]
2560    pub(crate) fn validate_source_drain_snapshot(
2561        &self,
2562        snapshot: &laminar_core::cluster::control::AssignmentSnapshot,
2563    ) -> Result<(), DbError> {
2564        if !snapshot.draining {
2565            return Err(DbError::Checkpoint(format!(
2566                "assignment {} is not a draining generation",
2567                snapshot.version
2568            )));
2569        }
2570        if !snapshot.has_canonical_participants() {
2571            return Err(DbError::Checkpoint(format!(
2572                "draining assignment {} has no canonical process roster",
2573                snapshot.version
2574            )));
2575        }
2576        let registry = self.vnode_registry.lock().clone().ok_or_else(|| {
2577            DbError::Checkpoint(format!(
2578                "draining assignment {} cannot be validated without a vnode registry",
2579                snapshot.version
2580            ))
2581        })?;
2582        let expected_version = registry
2583            .assignment_version()
2584            .checked_add(1)
2585            .ok_or_else(|| DbError::Checkpoint("assignment version overflow".into()))?;
2586        if snapshot.version != expected_version {
2587            return Err(DbError::Checkpoint(format!(
2588                "draining assignment {} is not the exact successor of local assignment {}",
2589                snapshot.version,
2590                registry.assignment_version()
2591            )));
2592        }
2593        snapshot
2594            .to_vnode_vec(registry.vnode_count())
2595            .map_err(|error| DbError::Checkpoint(error.to_string()))?;
2596        tracing::info!(
2597            version = snapshot.version,
2598            "validated global source-drain generation"
2599        );
2600        Ok(())
2601    }
2602
2603    /// Staged vnode state from the most recent rebalance adoptions, keyed by vnode.
2604    #[cfg(feature = "cluster")]
2605    #[must_use]
2606    pub fn rehydrated_vnode_state(&self) -> HashMap<u32, RehydratedVnode> {
2607        self.rehydrated_vnode_state.lock().clone()
2608    }
2609
2610    #[cfg(feature = "cluster")]
2611    pub(crate) fn set_cluster_controller(
2612        &self,
2613        controller: Arc<laminar_core::cluster::control::ClusterController>,
2614    ) -> Result<(), DbError> {
2615        if !self.is_cluster_runtime() {
2616            return Err(DbError::Config(
2617                "cluster controller cannot be installed on a local runtime".into(),
2618            ));
2619        }
2620        *self.cluster_controller.lock() = Some(controller);
2621        Ok(())
2622    }
2623
2624    pub(crate) fn set_state_backend(&self, backend: Arc<dyn laminar_core::state::StateBackend>) {
2625        *self.state_backend.lock() = Some(backend);
2626    }
2627
2628    pub(crate) fn set_vnode_registry(&self, registry: Arc<laminar_core::state::VnodeRegistry>) {
2629        *self.vnode_registry.lock() = Some(registry);
2630    }
2631
2632    /// Collect this node's physical table slice without invoking distributed fan-out.
2633    ///
2634    /// This is intended for cluster diagnostics and placement validation. It deliberately returns
2635    /// immutable batches rather than exposing the mutable `DataFusion` catalog.
2636    ///
2637    /// # Errors
2638    /// Returns an error when the local table cannot be resolved, planned, or collected.
2639    #[cfg(feature = "cluster")]
2640    pub async fn collect_local_table(&self, name: &str) -> Result<Vec<RecordBatch>, DbError> {
2641        const LOCAL_SCAN_NAME: &str = "__laminar_local_table_scan";
2642
2643        let _catalog_guard = self.topology_ddl_lock.read().await;
2644        let provider = self.ctx.table_provider(exact_table_reference(name)).await?;
2645        let context = SessionContext::new();
2646        context.register_table(exact_table_reference(LOCAL_SCAN_NAME), provider)?;
2647        Ok(context
2648            .sql(&format!("SELECT * FROM {LOCAL_SCAN_NAME}"))
2649            .await?
2650            .collect()
2651            .await?)
2652    }
2653
2654    /// Returns a fluent builder for constructing a [`LaminarDB`].
2655    #[must_use]
2656    pub fn builder() -> LaminarDbBuilder {
2657        LaminarDbBuilder::new()
2658    }
2659
2660    #[allow(unused_variables)]
2661    fn register_builtin_connectors(
2662        registry: &laminar_connectors::registry::ConnectorRegistry,
2663    ) -> Result<(), laminar_connectors::error::ConnectorError> {
2664        laminar_connectors::generator::register_generator_source(registry)?;
2665        #[cfg(feature = "kafka")]
2666        {
2667            laminar_connectors::kafka::register_kafka_source(registry)?;
2668            laminar_connectors::kafka::register_kafka_sink(registry)?;
2669        }
2670        #[cfg(feature = "postgres-cdc")]
2671        {
2672            laminar_connectors::postgres::register_postgres_cdc_source(registry)?;
2673        }
2674        #[cfg(feature = "postgres-sink")]
2675        {
2676            laminar_connectors::postgres::register_postgres_sink(registry)?;
2677        }
2678        #[cfg(feature = "delta-lake")]
2679        {
2680            laminar_connectors::lakehouse::register_delta_lake_sink(registry)?;
2681            laminar_connectors::lakehouse::register_delta_lake_source(registry)?;
2682        }
2683        #[cfg(feature = "iceberg")]
2684        {
2685            laminar_connectors::lakehouse::register_iceberg_sink(registry)?;
2686            laminar_connectors::lakehouse::register_iceberg_source(registry)?;
2687        }
2688        #[cfg(feature = "websocket")]
2689        {
2690            laminar_connectors::websocket::register_websocket_source(registry)?;
2691            laminar_connectors::websocket::register_websocket_sink(registry)?;
2692        }
2693        #[cfg(feature = "mongodb-cdc")]
2694        {
2695            laminar_connectors::mongodb::register_mongodb_cdc_source(registry)?;
2696            laminar_connectors::mongodb::register_mongodb_sink(registry)?;
2697        }
2698        #[cfg(feature = "files")]
2699        {
2700            laminar_connectors::files::register_file_source(registry)?;
2701            laminar_connectors::files::register_file_sink(registry)?;
2702        }
2703        #[cfg(feature = "otel")]
2704        {
2705            laminar_connectors::otel::register_otel_source(registry)?;
2706        }
2707        #[cfg(feature = "nats")]
2708        {
2709            laminar_connectors::nats::register_nats_source(registry)?;
2710            laminar_connectors::nats::register_nats_sink(registry)?;
2711        }
2712        Ok(())
2713    }
2714
2715    fn handle_register_lookup_table(
2716        &self,
2717        info: laminar_sql::planner::LookupTableInfo,
2718    ) -> Result<ExecuteResult, DbError> {
2719        use laminar_sql::parser::lookup_table::LookupConnector;
2720
2721        self.preflight_lookup_connector(&info.properties)?;
2722        if info.primary_key.len() != 1 {
2723            return Err(DbError::InvalidOperation(
2724                "Lookup table requires a single-column primary key".into(),
2725            ));
2726        }
2727        let pk = info.primary_key[0].clone();
2728
2729        self.table_store
2730            .write()
2731            .create_table(&info.name, info.arrow_schema.clone(), &pk)?;
2732
2733        if matches!(&info.properties.connector, LookupConnector::External(_)) {
2734            self.register_lookup_connector(&info, &pk);
2735        }
2736
2737        {
2738            let provider = crate::table_provider::ReferenceTableProvider::new(
2739                info.name.clone(),
2740                info.arrow_schema.clone(),
2741                self.table_store.clone(),
2742            );
2743            match self
2744                .ctx
2745                .register_table(exact_table_reference(&info.name), Arc::new(provider))
2746            {
2747                Ok(None) => {}
2748                Ok(Some(previous)) => {
2749                    let _ = self
2750                        .ctx
2751                        .register_table(exact_table_reference(&info.name), previous);
2752                    return Err(DbError::InvalidOperation(format!(
2753                        "cannot create lookup table '{}': its provider was claimed concurrently",
2754                        info.name
2755                    )));
2756                }
2757                Err(error) => {
2758                    return Err(DbError::InvalidOperation(format!(
2759                        "failed to register lookup table '{}': {error}",
2760                        info.name
2761                    )));
2762                }
2763            }
2764        }
2765
2766        if let Some(batch) = self.table_store.read().to_record_batch(&info.name)? {
2767            self.lookup_registry.register(
2768                &info.name,
2769                laminar_sql::datafusion::LookupSnapshot { batch },
2770            );
2771        }
2772
2773        self.refresh_lookup_optimizer_rule();
2774
2775        Ok(ExecuteResult::Ddl(DdlInfo {
2776            statement_type: "CREATE LOOKUP TABLE".to_string(),
2777            object_name: info.name,
2778            applied: true,
2779        }))
2780    }
2781
2782    fn preflight_lookup_connector(
2783        &self,
2784        properties: &laminar_sql::parser::lookup_table::LookupTableProperties,
2785    ) -> Result<(), DbError> {
2786        use laminar_sql::parser::lookup_table::{LookupConnector, LookupStrategy};
2787
2788        if properties.strategy == LookupStrategy::OnDemand
2789            && self.config.delivery_guarantee
2790                == laminar_connectors::connector::DeliveryGuarantee::ExactlyOnce
2791        {
2792            return Err(DbError::InvalidOperation(
2793                "on-demand LOOKUP TABLE is incompatible with exactly-once delivery because \
2794                 external lookup results are not checkpointed"
2795                    .into(),
2796            ));
2797        }
2798        let connector = match &properties.connector {
2799            LookupConnector::Static => {
2800                if properties.strategy == LookupStrategy::OnDemand {
2801                    return Err(DbError::InvalidOperation(
2802                        "static LOOKUP TABLE supports only the replicated strategy".into(),
2803                    ));
2804                }
2805                return Ok(());
2806            }
2807            LookupConnector::External(name) => name,
2808        };
2809        let (available, capability) = match properties.strategy {
2810            LookupStrategy::Replicated => (
2811                self.connector_registry.has_table_source(connector),
2812                "snapshot-capable table source",
2813            ),
2814            LookupStrategy::OnDemand => (
2815                self.connector_registry.has_lookup_source(connector),
2816                "on-demand lookup source",
2817            ),
2818        };
2819        if !available {
2820            return Err(DbError::InvalidOperation(format!(
2821                "LOOKUP TABLE connector '{connector}' has no registered {capability} required \
2822                 by strategy '{}'",
2823                properties.strategy
2824            )));
2825        }
2826        Ok(())
2827    }
2828
2829    fn register_lookup_connector(&self, info: &laminar_sql::planner::LookupTableInfo, pk: &str) {
2830        use laminar_sql::parser::lookup_table::LookupConnector;
2831
2832        let connector_type = match &info.properties.connector {
2833            LookupConnector::External(name) => name.clone(),
2834            LookupConnector::Static => unreachable!(),
2835        };
2836
2837        self.table_store
2838            .write()
2839            .set_connector(&info.name, &connector_type);
2840
2841        // Keys consumed by LookupTableProperties are excluded; "format.*" keys
2842        // go to format_options with the prefix stripped.
2843        let consumed = [
2844            "connector",
2845            "strategy",
2846            "cache.memory",
2847            "cache.ttl",
2848            "pushdown",
2849            "format",
2850        ];
2851        let mut connector_options = HashMap::with_capacity(info.raw_options.len());
2852        let mut format_options = HashMap::with_capacity(4);
2853        for (k, v) in &info.raw_options {
2854            let lower = k.to_lowercase();
2855            if consumed.contains(&lower.as_str()) {
2856                continue;
2857            }
2858            if let Some(suffix) = lower.strip_prefix("format.") {
2859                format_options.insert(suffix.to_string(), v.clone());
2860            } else {
2861                connector_options.insert(k.clone(), v.clone());
2862            }
2863        }
2864
2865        // Carry as bytes; the partial lookup cache is byte-weighted, not entry-counted.
2866        let cache_max_bytes = info
2867            .properties
2868            .cache_memory
2869            .map(|m| usize::try_from(m.as_bytes()).unwrap_or(usize::MAX));
2870
2871        let cache_ttl = info
2872            .properties
2873            .cache_ttl
2874            .map(std::time::Duration::from_secs);
2875
2876        self.connector_manager
2877            .lock()
2878            .register_table(crate::connector_manager::TableRegistration {
2879                name: info.name.clone(),
2880                primary_key: pk.to_string(),
2881                connector_type: Some(connector_type),
2882                connector_options,
2883                format: info.raw_options.get("format").cloned(),
2884                format_options,
2885                on_demand: matches!(
2886                    info.properties.strategy,
2887                    laminar_sql::parser::lookup_table::LookupStrategy::OnDemand
2888                ),
2889                cache_max_bytes,
2890                cache_ttl,
2891            });
2892    }
2893
2894    /// Rebuild the lookup optimizer rules for the current set of registered tables.
2895    pub(crate) fn refresh_lookup_optimizer_rule(&self) {
2896        use laminar_sql::planner::lookup_join::{LookupColumnPruningRule, LookupJoinRewriteRule};
2897        use laminar_sql::planner::predicate_split::{
2898            PlanPushdownMode, PlanSourceCapabilities, PredicateSplitterRule,
2899            SourceCapabilitiesRegistry,
2900        };
2901
2902        self.ctx.remove_optimizer_rule("lookup_join_rewrite");
2903        self.ctx.remove_optimizer_rule("predicate_splitter");
2904        self.ctx.remove_optimizer_rule("lookup_column_pruning");
2905
2906        let tables = self.planner.lock().lookup_tables_cloned();
2907        if tables.is_empty() {
2908            return;
2909        }
2910
2911        let mut caps_registry = SourceCapabilitiesRegistry::default();
2912        for (name, info) in &tables {
2913            let mode = match info.properties.pushdown_mode {
2914                laminar_sql::parser::lookup_table::PushdownMode::Enabled
2915                | laminar_sql::parser::lookup_table::PushdownMode::Auto => PlanPushdownMode::Full,
2916                laminar_sql::parser::lookup_table::PushdownMode::Disabled => PlanPushdownMode::None,
2917            };
2918            let pk_set: std::collections::HashSet<String> =
2919                info.primary_key.iter().cloned().collect();
2920            caps_registry.register(
2921                name.clone(),
2922                PlanSourceCapabilities {
2923                    pushdown_mode: mode,
2924                    eq_columns: pk_set,
2925                    range_columns: std::collections::HashSet::new(),
2926                    in_columns: std::collections::HashSet::new(),
2927                    supports_null_check: false,
2928                },
2929            );
2930        }
2931
2932        self.ctx
2933            .add_optimizer_rule(Arc::new(LookupJoinRewriteRule::new(tables)));
2934        self.ctx
2935            .add_optimizer_rule(Arc::new(PredicateSplitterRule::new(caps_registry)));
2936        self.ctx
2937            .add_optimizer_rule(Arc::new(LookupColumnPruningRule));
2938    }
2939
2940    /// Returns the frozen connector registry for factory lookup and deployment introspection.
2941    /// Custom factories must be registered through [`LaminarDbBuilder::register_connector`].
2942    #[must_use]
2943    pub fn connector_registry(&self) -> &laminar_connectors::registry::ConnectorRegistry {
2944        &self.connector_registry
2945    }
2946
2947    /// Register a custom scalar UDF. Called by the builder after construction.
2948    pub(crate) fn register_custom_udf(&self, udf: datafusion_expr::ScalarUDF) {
2949        self.ctx.register_udf(udf);
2950    }
2951
2952    /// Register a custom aggregate UDF. Called by the builder after construction.
2953    pub(crate) fn register_custom_udaf(&self, udaf: datafusion_expr::AggregateUDF) {
2954        self.ctx.register_udaf(udaf);
2955    }
2956
2957    /// Execute a SQL statement.
2958    ///
2959    /// # Errors
2960    ///
2961    /// Returns `DbError` if SQL parsing, planning, or execution fails.
2962    pub async fn execute(&self, sql: &str) -> Result<ExecuteResult, DbError> {
2963        if self.shutdown.load(std::sync::atomic::Ordering::Relaxed) {
2964            return Err(DbError::Shutdown);
2965        }
2966
2967        let stmts = sql_utils::split_statements(sql);
2968        if stmts.is_empty() {
2969            return Err(DbError::InvalidOperation("Empty SQL statement".into()));
2970        }
2971
2972        let mut last_result = None;
2973        for stmt_sql in &stmts {
2974            last_result = Some(self.execute_single(stmt_sql).await?);
2975        }
2976
2977        last_result.ok_or_else(|| DbError::InvalidOperation("Empty SQL statement".into()))
2978    }
2979
2980    /// Apply and durably seal the complete startup catalog as one immutable batch.
2981    /// Existing sealed catalogs accept exact replay/no-op definitions only.
2982    ///
2983    /// # Errors
2984    /// Returns an error for a partial or divergent bootstrap, unsafe catalog mutation, lost leader
2985    /// authority, or manifest sealing failure. Local creates are rolled back on every error.
2986    #[cfg(feature = "cluster")]
2987    pub async fn execute_cluster_bootstrap_batch(
2988        &self,
2989        sql: &[String],
2990    ) -> Result<Vec<ExecuteResult>, DbError> {
2991        self.ensure_catalog_cleanup_unfenced("cluster catalog bootstrap")?;
2992        self.connector_registry.freeze();
2993        if self.shutdown.load(std::sync::atomic::Ordering::Relaxed) {
2994            return Err(DbError::Shutdown);
2995        }
2996        if DbState::load(&self.state) != DbState::Created {
2997            return Err(DbError::InvalidOperation(
2998                "cluster catalog bootstrap is only valid before pipeline startup".into(),
2999            ));
3000        }
3001
3002        let mut parsed = Vec::new();
3003        for batch_entry in sql {
3004            for stmt_sql in sql_utils::split_statements(batch_entry) {
3005                let mut statements = parse_streaming_sql(stmt_sql)?;
3006                if statements.len() != 1 {
3007                    return Err(DbError::InvalidOperation(
3008                        "cluster bootstrap entries must contain exactly one SQL statement".into(),
3009                    ));
3010                }
3011                let statement = statements.pop().ok_or_else(|| {
3012                    DbError::InvalidOperation(
3013                        "cluster bootstrap entries must contain exactly one SQL statement".into(),
3014                    )
3015                })?;
3016                let (name, kind, _) = validate_cluster_catalog_create(stmt_sql, &statement)?;
3017                parsed.push((stmt_sql.to_owned(), statement, name, kind));
3018            }
3019        }
3020        {
3021            let mut names = std::collections::HashSet::with_capacity(parsed.len());
3022            for (_, _, name, _) in &parsed {
3023                if !names.insert(name.as_str()) {
3024                    return Err(DbError::InvalidOperation(format!(
3025                        "cluster bootstrap defines '{name}' more than once"
3026                    )));
3027                }
3028            }
3029        }
3030
3031        let _topology_ddl = self.topology_ddl_lock.write().await;
3032        self.ensure_catalog_cleanup_unfenced("cluster catalog bootstrap")?;
3033        self.ensure_coordinated_recovery_mutation_unfenced("cluster catalog bootstrap")?;
3034        if DbState::load(&self.state) != DbState::Created {
3035            return Err(DbError::InvalidOperation(
3036                "cluster catalog bootstrap is only valid before pipeline startup".into(),
3037            ));
3038        }
3039
3040        if let Some(manifest) = self.restore_catalog_from_manifest().await? {
3041            let requested = parsed
3042                .iter()
3043                .map(|(ddl, _, canonical_name, kind)| {
3044                    laminar_core::cluster::control::CatalogManifestEntry {
3045                        canonical_name: canonical_name.clone(),
3046                        kind: *kind,
3047                        ddl: ddl.clone(),
3048                    }
3049                })
3050                .collect::<Vec<_>>();
3051            if requested.as_slice() != manifest.entries.as_slice() {
3052                return Err(DbError::Pipeline(format!(
3053                    "configured cluster catalog must exactly match the complete ordered sealed inventory (configured entries: {}, sealed entries: {})",
3054                    requested.len(),
3055                    manifest.entries.len()
3056                )));
3057            }
3058            let mut results = Vec::with_capacity(parsed.len());
3059            for (stmt_sql, statement, name, _) in &parsed {
3060                let result = self
3061                    .exact_bootstrap_noop(stmt_sql, statement)?
3062                    .ok_or_else(|| {
3063                        DbError::Pipeline(format!(
3064                            "sealed cluster catalog rejects startup addition '{name}'"
3065                        ))
3066                    })?;
3067                results.push(result);
3068            }
3069            return Ok(results);
3070        }
3071
3072        if !self.catalog_manifest_inventory()?.is_empty() {
3073            return Err(DbError::Pipeline(
3074                "cannot seal a new cluster catalog over uncommitted local topology".into(),
3075            ));
3076        }
3077        let store = self.catalog_manifest_store.lock().clone().ok_or_else(|| {
3078            DbError::Pipeline("cluster catalog manifest store is not configured".into())
3079        })?;
3080        let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
3081            DbError::Pipeline(
3082                "[LDB-6043] cluster catalog bootstrap requires a cluster controller".into(),
3083            )
3084        })?;
3085        let leader_proof = controller
3086            .capture_catalog_bootstrap_proof()
3087            .ok_or_else(|| {
3088                DbError::Pipeline(
3089                    "[LDB-6043] cluster catalog bootstrap requires the active durable leader lease"
3090                        .into(),
3091                )
3092            })?;
3093        self.validate_catalog_seal_authority(Some(&leader_proof))?;
3094
3095        let mut bootstrap_guard = CatalogBootstrapGuard {
3096            db: self,
3097            created: Vec::with_capacity(parsed.len()),
3098            sealed: false,
3099        };
3100        let mut results = Vec::with_capacity(parsed.len());
3101        for (stmt_sql, statement, name, kind) in &parsed {
3102            let result = CATALOG_BOOTSTRAP
3103                .scope((), self.execute_parsed_single(stmt_sql, statement))
3104                .await?;
3105            let ExecuteResult::Ddl(info) = &result else {
3106                return Err(DbError::Pipeline(format!(
3107                    "cluster catalog create '{name}' returned a non-DDL result"
3108                )));
3109            };
3110            if !info.applied || info.object_name != *name {
3111                return Err(DbError::Pipeline(format!(
3112                    "cluster catalog create '{name}' did not apply exactly once"
3113                )));
3114            }
3115            bootstrap_guard.record(name.clone(), *kind);
3116            results.push(result);
3117        }
3118
3119        let manifest = laminar_core::cluster::control::CatalogManifest::new(
3120            self.catalog_manifest_inventory()?,
3121        )
3122        .map_err(|error| {
3123            DbError::Pipeline(format!("invalid cluster catalog inventory: {error}"))
3124        })?;
3125
3126        #[cfg(test)]
3127        let catalog_seal_gate = { self.catalog_seal_gate.lock().clone() };
3128        #[cfg(test)]
3129        if let Some((entered, release)) = catalog_seal_gate {
3130            entered.notify_one();
3131            release.notified().await;
3132        }
3133        self.validate_catalog_seal_authority(Some(&leader_proof))?;
3134        store
3135            .seal(&manifest, &leader_proof)
3136            .await
3137            .map_err(|error| DbError::Pipeline(format!("catalog manifest seal failed: {error}")))?;
3138        bootstrap_guard.sealed();
3139        Ok(results)
3140    }
3141
3142    /// Apply one startup catalog definition and seal it as the complete inventory.
3143    /// Prefer [`Self::execute_cluster_bootstrap_batch`] for server configuration.
3144    ///
3145    /// # Errors
3146    /// Returns an error when the definition is invalid or the catalog batch cannot be sealed.
3147    #[cfg(feature = "cluster")]
3148    pub async fn execute_cluster_bootstrap(&self, sql: &str) -> Result<ExecuteResult, DbError> {
3149        let entries = vec![sql.to_owned()];
3150        self.execute_cluster_bootstrap_batch(&entries)
3151            .await?
3152            .into_iter()
3153            .last()
3154            .ok_or_else(|| DbError::InvalidOperation("Empty SQL statement".into()))
3155    }
3156
3157    async fn execute_single(&self, sql: &str) -> Result<ExecuteResult, DbError> {
3158        let statements = parse_streaming_sql(sql)?;
3159        if statements.is_empty() {
3160            return Err(DbError::InvalidOperation("Empty SQL statement".into()));
3161        }
3162        let statement = &statements[0];
3163        if mutates_database(statement) {
3164            self.ensure_catalog_cleanup_unfenced("database mutation")?;
3165        }
3166        if is_topology_ddl(statement) {
3167            let _topology_ddl = self.topology_ddl_lock.write().await;
3168            self.ensure_catalog_cleanup_unfenced("database mutation")?;
3169            #[cfg(feature = "cluster")]
3170            self.ensure_coordinated_recovery_mutation_unfenced("database mutation")?;
3171            self.execute_parsed_single(sql, statement).await
3172        } else if reads_catalog(statement) {
3173            let _topology_read = self.topology_ddl_lock.read().await;
3174            if mutates_database(statement) {
3175                self.ensure_catalog_cleanup_unfenced("database mutation")?;
3176                #[cfg(feature = "cluster")]
3177                self.ensure_coordinated_recovery_mutation_unfenced("database mutation")?;
3178            }
3179            self.execute_parsed_single(sql, statement).await
3180        } else if mutates_database(statement) {
3181            let _topology_read = self.topology_ddl_lock.read().await;
3182            self.ensure_catalog_cleanup_unfenced("database mutation")?;
3183            #[cfg(feature = "cluster")]
3184            self.ensure_coordinated_recovery_mutation_unfenced("database mutation")?;
3185            self.execute_parsed_single(sql, statement).await
3186        } else {
3187            self.execute_parsed_single(sql, statement).await
3188        }
3189    }
3190
3191    #[cfg(feature = "cluster")]
3192    async fn execute_single_already_gated(&self, sql: &str) -> Result<ExecuteResult, DbError> {
3193        let statements = parse_streaming_sql(sql)?;
3194        if statements.len() != 1 || !is_topology_ddl(&statements[0]) {
3195            return Err(DbError::InvalidOperation(
3196                "catalog manifest entries must contain exactly one topology DDL statement".into(),
3197            ));
3198        }
3199        self.execute_parsed_single(sql, &statements[0]).await
3200    }
3201
3202    async fn execute_parsed_single(
3203        &self,
3204        sql: &str,
3205        statement: &StreamingStatement,
3206    ) -> Result<ExecuteResult, DbError> {
3207        if self.is_cluster_runtime()
3208            && matches!(
3209                statement,
3210                StreamingStatement::CreateStream {
3211                    retention_bytes: Some(_),
3212                    ..
3213                }
3214            )
3215        {
3216            return Err(DbError::Unsupported(
3217                "CREATE STREAM RETAIN HISTORY is not supported in cluster runtime until replay is globally ordered and checkpoint-aligned"
3218                    .into(),
3219            ));
3220        }
3221
3222        #[cfg(feature = "cluster")]
3223        if is_topology_ddl(statement) && !catalog_manifest_replay_active() {
3224            let store_configured = self.catalog_manifest_store.lock().is_some();
3225            let cluster_runtime = self.is_cluster_runtime();
3226            if store_configured || cluster_runtime {
3227                validate_cluster_catalog_create(sql, statement)?;
3228                if !store_configured {
3229                    return Err(DbError::Pipeline(
3230                        "cluster topology DDL requires a catalog manifest store".into(),
3231                    ));
3232                }
3233                if !catalog_bootstrap_active() {
3234                    return Err(DbError::Pipeline(
3235                        "[LDB-6043] configured cluster topology can change only through startup bootstrap/replay until a replicated topology-version barrier is implemented"
3236                            .into(),
3237                    ));
3238                }
3239            }
3240        }
3241
3242        let result = match statement {
3243            StreamingStatement::CreateSource(create) => {
3244                let result = self.handle_create_source(create).await?;
3245                if let ExecuteResult::Ddl(ref info) = result {
3246                    if info.applied {
3247                        self.connector_manager
3248                            .lock()
3249                            .store_ddl(&info.object_name, sql);
3250                    }
3251                }
3252                Ok(result)
3253            }
3254            StreamingStatement::CreateSink(create) => {
3255                let result = self.handle_create_sink(create)?;
3256                if let ExecuteResult::Ddl(ref info) = result {
3257                    if info.applied {
3258                        self.connector_manager
3259                            .lock()
3260                            .store_ddl(&info.object_name, sql);
3261                    }
3262                }
3263                Ok(result)
3264            }
3265            StreamingStatement::CreateStream {
3266                name,
3267                query,
3268                emit_clause,
3269                if_not_exists,
3270                query_sql,
3271                retention_bytes,
3272                ..
3273            } => {
3274                let result = self
3275                    .handle_create_stream(
3276                        sql,
3277                        name,
3278                        query,
3279                        emit_clause.as_ref(),
3280                        *if_not_exists,
3281                        query_sql,
3282                        *retention_bytes,
3283                    )
3284                    .await?;
3285                if let ExecuteResult::Ddl(ref info) = result {
3286                    if info.applied {
3287                        self.connector_manager
3288                            .lock()
3289                            .store_ddl(&info.object_name, sql);
3290                    }
3291                }
3292                Ok(result)
3293            }
3294            StreamingStatement::CreateContinuousQuery { .. } => Err(DbError::InvalidOperation(
3295                "CREATE CONTINUOUS QUERY has no typed catalog/drop lifecycle; use CREATE STREAM"
3296                    .into(),
3297            )),
3298            StreamingStatement::DropLookupTable { name, if_exists } => {
3299                self.handle_drop_lookup_table(name, *if_exists)
3300            }
3301            StreamingStatement::CreateLookupTable(create) => {
3302                if create.or_replace {
3303                    return Err(DbError::InvalidOperation(
3304                        "CREATE OR REPLACE LOOKUP TABLE is not atomic; use DROP LOOKUP TABLE followed by CREATE LOOKUP TABLE"
3305                            .into(),
3306                    ));
3307                }
3308                let name = canonical_object_name(&create.name)?;
3309                let Some(reservation) = self.reserve_catalog_name(
3310                    &name,
3311                    CatalogObjectKind::LookupTable,
3312                    create.if_not_exists,
3313                )?
3314                else {
3315                    return Ok(ExecuteResult::Ddl(DdlInfo {
3316                        statement_type: "CREATE LOOKUP TABLE".into(),
3317                        object_name: name,
3318                        applied: false,
3319                    }));
3320                };
3321                let result = self.handle_query(sql).await?;
3322                if let ExecuteResult::Ddl(ref info) = result {
3323                    if info.applied {
3324                        self.connector_manager
3325                            .lock()
3326                            .store_ddl(&info.object_name, sql);
3327                    }
3328                }
3329                reservation.commit();
3330                Ok(result)
3331            }
3332            StreamingStatement::Standard(stmt) => {
3333                if let sqlparser::ast::Statement::CreateTable(ct) = stmt.as_ref() {
3334                    let result = self.handle_create_table(ct)?;
3335                    if let ExecuteResult::Ddl(ref info) = result {
3336                        if info.applied {
3337                            self.connector_manager
3338                                .lock()
3339                                .store_ddl(&info.object_name, sql);
3340                        }
3341                    }
3342                    Ok(result)
3343                } else if let sqlparser::ast::Statement::Drop {
3344                    object_type: sqlparser::ast::ObjectType::Table,
3345                    names,
3346                    if_exists,
3347                    cascade,
3348                    ..
3349                } = stmt.as_ref()
3350                {
3351                    self.handle_drop_table(names, *if_exists, *cascade)
3352                } else if let sqlparser::ast::Statement::Set(set_stmt) = stmt.as_ref() {
3353                    self.handle_set(set_stmt)
3354                } else if matches!(stmt.as_ref(), sqlparser::ast::Statement::AlterTable { .. }) {
3355                    Err(DbError::InvalidOperation(
3356                        "ALTER TABLE is disabled until catalog/provider changes are transactional"
3357                            .into(),
3358                    ))
3359                } else if matches!(stmt.as_ref(), sqlparser::ast::Statement::Query(_)) {
3360                    self.handle_query(sql).await
3361                } else {
3362                    Err(DbError::InvalidOperation(format!(
3363                        "unsupported standard SQL statement; catalog mutation is not typed or transactional: {stmt}"
3364                    )))
3365                }
3366            }
3367            StreamingStatement::InsertInto {
3368                table_name,
3369                columns,
3370                values,
3371            } => self.handle_insert_into(table_name, columns, values),
3372            StreamingStatement::DropSource {
3373                name,
3374                if_exists,
3375                cascade,
3376            } => self.handle_drop_source(name, *if_exists, *cascade),
3377            StreamingStatement::DropSink {
3378                name,
3379                if_exists,
3380                cascade,
3381            } => self.handle_drop_sink(name, *if_exists, *cascade),
3382            StreamingStatement::DropStream {
3383                name,
3384                if_exists,
3385                cascade,
3386            } => self.handle_drop_stream(name, *if_exists, *cascade).await,
3387            StreamingStatement::DropMaterializedView {
3388                name,
3389                if_exists,
3390                cascade,
3391            } => {
3392                self.handle_drop_materialized_view(name, *if_exists, *cascade)
3393                    .await
3394            }
3395            StreamingStatement::Show(cmd) => {
3396                let batch = match cmd {
3397                    ShowCommand::Sources => self.build_show_sources(),
3398                    ShowCommand::Sinks => self.build_show_sinks(),
3399                    ShowCommand::Queries => self.build_show_queries(),
3400                    ShowCommand::MaterializedViews => self.build_show_materialized_views(),
3401                    ShowCommand::Streams => self.build_show_streams(),
3402                    ShowCommand::Tables => self.build_show_tables(),
3403                    ShowCommand::CheckpointStatus => self.build_show_checkpoint_status().await?,
3404                    ShowCommand::CreateSource { name } => {
3405                        self.build_show_create_source(&canonical_object_name(name)?)?
3406                    }
3407                    ShowCommand::CreateSink { name } => {
3408                        self.build_show_create_sink(&canonical_object_name(name)?)?
3409                    }
3410                };
3411                Ok(ExecuteResult::Metadata(batch))
3412            }
3413            StreamingStatement::Checkpoint => {
3414                let result = self.checkpoint().await?;
3415                Ok(ExecuteResult::Ddl(DdlInfo {
3416                    statement_type: "CHECKPOINT".to_string(),
3417                    object_name: format!("checkpoint_{}", result.checkpoint_id),
3418                    applied: true,
3419                }))
3420            }
3421            StreamingStatement::RestoreCheckpoint { checkpoint_id } => {
3422                self.handle_restore_checkpoint(*checkpoint_id)
3423            }
3424            StreamingStatement::Describe { name, .. } => {
3425                let name_str = name.to_string();
3426                let batch = self.build_describe(&name_str)?;
3427                Ok(ExecuteResult::Metadata(batch))
3428            }
3429            StreamingStatement::Explain {
3430                statement, analyze, ..
3431            } => {
3432                if *analyze {
3433                    self.handle_explain_analyze(statement, sql).await
3434                } else {
3435                    self.handle_explain(statement)
3436                }
3437            }
3438            StreamingStatement::CreateMaterializedView {
3439                name,
3440                query,
3441                emit_clause,
3442                or_replace,
3443                if_not_exists,
3444                query_sql,
3445                ..
3446            } => {
3447                let result = self
3448                    .handle_create_materialized_view(
3449                        sql,
3450                        name,
3451                        query,
3452                        emit_clause.clone(),
3453                        *or_replace,
3454                        *if_not_exists,
3455                        query_sql,
3456                    )
3457                    .await?;
3458                if let ExecuteResult::Ddl(ref info) = result {
3459                    if info.applied {
3460                        self.connector_manager
3461                            .lock()
3462                            .store_ddl(&info.object_name, sql);
3463                    }
3464                }
3465                Ok(result)
3466            }
3467            StreamingStatement::AlterSource { .. } => Err(DbError::InvalidOperation(
3468                "ALTER SOURCE is disabled until catalog/provider changes are transactional".into(),
3469            )),
3470            StreamingStatement::Subscribe(_) => Err(DbError::InvalidOperation(
3471                "SUBSCRIBE requires the pgwire endpoint, not HTTP /api/v1/sql".into(),
3472            )),
3473            StreamingStatement::DeclareCursorForSubscribe { .. } => Err(DbError::InvalidOperation(
3474                "DECLARE CURSOR FOR SUBSCRIBE requires the pgwire endpoint, not HTTP /api/v1/sql"
3475                    .into(),
3476            )),
3477        };
3478
3479        result
3480    }
3481
3482    fn handle_insert_into(
3483        &self,
3484        table_name: &sqlparser::ast::ObjectName,
3485        columns: &[sqlparser::ast::Ident],
3486        values: &[Vec<sqlparser::ast::Expr>],
3487    ) -> Result<ExecuteResult, DbError> {
3488        let name = canonical_object_name(table_name)?;
3489        if !columns.is_empty() {
3490            return Err(DbError::InvalidOperation(
3491                "INSERT column lists are unsupported until projection and default-value semantics are implemented"
3492                    .into(),
3493            ));
3494        }
3495        if values.is_empty() {
3496            return Err(DbError::InvalidOperation(
3497                "INSERT requires at least one VALUES row".into(),
3498            ));
3499        }
3500
3501        if let Some(entry) = self.catalog.get_source(&name) {
3502            let batch = sql_utils::sql_values_to_record_batch(&entry.schema, values)?;
3503            entry
3504                .push_and_buffer(batch)
3505                .map_err(|e| DbError::InsertError(format!("Failed to push to source: {e}")))?;
3506            return Ok(ExecuteResult::RowsAffected(values.len() as u64));
3507        }
3508
3509        // Single lock scope avoids TOCTOU between has_table/schema/upsert.
3510        {
3511            let mut ts = self.table_store.write();
3512            if ts.has_table(&name) {
3513                let schema = ts
3514                    .table_schema(&name)
3515                    .ok_or_else(|| DbError::TableNotFound(name.clone()))?;
3516                let batch = sql_utils::sql_values_to_record_batch(&schema, values)?;
3517                ts.upsert(&name, &batch)?;
3518                drop(ts); // release before sync (which may also lock)
3519
3520                self.sync_table_to_datafusion(&name)?;
3521                return Ok(ExecuteResult::RowsAffected(values.len() as u64));
3522            }
3523        }
3524
3525        Err(DbError::InvalidOperation(format!(
3526            "INSERT target '{name}' is not a typed mutable source or table"
3527        )))
3528    }
3529
3530    #[allow(clippy::unused_self)] // will use self when implemented
3531    fn handle_restore_checkpoint(&self, _checkpoint_id: u64) -> Result<ExecuteResult, DbError> {
3532        Err(DbError::Unsupported(
3533            "RESTORE FROM CHECKPOINT is not yet implemented — \
3534             requires pipeline stop, state reload from manifest, \
3535             source offset seek, and pipeline restart"
3536                .to_string(),
3537        ))
3538    }
3539
3540    /// Return a session property value.
3541    #[must_use]
3542    pub fn get_session_property(&self, key: &str) -> Option<String> {
3543        self.session_properties
3544            .lock()
3545            .get(&key.to_lowercase())
3546            .cloned()
3547    }
3548
3549    /// Return all session properties.
3550    #[must_use]
3551    pub fn session_properties(&self) -> HashMap<String, String> {
3552        self.session_properties.lock().clone()
3553    }
3554
3555    /// Set a session property (keys are lowercased).
3556    pub fn set_session_property(&self, key: &str, value: &str) {
3557        self.session_properties
3558            .lock()
3559            .insert(key.to_lowercase(), value.to_string());
3560    }
3561
3562    /// Subscribe to a named stream or materialized view.
3563    ///
3564    /// # Errors
3565    ///
3566    /// Returns `DbError::StreamNotFound` if the object or its output schema is unresolved.
3567    pub async fn subscribe<T: crate::handle::FromBatch>(
3568        &self,
3569        name: &str,
3570    ) -> Result<crate::handle::TypedSubscription<T>, DbError> {
3571        let portal = self
3572            .open_subscription(name, None, crate::subscription::SubscribeStart::Tail)
3573            .await?;
3574        Ok(crate::handle::TypedSubscription::new(portal))
3575    }
3576
3577    fn ensure_subscription_runtime_supported(&self) -> Result<(), DbError> {
3578        if self.is_cluster_runtime() {
3579            return Err(DbError::Unsupported(
3580                "SUBSCRIBE is not supported in cluster runtime until delivery is sequenced and checkpoint-aligned"
3581                    .into(),
3582            ));
3583        }
3584        Ok(())
3585    }
3586
3587    /// Schema a `SUBSCRIBE` against `name` would emit.
3588    /// A stream is visible here only after its physical output schema has been
3589    /// resolved. Bare sources and unresolved streams are not subscribable.
3590    #[must_use]
3591    pub fn lookup_subscription_schema(&self, name: &str) -> Option<arrow_schema::SchemaRef> {
3592        if let Some(mv) = self.mv_registry.lock().get(name).cloned() {
3593            return Some(mv.schema);
3594        }
3595        if let Some(schema) = self.stream_schemas.read().get(name).cloned() {
3596            return Some(schema);
3597        }
3598        None
3599    }
3600
3601    /// Open a SUBSCRIBE portal against a named MV or resolved stream. A bare
3602    /// SOURCE is not subscribable (surfaced as `StreamNotFound`).
3603    ///
3604    /// # Errors
3605    /// `Unsupported` in cluster runtime; `StreamNotFound` for unknown `name`;
3606    /// `Pipeline` for subscriber-cap
3607    /// or filter-compile failures; `InvalidOperation` when `AsOfEpoch(n)`
3608    /// is not committed or is no longer retained.
3609    pub async fn open_subscription(
3610        &self,
3611        name: &str,
3612        filter_sql: Option<&str>,
3613        start: crate::subscription::SubscribeStart,
3614    ) -> Result<crate::subscription::SubscriptionPortal, DbError> {
3615        self.ensure_subscription_runtime_supported()?;
3616
3617        // Serialize schema resolution, filter compilation, and cursor attachment
3618        // with topology DDL. Otherwise DROP/recreate can leave a portal attached
3619        // to an orphaned log with the previous object's schema.
3620        let _topology = self.topology_ddl_lock.read().await;
3621
3622        // SUBSCRIBE to an incremental MV delivers consolidated snapshots (plain rows), not the
3623        // raw `__weight` changelog.
3624
3625        let schema = self
3626            .lookup_subscription_schema(name)
3627            .ok_or_else(|| DbError::StreamNotFound(name.to_string()))?;
3628
3629        let filter = match filter_sql {
3630            None => None,
3631            Some(sql) => Some(crate::filter_compile::compile(&self.ctx, sql, &schema).await?),
3632        };
3633
3634        let reader =
3635            self.subscription_registry
3636                .subscribe(name, start)
3637                .map_err(|error| match error {
3638                    crate::subscription::SubscriptionOpenError::ReplayPruned {
3639                        earliest_retained,
3640                    } => {
3641                        let requested = match start {
3642                            crate::subscription::SubscribeStart::AsOfEpoch(n) => n,
3643                            crate::subscription::SubscribeStart::Tail => 0,
3644                        };
3645                        DbError::SubscriptionReplayPruned {
3646                            name: name.to_string(),
3647                            requested,
3648                            earliest_retained,
3649                        }
3650                    }
3651                    crate::subscription::SubscriptionOpenError::EpochNotCommitted {
3652                        requested,
3653                        latest_committed,
3654                    } => DbError::SubscriptionEpochNotCommitted {
3655                        name: name.to_string(),
3656                        requested,
3657                        latest_committed,
3658                    },
3659                    crate::subscription::SubscriptionOpenError::Capacity { attached, limit } => {
3660                        DbError::Pipeline(format!(
3661                            "subscriber cap reached for '{name}' ({attached}/{limit})"
3662                        ))
3663                    }
3664                })?;
3665
3666        Ok(match filter {
3667            Some(phys) => crate::subscription::SubscriptionPortal::open_with_filter(
3668                name, schema, reader, phys,
3669            ),
3670            None => crate::subscription::SubscriptionPortal::open(name, schema, reader),
3671        })
3672    }
3673
3674    fn handle_explain(&self, statement: &StreamingStatement) -> Result<ExecuteResult, DbError> {
3675        let mut planner = self.planner.lock();
3676        let plan_result = planner.plan(statement);
3677
3678        let mut rows: Vec<(String, String)> = Vec::new();
3679
3680        match plan_result {
3681            Ok(plan) => {
3682                rows.push((
3683                    "plan_type".into(),
3684                    match &plan {
3685                        laminar_sql::planner::StreamingPlan::Query(_) => "Query",
3686                        laminar_sql::planner::StreamingPlan::RegisterSource(_) => "RegisterSource",
3687                        laminar_sql::planner::StreamingPlan::RegisterSink(_) => "RegisterSink",
3688                        laminar_sql::planner::StreamingPlan::Standard(_) => "Standard",
3689                        laminar_sql::planner::StreamingPlan::RegisterLookupTable(_) => {
3690                            "RegisterLookupTable"
3691                        }
3692                        laminar_sql::planner::StreamingPlan::DropLookupTable { .. } => {
3693                            "DropLookupTable"
3694                        }
3695                    }
3696                    .into(),
3697                ));
3698                match &plan {
3699                    laminar_sql::planner::StreamingPlan::Query(qp) => {
3700                        if let Some(name) = &qp.name {
3701                            rows.push(("query_name".into(), name.clone()));
3702                        }
3703                        if let Some(wc) = &qp.window_config {
3704                            rows.push(("window".into(), format!("{wc}")));
3705                        }
3706                        if let Some(jcs) = &qp.join_config {
3707                            if jcs.len() == 1 {
3708                                rows.push(("join".into(), format!("{}", jcs[0])));
3709                            } else {
3710                                for (i, jc) in jcs.iter().enumerate() {
3711                                    rows.push((format!("join_step_{}", i + 1), format!("{jc}")));
3712                                }
3713                            }
3714                        }
3715                        if let Some(oc) = &qp.order_config {
3716                            rows.push(("order_by".into(), format!("{oc:?}")));
3717                        }
3718                        if let Some(fc) = &qp.frame_config {
3719                            rows.push((
3720                                "frame_functions".into(),
3721                                format!("{}", fc.functions.len()),
3722                            ));
3723                        }
3724                        if let Some(ec) = &qp.emit_clause {
3725                            rows.push(("emit".into(), format!("{ec}")));
3726                        }
3727                    }
3728                    laminar_sql::planner::StreamingPlan::RegisterSource(info) => {
3729                        rows.push(("source".into(), info.name.clone()));
3730                    }
3731                    laminar_sql::planner::StreamingPlan::RegisterSink(info) => {
3732                        rows.push(("sink".into(), info.name.clone()));
3733                    }
3734                    laminar_sql::planner::StreamingPlan::Standard(_) => {
3735                        rows.push(("execution".into(), "DataFusion pass-through".into()));
3736                    }
3737                    laminar_sql::planner::StreamingPlan::RegisterLookupTable(info) => {
3738                        rows.push(("lookup_table".into(), info.name.clone()));
3739                    }
3740                    laminar_sql::planner::StreamingPlan::DropLookupTable { name } => {
3741                        rows.push(("drop_lookup_table".into(), name.clone()));
3742                    }
3743                }
3744            }
3745            Err(e) => {
3746                rows.push(("error".into(), format!("{e}")));
3747                rows.push((
3748                    "statement".into(),
3749                    format!("{:?}", std::mem::discriminant(statement)),
3750                ));
3751            }
3752        }
3753
3754        let keys: Vec<&str> = rows.iter().map(|(k, _)| k.as_str()).collect();
3755        let values: Vec<&str> = rows.iter().map(|(_, v)| v.as_str()).collect();
3756
3757        let schema = Arc::new(Schema::new(vec![
3758            Field::new("plan_key", DataType::Utf8, false),
3759            Field::new("plan_value", DataType::Utf8, false),
3760        ]));
3761
3762        let batch = RecordBatch::try_new(
3763            schema,
3764            vec![
3765                Arc::new(StringArray::from(keys)),
3766                Arc::new(StringArray::from(values)),
3767            ],
3768        )
3769        .map_err(|e| DbError::InvalidOperation(format!("explain metadata: {e}")))?;
3770
3771        Ok(ExecuteResult::Metadata(batch))
3772    }
3773
3774    async fn handle_explain_analyze(
3775        &self,
3776        statement: &StreamingStatement,
3777        original_sql: &str,
3778    ) -> Result<ExecuteResult, DbError> {
3779        let explain_result = self.handle_explain(statement)?;
3780        let mut rows: Vec<(String, String)> = Vec::new();
3781
3782        if let ExecuteResult::Metadata(explain_batch) = &explain_result {
3783            let keys_col = explain_batch
3784                .column(0)
3785                .as_any()
3786                .downcast_ref::<StringArray>();
3787            let vals_col = explain_batch
3788                .column(1)
3789                .as_any()
3790                .downcast_ref::<StringArray>();
3791            if let (Some(keys), Some(vals)) = (keys_col, vals_col) {
3792                for i in 0..explain_batch.num_rows() {
3793                    rows.push((keys.value(i).to_string(), vals.value(i).to_string()));
3794                }
3795            }
3796        }
3797
3798        let upper = original_sql.to_uppercase();
3799        let inner_start = upper.find("ANALYZE").map_or(0, |pos| pos + "ANALYZE".len());
3800        let inner_sql = original_sql[inner_start..].trim();
3801
3802        let start = std::time::Instant::now();
3803        match self.ctx.sql(inner_sql).await {
3804            Ok(df) => match df.collect().await {
3805                Ok(batches) => {
3806                    let elapsed = start.elapsed();
3807                    let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
3808                    rows.push(("rows_produced".into(), total_rows.to_string()));
3809                    rows.push(("execution_time_ms".into(), elapsed.as_millis().to_string()));
3810                    rows.push(("batches_processed".into(), batches.len().to_string()));
3811                }
3812                Err(e) => {
3813                    let elapsed = start.elapsed();
3814                    rows.push(("execution_time_ms".into(), elapsed.as_millis().to_string()));
3815                    rows.push(("analyze_error".into(), format!("{e}")));
3816                }
3817            },
3818            Err(e) => {
3819                rows.push(("analyze_error".into(), format!("{e}")));
3820            }
3821        }
3822
3823        let keys: Vec<&str> = rows.iter().map(|(k, _)| k.as_str()).collect();
3824        let values: Vec<&str> = rows.iter().map(|(_, v)| v.as_str()).collect();
3825
3826        let schema = Arc::new(Schema::new(vec![
3827            Field::new("plan_key", DataType::Utf8, false),
3828            Field::new("plan_value", DataType::Utf8, false),
3829        ]));
3830
3831        let batch = RecordBatch::try_new(
3832            schema,
3833            vec![
3834                Arc::new(StringArray::from(keys)),
3835                Arc::new(StringArray::from(values)),
3836            ],
3837        )
3838        .map_err(|e| DbError::InvalidOperation(format!("explain analyze metadata: {e}")))?;
3839
3840        Ok(ExecuteResult::Metadata(batch))
3841    }
3842
3843    #[allow(clippy::too_many_lines)]
3844    pub(crate) async fn handle_query(&self, sql: &str) -> Result<ExecuteResult, DbError> {
3845        let plan = {
3846            let statements = parse_streaming_sql(sql)?;
3847            if statements.is_empty() {
3848                return Err(DbError::InvalidOperation("Empty SQL statement".into()));
3849            }
3850            match &statements[0] {
3851                StreamingStatement::CreateContinuousQuery { .. } => {
3852                    return Err(DbError::InvalidOperation(
3853                        "CREATE CONTINUOUS QUERY has no typed catalog/drop lifecycle; use CREATE STREAM"
3854                            .into(),
3855                    ));
3856                }
3857                StreamingStatement::CreateLookupTable(statement) => {
3858                    self.ensure_offline_topology_ddl_allowed("CREATE LOOKUP TABLE")?;
3859                    let name = canonical_object_name(&statement.name)?;
3860                    if self.catalog_namespace.lock().get(&name)
3861                        != Some(&CatalogObjectKind::LookupTable)
3862                    {
3863                        return Err(DbError::InvalidOperation(
3864                            "CREATE LOOKUP TABLE must pass typed namespace admission".into(),
3865                        ));
3866                    }
3867                }
3868                StreamingStatement::DropLookupTable { name, if_exists } => {
3869                    return self.handle_drop_lookup_table(name, *if_exists);
3870                }
3871                _ => {}
3872            }
3873            let mut planner = self.planner.lock();
3874            planner
3875                .plan(&statements[0])
3876                .map_err(laminar_sql::Error::from)?
3877        };
3878
3879        match plan {
3880            laminar_sql::planner::StreamingPlan::RegisterSource(info) => {
3881                Ok(ExecuteResult::Ddl(DdlInfo {
3882                    statement_type: "DDL".to_string(),
3883                    object_name: info.name,
3884                    applied: true,
3885                }))
3886            }
3887            laminar_sql::planner::StreamingPlan::RegisterSink(info) => {
3888                Ok(ExecuteResult::Ddl(DdlInfo {
3889                    statement_type: "DDL".to_string(),
3890                    object_name: info.name,
3891                    applied: true,
3892                }))
3893            }
3894            laminar_sql::planner::StreamingPlan::Query(query_plan) => {
3895                if let Some(asof_config) = Self::extract_asof_config(&query_plan) {
3896                    return self.execute_asof_query(&asof_config, sql).await;
3897                }
3898
3899                let plan_sql = query_plan.statement.to_string();
3900                let logical_plan = self.ctx.state().create_logical_plan(&plan_sql).await?;
3901                let df = self.ctx.execute_logical_plan(logical_plan).await?;
3902                let stream = df.execute_stream().await?;
3903
3904                Ok(self.bridge_query_stream(sql, stream))
3905            }
3906            laminar_sql::planner::StreamingPlan::Standard(stmt) => {
3907                let sql_str = stmt.to_string();
3908                let df = self.ctx.sql(&sql_str).await?;
3909                let stream = df.execute_stream().await?;
3910
3911                Ok(self.bridge_query_stream(sql, stream))
3912            }
3913            laminar_sql::planner::StreamingPlan::RegisterLookupTable(info) => {
3914                self.handle_register_lookup_table(info)
3915            }
3916            laminar_sql::planner::StreamingPlan::DropLookupTable { .. } => Err(
3917                DbError::InvalidOperation("lookup drop bypassed typed catalog admission".into()),
3918            ),
3919        }
3920    }
3921
3922    fn bridge_query_stream(
3923        &self,
3924        sql: &str,
3925        stream: datafusion::physical_plan::SendableRecordBatchStream,
3926    ) -> ExecuteResult {
3927        let query_id = self.catalog.register_query(sql);
3928        let schema = stream.schema();
3929
3930        let source_cfg = streaming::SourceConfig::with_buffer_size(self.config.default_buffer_size);
3931        let (source, sink) =
3932            streaming::create_with_config::<crate::catalog::ArrowRecord>(source_cfg);
3933
3934        let subscription = sink.subscribe();
3935
3936        let cancel_token = tokio_util::sync::CancellationToken::new();
3937        let cancel_token_clone = cancel_token.clone();
3938
3939        let source_clone = source.clone();
3940        let catalog = Arc::clone(&self.catalog);
3941        let query_id_clone = query_id;
3942        tokio::spawn(async move {
3943            use tokio_stream::StreamExt;
3944            let mut stream = stream;
3945            loop {
3946                tokio::select! {
3947                    () = cancel_token_clone.cancelled() => {
3948                        break;
3949                    }
3950                    result = stream.next() => {
3951                        match result {
3952                            Some(Ok(batch)) => {
3953                                if source_clone.push_arrow(batch).is_err() {
3954                                    break;
3955                                }
3956                            }
3957                            _ => break,
3958                        }
3959                    }
3960                }
3961            }
3962            drop(source_clone);
3963            catalog.deactivate_query(query_id_clone);
3964        });
3965
3966        ExecuteResult::Query(QueryHandle {
3967            id: query_id,
3968            schema,
3969            sql: sql.to_string(),
3970            subscription: Some(subscription),
3971            active: true,
3972            cancel_token,
3973        })
3974    }
3975
3976    fn extract_asof_config(
3977        plan: &laminar_sql::planner::QueryPlan,
3978    ) -> Option<AsofJoinTranslatorConfig> {
3979        plan.join_config.as_ref()?.iter().find_map(|jc| {
3980            if let JoinOperatorConfig::Asof(cfg) = jc {
3981                Some(cfg.clone())
3982            } else {
3983                None
3984            }
3985        })
3986    }
3987
3988    async fn execute_asof_query(
3989        &self,
3990        asof_config: &AsofJoinTranslatorConfig,
3991        original_sql: &str,
3992    ) -> Result<ExecuteResult, DbError> {
3993        let left_sql = format!("SELECT * FROM {}", asof_config.left_table);
3994        let right_sql = format!("SELECT * FROM {}", asof_config.right_table);
3995
3996        let left_batches = self
3997            .ctx
3998            .sql(&left_sql)
3999            .await
4000            .map_err(|e| DbError::query_pipeline(&asof_config.left_table, &e))?
4001            .collect()
4002            .await
4003            .map_err(|e| DbError::query_pipeline(&asof_config.left_table, &e))?;
4004
4005        let right_batches = self
4006            .ctx
4007            .sql(&right_sql)
4008            .await
4009            .map_err(|e| DbError::query_pipeline(&asof_config.right_table, &e))?
4010            .collect()
4011            .await
4012            .map_err(|e| DbError::query_pipeline(&asof_config.right_table, &e))?;
4013
4014        let result_batch =
4015            crate::asof_batch::execute_asof_join_batch(&left_batches, &right_batches, asof_config)?;
4016
4017        if result_batch.num_rows() == 0 {
4018            let query_id = self.catalog.register_query(original_sql);
4019            self.catalog.deactivate_query(query_id);
4020            return Ok(ExecuteResult::Query(QueryHandle {
4021                id: query_id,
4022                schema: result_batch.schema(),
4023                sql: original_sql.to_string(),
4024                subscription: None,
4025                active: false,
4026                cancel_token: tokio_util::sync::CancellationToken::new(),
4027            }));
4028        }
4029
4030        let schema = result_batch.schema();
4031        let mem_table =
4032            datafusion::datasource::MemTable::try_new(schema.clone(), vec![vec![result_batch]])
4033                .map_err(|e| DbError::query_pipeline("ASOF join", &e))?;
4034
4035        let _ = self
4036            .ctx
4037            .deregister_table(exact_table_reference("__asof_result"));
4038        self.ctx
4039            .register_table(exact_table_reference("__asof_result"), Arc::new(mem_table))
4040            .map_err(|e| DbError::query_pipeline("ASOF join", &e))?;
4041
4042        let df = self
4043            .ctx
4044            .sql("SELECT * FROM __asof_result")
4045            .await
4046            .map_err(|e| DbError::query_pipeline("ASOF join", &e))?;
4047        let stream = df
4048            .execute_stream()
4049            .await
4050            .map_err(|e| DbError::query_pipeline("ASOF join", &e))?;
4051
4052        let _ = self
4053            .ctx
4054            .deregister_table(exact_table_reference("__asof_result"));
4055
4056        Ok(self.bridge_query_stream(original_sql, stream))
4057    }
4058
4059    /// Get a typed handle for pushing data to a registered source.
4060    ///
4061    /// # Errors
4062    ///
4063    /// Returns `DbError::SourceNotFound` if the source is not registered.
4064    /// Returns `DbError::SchemaMismatch` if the Rust type's schema doesn't match.
4065    pub fn source<T: laminar_core::streaming::Record>(
4066        &self,
4067        name: &str,
4068    ) -> Result<SourceHandle<T>, DbError> {
4069        let entry = self
4070            .catalog
4071            .get_source(name)
4072            .ok_or_else(|| DbError::SourceNotFound(name.to_string()))?;
4073        SourceHandle::new(entry)
4074    }
4075
4076    /// Get an untyped source handle for pushing `RecordBatch` data.
4077    ///
4078    /// # Errors
4079    ///
4080    /// Returns `DbError::SourceNotFound` if the source is not registered.
4081    pub fn source_untyped(&self, name: &str) -> Result<UntypedSourceHandle, DbError> {
4082        let entry = self
4083            .catalog
4084            .get_source(name)
4085            .ok_or_else(|| DbError::SourceNotFound(name.to_string()))?;
4086        Ok(UntypedSourceHandle::new(entry))
4087    }
4088
4089    /// List registered sources.
4090    pub fn sources(&self) -> Vec<SourceInfo> {
4091        let names = self.catalog.list_sources();
4092        names
4093            .into_iter()
4094            .filter_map(|name| {
4095                self.catalog.get_source(&name).map(|e| SourceInfo {
4096                    name: e.name.clone(),
4097                    schema: e.schema.clone(),
4098                    watermark_column: e.watermark_column.clone(),
4099                })
4100            })
4101            .collect()
4102    }
4103
4104    /// List registered sinks.
4105    pub fn sinks(&self) -> Vec<SinkInfo> {
4106        self.catalog
4107            .list_sinks()
4108            .into_iter()
4109            .map(|name| SinkInfo { name })
4110            .collect()
4111    }
4112
4113    /// List registered materialized views.
4114    pub fn materialized_views(&self) -> Vec<crate::handle::MaterializedViewInfo> {
4115        let registry = self.mv_registry.lock();
4116        registry
4117            .views()
4118            .map(crate::handle::MaterializedViewInfo::from)
4119            .collect()
4120    }
4121
4122    /// List registered streams.
4123    pub fn streams(&self) -> Vec<crate::handle::StreamInfo> {
4124        let mgr = self.connector_manager.lock();
4125        mgr.streams()
4126            .iter()
4127            .map(|(name, reg)| crate::handle::StreamInfo {
4128                name: name.clone(),
4129                sql: Some(reg.query_sql.clone()),
4130            })
4131            .collect()
4132    }
4133
4134    /// Build the pipeline topology graph (nodes + edges) from registered sources, streams, and sinks.
4135    pub fn pipeline_topology(&self) -> crate::handle::PipelineTopology {
4136        use crate::handle::{PipelineEdge, PipelineNode, PipelineNodeType};
4137
4138        let mut nodes = Vec::new();
4139        let mut edges = Vec::new();
4140
4141        let source_names = self.catalog.list_sources();
4142
4143        for name in &source_names {
4144            let schema = self.catalog.get_source(name).map(|e| e.schema.clone());
4145            nodes.push(PipelineNode {
4146                name: name.clone(),
4147                node_type: PipelineNodeType::Source,
4148                schema,
4149                sql: None,
4150            });
4151        }
4152
4153        let mgr = self.connector_manager.lock();
4154        let stream_names: Vec<String> = mgr.streams().keys().cloned().collect();
4155        for (name, reg) in mgr.streams() {
4156            nodes.push(PipelineNode {
4157                name: name.clone(),
4158                node_type: PipelineNodeType::Stream,
4159                schema: None,
4160                sql: Some(reg.query_sql.clone()),
4161            });
4162
4163            // Lightweight heuristic: substring match instead of a full parse.
4164            let sql_upper = reg.query_sql.to_uppercase();
4165            for src in &source_names {
4166                if sql_upper.contains(&src.to_uppercase()) {
4167                    edges.push(PipelineEdge {
4168                        from: src.clone(),
4169                        to: name.clone(),
4170                    });
4171                }
4172            }
4173            for other in &stream_names {
4174                if other != name && sql_upper.contains(&other.to_uppercase()) {
4175                    edges.push(PipelineEdge {
4176                        from: other.clone(),
4177                        to: name.clone(),
4178                    });
4179                }
4180            }
4181        }
4182
4183        for (name, reg) in mgr.sinks() {
4184            nodes.push(PipelineNode {
4185                name: name.clone(),
4186                node_type: PipelineNodeType::Sink,
4187                schema: None,
4188                sql: None,
4189            });
4190
4191            if !reg.input.is_empty() {
4192                edges.push(PipelineEdge {
4193                    from: reg.input.clone(),
4194                    to: name.clone(),
4195                });
4196            }
4197        }
4198
4199        let cm_sink_names: std::collections::HashSet<&String> = mgr.sinks().keys().collect();
4200        for name in self.catalog.list_sinks() {
4201            if !cm_sink_names.contains(&name) {
4202                if let Some(input) = self.catalog.get_sink_input(&name) {
4203                    nodes.push(PipelineNode {
4204                        name: name.clone(),
4205                        node_type: PipelineNodeType::Sink,
4206                        schema: None,
4207                        sql: None,
4208                    });
4209                    if !input.is_empty() {
4210                        edges.push(PipelineEdge {
4211                            from: input,
4212                            to: name,
4213                        });
4214                    }
4215                }
4216            }
4217        }
4218
4219        drop(mgr);
4220
4221        crate::handle::PipelineTopology { nodes, edges }
4222    }
4223
4224    /// List active queries.
4225    pub fn queries(&self) -> Vec<QueryInfo> {
4226        self.catalog
4227            .list_queries()
4228            .into_iter()
4229            .map(|(id, sql, active)| QueryInfo { id, sql, active })
4230            .collect()
4231    }
4232
4233    /// Returns whether streaming checkpointing is enabled.
4234    #[must_use]
4235    pub fn is_checkpoint_enabled(&self) -> bool {
4236        self.config.checkpoint.is_some()
4237    }
4238
4239    /// Stable participant identity used to namespace checkpoint manifests.
4240    ///
4241    /// Local runtimes return `None` and keep the historical unprefixed layout. Cluster runtimes
4242    /// use the controller's numeric instance id; decision markers intentionally do not use this
4243    /// namespace because they are cluster-wide.
4244    pub(crate) fn checkpoint_participant(&self) -> Option<u64> {
4245        checkpoint_participant_for_runtime(self)
4246    }
4247
4248    /// Stable logical partition count used by checkpoint and state identity.
4249    /// Local runtimes have one key group. Cluster runtimes use their exact registry or the
4250    /// fixed cluster default when no keyed state topology is installed.
4251    pub(crate) fn checkpoint_key_groups(&self) -> laminar_core::state::KeyGroupCount {
4252        let runtime_default = match self.runtime_mode() {
4253            RuntimeMode::Local => laminar_core::state::LOCAL_KEY_GROUP_COUNT,
4254            RuntimeMode::Cluster => laminar_core::state::DEFAULT_CLUSTER_KEY_GROUP_COUNT,
4255        };
4256        self.vnode_registry
4257            .lock()
4258            .as_ref()
4259            .map_or(runtime_default, |registry| {
4260                laminar_core::state::KeyGroupCount::try_from(registry.vnode_count())
4261                    .expect("builder validated the vnode registry key-group count")
4262            })
4263    }
4264
4265    /// Return a checkpoint store for the resolved runtime configuration, if any.
4266    pub(crate) fn checkpoint_store(
4267        &self,
4268    ) -> Result<Option<Box<dyn laminar_core::storage::CheckpointStore>>, DbError> {
4269        let Some(cp_config) = self.config.checkpoint.as_ref() else {
4270            return Ok(None);
4271        };
4272        let key_group_count = self.checkpoint_key_groups();
4273        let participant = self.checkpoint_participant();
4274        let participant_id = participant.unwrap_or(0);
4275        let max_state_data_bytes = cp_config.max_staged_bytes.ok_or_else(|| {
4276            DbError::Config("checkpoint.max_staged_bytes was not resolved at construction".into())
4277        })?;
4278
4279        #[cfg(feature = "cluster")]
4280        if let Some(object_store) = self.cluster_checkpoint_object_store() {
4281            return Ok(Some(Box::new(
4282                laminar_core::storage::checkpoint_store::ObjectStoreCheckpointStore::new(
4283                    object_store,
4284                    participant.map_or_else(String::new, |id| format!("nodes/{id}/")),
4285                )
4286                .with_max_state_data_bytes(max_state_data_bytes)?
4287                .with_key_group_count(key_group_count)
4288                .with_participant_id(participant_id),
4289            )));
4290        }
4291
4292        if let Some(url) = self
4293            .config
4294            .object_store_url
4295            .as_deref()
4296            .filter(|url| url.starts_with("file://"))
4297        {
4298            let root = laminar_core::storage::object_store_builder::file_url_path(url)
4299                .map_err(|error| DbError::Checkpoint(format!("checkpoint storage URL: {error}")))?;
4300            let checkpoint_dir =
4301                participant.map_or(root.clone(), |id| root.join("nodes").join(id.to_string()));
4302            Ok(Some(Box::new(
4303                laminar_core::storage::checkpoint_store::FileSystemCheckpointStore::new(
4304                    checkpoint_dir,
4305                )
4306                .with_max_state_data_bytes(max_state_data_bytes)?
4307                .with_key_group_count(key_group_count)
4308                .with_participant_id(participant_id),
4309            )))
4310        } else if let Some(ref url) = self.config.object_store_url {
4311            let obj_store = laminar_core::storage::object_store_builder::build_object_store(
4312                url,
4313                &self.config.object_store_options,
4314            )
4315            .map_err(|error| DbError::Checkpoint(format!("checkpoint object store: {error}")))?;
4316            Ok(Some(Box::new(
4317                laminar_core::storage::checkpoint_store::ObjectStoreCheckpointStore::new(
4318                    obj_store,
4319                    participant.map_or_else(String::new, |id| format!("nodes/{id}/")),
4320                )
4321                .with_max_state_data_bytes(max_state_data_bytes)?
4322                .with_key_group_count(key_group_count)
4323                .with_participant_id(participant_id),
4324            )))
4325        } else {
4326            let data_dir = cp_config
4327                .data_dir
4328                .clone()
4329                .or_else(|| self.config.storage_dir.clone())
4330                .unwrap_or_else(|| std::path::PathBuf::from("./data"));
4331            let checkpoint_dir = participant.map_or(data_dir.clone(), |id| {
4332                data_dir.join("nodes").join(id.to_string())
4333            });
4334            Ok(Some(Box::new(
4335                laminar_core::storage::checkpoint_store::FileSystemCheckpointStore::new(
4336                    checkpoint_dir,
4337                )
4338                .with_max_state_data_bytes(max_state_data_bytes)?
4339                .with_key_group_count(key_group_count)
4340                .with_participant_id(participant_id),
4341            )))
4342        }
4343    }
4344
4345    /// Trigger a checkpoint that persists source offsets, sink positions, and operator state.
4346    ///
4347    /// # Errors
4348    ///
4349    /// Returns `DbError::Checkpoint` if checkpointing is disabled, no live
4350    /// manual-checkpoint route exists, cluster leadership cannot be resolved,
4351    /// or the checkpoint fails.
4352    pub async fn checkpoint(
4353        &self,
4354    ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
4355        self.ensure_catalog_cleanup_unfenced("manual checkpoint")?;
4356        #[cfg(feature = "cluster")]
4357        self.ensure_coordinated_recovery_mutation_unfenced("manual checkpoint")?;
4358        if self.config.checkpoint.is_none() {
4359            return Err(DbError::Checkpoint(
4360                "checkpointing is not enabled".to_string(),
4361            ));
4362        }
4363        if DbState::load(&self.state) != DbState::Running {
4364            return Err(DbError::Checkpoint(
4365                "manual checkpoint coordinator is not running — a fully running pipeline is required"
4366                    .into(),
4367            ));
4368        }
4369
4370        #[cfg(feature = "cluster")]
4371        if self.is_cluster_runtime() {
4372            let leader_rpc: Result<Option<String>, DbError> = {
4373                let cc_guard = self.cluster_controller.lock();
4374                let cc = cc_guard.as_ref().ok_or_else(|| {
4375                    DbError::Checkpoint("cluster runtime has no cluster controller".into())
4376                })?;
4377                match cc {
4378                    cc if cc.is_leader() => Ok(None),
4379                    cc => {
4380                        let leader_id = cc.current_leader().ok_or_else(|| {
4381                            DbError::Checkpoint(
4382                                "cannot route checkpoint: cluster leader is unresolved".into(),
4383                            )
4384                        })?;
4385                        let watch = cc.members_watch();
4386                        let members = watch.borrow();
4387                        let address = members
4388                            .iter()
4389                            .find(|member| member.id == leader_id)
4390                            .map(|member| member.rpc_address.trim())
4391                            .filter(|address| !address.is_empty())
4392                            .ok_or_else(|| {
4393                                DbError::Checkpoint(format!(
4394                                    "cannot route checkpoint: RPC address for cluster leader \
4395                                     {leader_id} is unresolved"
4396                                ))
4397                            })?;
4398                        Ok(Some(address.to_owned()))
4399                    }
4400                }
4401            };
4402            if let Some(leader_rpc) = leader_rpc? {
4403                tracing::info!(
4404                    "Forwarding checkpoint request to leader node at HTTP address {}",
4405                    leader_rpc
4406                );
4407                return self.forward_checkpoint_to_leader(&leader_rpc).await;
4408            }
4409        }
4410
4411        // Route through the live streaming coordinator so the manual checkpoint observes the
4412        // same source, operator, and sink cut as a periodic barrier checkpoint.
4413        let tx = self.force_ckpt_tx.lock().clone().ok_or_else(|| {
4414            DbError::Checkpoint(
4415                "manual checkpoint coordinator is not running — call start() first".into(),
4416            )
4417        })?;
4418        let (reply_tx, reply_rx) = crossfire::oneshot::oneshot();
4419        tx.send(reply_tx).await.map_err(|_| {
4420            DbError::Checkpoint(
4421                "manual checkpoint receiver closed — engine may be shutting down".into(),
4422            )
4423        })?;
4424        let result = reply_rx.await.map_err(|_| {
4425            DbError::Checkpoint("manual checkpoint ended without a terminal reply".into())
4426        })?;
4427
4428        result
4429    }
4430
4431    #[cfg(feature = "cluster")]
4432    async fn forward_checkpoint_to_leader(
4433        &self,
4434        addr: &str,
4435    ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
4436        #[derive(serde::Deserialize)]
4437        struct ForwardedCheckpointResponse {
4438            success: bool,
4439            checkpoint_id: u64,
4440            epoch: u64,
4441            duration_ms: u64,
4442            error: Option<String>,
4443            failure_disposition:
4444                Option<crate::checkpoint_coordinator::CheckpointFailureDisposition>,
4445        }
4446
4447        let mut req = reqwest::Client::new()
4448            .post(format!("http://{addr}/api/v1/checkpoint"))
4449            .timeout(std::time::Duration::from_secs(10));
4450        if let Some(token) = &self.config.http_auth_token {
4451            req = req.bearer_auth(token.expose());
4452        }
4453        let resp = req.send().await.map_err(|e| {
4454            DbError::Checkpoint(format!(
4455                "failed to forward checkpoint to leader at {addr}: {e}"
4456            ))
4457        })?;
4458
4459        let status = resp.status();
4460        let body = resp.text().await.map_err(|e| {
4461            DbError::Checkpoint(format!("failed to read leader checkpoint response: {e}"))
4462        })?;
4463
4464        // The leader returns a `CheckpointResponse` body even on failure (HTTP 500 +
4465        // `success: false`), so parse it to relay structured failure. A non-`CheckpointResponse`
4466        // body (e.g. a 401 payload) is an auth/transport failure — surface status and body.
4467        match serde_json::from_str::<ForwardedCheckpointResponse>(&body) {
4468            Ok(response) => Ok(crate::checkpoint_coordinator::CheckpointResult {
4469                success: response.success,
4470                checkpoint_id: response.checkpoint_id,
4471                epoch: response.epoch,
4472                duration: std::time::Duration::from_millis(response.duration_ms),
4473                error: response.error,
4474                failure_disposition: response.failure_disposition,
4475            }),
4476            Err(_) => Err(DbError::Checkpoint(format!(
4477                "leader rejected checkpoint ({status}): {body}"
4478            ))),
4479        }
4480    }
4481
4482    /// Returns checkpoint performance statistics, or `None` if the coordinator is not initialized.
4483    pub async fn checkpoint_stats(&self) -> Option<crate::checkpoint_coordinator::CheckpointStats> {
4484        let guard = self.coordinator.lock().await;
4485        guard
4486            .as_ref()
4487            .map(crate::checkpoint_coordinator::CheckpointCoordinator::stats)
4488    }
4489}
4490
4491impl std::fmt::Debug for LaminarDB {
4492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4493        f.debug_struct("LaminarDB")
4494            .field("runtime_mode", &self.runtime_mode)
4495            .field("sources", &self.catalog.list_sources().len())
4496            .field("sinks", &self.catalog.list_sinks().len())
4497            .field("materialized_views", &self.mv_registry.lock().len())
4498            .field("checkpoint_enabled", &self.is_checkpoint_enabled())
4499            .field("shutdown", &self.is_closed())
4500            .finish_non_exhaustive()
4501    }
4502}
4503
4504/// `DefaultPhysicalPlanner` with lookup-join extension support.
4505struct LookupQueryPlanner {
4506    extension_planner: Arc<dyn datafusion::physical_planner::ExtensionPlanner + Send + Sync>,
4507}
4508
4509impl std::fmt::Debug for LookupQueryPlanner {
4510    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4511        f.debug_struct("LookupQueryPlanner").finish_non_exhaustive()
4512    }
4513}
4514
4515#[async_trait::async_trait]
4516impl datafusion::execution::context::QueryPlanner for LookupQueryPlanner {
4517    async fn create_physical_plan(
4518        &self,
4519        logical_plan: &datafusion::logical_expr::LogicalPlan,
4520        session_state: &datafusion::execution::SessionState,
4521    ) -> datafusion_common::Result<Arc<dyn datafusion::physical_plan::ExecutionPlan>> {
4522        use datafusion::physical_planner::PhysicalPlanner;
4523        let planner =
4524            datafusion::physical_planner::DefaultPhysicalPlanner::with_extension_planners(vec![
4525                Arc::clone(&self.extension_planner),
4526            ]);
4527        planner
4528            .create_physical_plan(logical_plan, session_state)
4529            .await
4530    }
4531}
4532
4533#[cfg(test)]
4534mod tests;