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