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