1use laminar_core::error_codes;
4
5#[derive(Debug, thiserror::Error)]
7pub enum DbError {
8 Sql(#[from] laminar_sql::Error),
10
11 Engine(#[from] laminar_core::Error),
13
14 Streaming(#[from] laminar_core::streaming::StreamingError),
16
17 DataFusion(#[from] datafusion_common::DataFusionError),
19
20 SourceNotFound(String),
22
23 SinkNotFound(String),
25
26 QueryNotFound(String),
28
29 SourceAlreadyExists(String),
31
32 SinkAlreadyExists(String),
34
35 StreamNotFound(String),
37
38 StreamAlreadyExists(String),
40
41 TableNotFound(String),
43
44 TableAlreadyExists(String),
46
47 ReferenceTableQuotaExceeded {
49 table: String,
51 rows: usize,
53 bytes: usize,
55 max_rows: usize,
57 max_bytes: usize,
59 },
60
61 InsertError(String),
63
64 SchemaMismatch(String),
66
67 InvalidOperation(String),
69
70 SubscriptionReplayPruned {
72 name: String,
74 requested: u64,
76 earliest_retained: u64,
78 },
79
80 SubscriptionEpochNotCommitted {
82 name: String,
84 requested: u64,
86 latest_committed: Option<u64>,
88 },
89
90 SubscriptionSequencePruned {
92 name: String,
94 requested_sequence: u64,
96 earliest_retained_sequence: Option<u64>,
98 },
99
100 SubscriptionSequenceNotPublished {
102 name: String,
104 requested_sequence: u64,
106 next_sequence: u64,
108 },
109
110 Subscription(#[from] crate::subscription::ClusterSubscriptionError),
112
113 #[cfg(feature = "cluster")]
115 Topology(#[from] laminar_core::cluster::control::TopologyError),
116
117 #[cfg(feature = "cluster")]
119 TopologySubmission {
120 operation_id: laminar_core::cluster::control::TopologyOperationId,
122 source: Box<Self>,
124 },
125
126 SqlParse(#[from] laminar_sql::parser::ParseError),
128
129 Shutdown,
131
132 Checkpoint(String),
134
135 CheckpointStore(#[from] laminar_core::checkpoint::checkpoint_store::CheckpointStoreError),
137
138 UnresolvedConfigVar(String),
140
141 Connector(String),
143
144 ConnectorOp(#[from] laminar_connectors::error::ConnectorError),
146
147 Pipeline(String),
149
150 PipelineTerminal(String),
152
153 BackpressureFail(String),
155
156 GraphBufferBudgetExceeded {
159 node: String,
161 port: u8,
163 batches: usize,
165 bytes: usize,
167 max_batches: usize,
169 max_bytes: Option<usize>,
171 },
172
173 ShuffleNotReady(String),
176
177 ShuffleTerminal(String),
180
181 ShufflePartialSend(String),
186
187 ShuffleSendStalled(String),
191
192 StatefulOperatorPartialApply(String),
197
198 ManagedStateBudgetExceeded {
202 context: String,
204 accounted_bytes: usize,
206 limit_bytes: usize,
208 },
209
210 GroupLimitExceeded {
213 context: String,
215 groups: usize,
217 limit: usize,
219 },
220
221 QueryPipeline {
224 context: String,
226 translated: String,
229 },
230
231 MaterializedView(String),
233
234 CommittedRead(crate::mv_store::CommittedReadError),
236
237 MaterializedViewQuotaExceeded {
239 view: String,
241 rows: usize,
243 bytes: usize,
245 max_rows: usize,
247 max_bytes: usize,
249 },
250
251 Storage(String),
253
254 Config(String),
256
257 Unsupported(String),
259}
260
261impl DbError {
262 pub fn query_pipeline(
267 context: impl Into<String>,
268 df_error: &datafusion_common::DataFusionError,
269 ) -> Self {
270 let translated = laminar_sql::error::translate_datafusion_error(&df_error.to_string());
271 Self::QueryPipeline {
272 context: context.into(),
273 translated: translated.to_string(),
274 }
275 }
276
277 pub fn query_pipeline_with_columns(
280 context: impl Into<String>,
281 df_error: &datafusion_common::DataFusionError,
282 available_columns: &[&str],
283 ) -> Self {
284 let translated = laminar_sql::error::translate_datafusion_error_with_context(
285 &df_error.to_string(),
286 Some(available_columns),
287 );
288 Self::QueryPipeline {
289 context: context.into(),
290 translated: translated.to_string(),
291 }
292 }
293
294 pub fn query_pipeline_arrow(
296 context: impl Into<String>,
297 arrow_error: &arrow::error::ArrowError,
298 ) -> Self {
299 let translated = laminar_sql::error::translate_datafusion_error(&arrow_error.to_string());
300 Self::QueryPipeline {
301 context: context.into(),
302 translated: translated.to_string(),
303 }
304 }
305
306 #[must_use]
311 pub fn code(&self) -> &'static str {
312 match self {
313 Self::Sql(_) | Self::SqlParse(_) => error_codes::SQL_UNSUPPORTED,
314 Self::Engine(_) | Self::Streaming(_) => error_codes::INTERNAL,
315 Self::DataFusion(_) => error_codes::QUERY_EXECUTION_FAILED,
316 Self::SourceNotFound(_) => error_codes::SOURCE_NOT_FOUND,
317 Self::SinkNotFound(_) => error_codes::SINK_NOT_FOUND,
318 Self::QueryNotFound(_) | Self::StreamNotFound(_) | Self::TableNotFound(_) => {
319 error_codes::SQL_TABLE_NOT_FOUND
320 }
321 Self::SourceAlreadyExists(_)
322 | Self::StreamAlreadyExists(_)
323 | Self::TableAlreadyExists(_) => error_codes::SOURCE_ALREADY_EXISTS,
324 Self::SinkAlreadyExists(_) => error_codes::SINK_ALREADY_EXISTS,
325 Self::InsertError(_) => error_codes::CONNECTOR_WRITE_ERROR,
326 Self::SchemaMismatch(_) => error_codes::SCHEMA_MISMATCH,
327 Self::InvalidOperation(_)
328 | Self::ReferenceTableQuotaExceeded { .. }
329 | Self::SubscriptionReplayPruned { .. }
330 | Self::SubscriptionEpochNotCommitted { .. }
331 | Self::SubscriptionSequencePruned { .. }
332 | Self::SubscriptionSequenceNotPublished { .. }
333 | Self::Unsupported(_) => error_codes::INVALID_OPERATION,
334 Self::Subscription(error) => error.code(),
335 #[cfg(feature = "cluster")]
336 Self::Topology(error) => error.code(),
337 #[cfg(feature = "cluster")]
338 Self::TopologySubmission { source, .. } => source.code(),
339 Self::Shutdown => error_codes::SHUTDOWN,
340 Self::Checkpoint(_) | Self::CheckpointStore(_) => error_codes::CHECKPOINT_FAILED,
341 Self::UnresolvedConfigVar(_) => error_codes::UNRESOLVED_CONFIG_VAR,
342 Self::Connector(_) | Self::ConnectorOp(_) => error_codes::CONNECTOR_CONNECTION_FAILED,
343 Self::Pipeline(_)
344 | Self::PipelineTerminal(_)
345 | Self::BackpressureFail(_)
346 | Self::GraphBufferBudgetExceeded { .. }
347 | Self::ShuffleNotReady(_)
348 | Self::ShuffleTerminal(_)
349 | Self::ShufflePartialSend(_)
350 | Self::ShuffleSendStalled(_)
351 | Self::StatefulOperatorPartialApply(_) => error_codes::PIPELINE_ERROR,
352 Self::ManagedStateBudgetExceeded { .. } | Self::GroupLimitExceeded { .. } => {
353 error_codes::MANAGED_STATE_BUDGET_EXCEEDED
354 }
355 Self::QueryPipeline { .. } => error_codes::QUERY_PIPELINE_ERROR,
356 Self::MaterializedView(_)
357 | Self::MaterializedViewQuotaExceeded { .. }
358 | Self::CommittedRead(_) => error_codes::MATERIALIZED_VIEW_ERROR,
359 Self::Storage(_) => error_codes::WAL_ERROR,
360 Self::Config(_) => error_codes::INVALID_CONFIG,
361 }
362 }
363
364 #[must_use]
366 pub fn is_shuffle_not_ready(&self) -> bool {
367 matches!(self, Self::ShuffleNotReady(_))
368 }
369
370 #[must_use]
372 pub fn requires_pipeline_halt(&self) -> bool {
373 matches!(
374 self,
375 Self::PipelineTerminal(_)
376 | Self::BackpressureFail(_)
377 | Self::GraphBufferBudgetExceeded { .. }
378 | Self::ShuffleTerminal(_)
379 | Self::ManagedStateBudgetExceeded { .. }
380 | Self::GroupLimitExceeded { .. }
381 | Self::MaterializedViewQuotaExceeded { .. }
382 )
383 }
384
385 #[must_use]
387 pub fn requires_pipeline_recovery(&self) -> bool {
388 matches!(
389 self,
390 Self::Checkpoint(_)
391 | Self::ShufflePartialSend(_)
392 | Self::ShuffleSendStalled(_)
393 | Self::StatefulOperatorPartialApply(_)
394 )
395 }
396
397 #[must_use]
399 pub fn is_transient(&self) -> bool {
400 match self {
401 Self::Streaming(_)
402 | Self::Connector(_)
403 | Self::Checkpoint(_)
404 | Self::CheckpointStore(_) => true,
405 Self::ConnectorOp(e) => e.is_transient(),
406 _ => false,
407 }
408 }
409}
410
411impl std::fmt::Display for DbError {
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 match self {
414 Self::Sql(e) => write!(f, "SQL error: {e}"),
415 Self::Engine(e) => write!(f, "Engine error: {e}"),
416 Self::Streaming(e) => write!(f, "Streaming error: {e}"),
417 Self::DataFusion(e) => {
418 let translated = laminar_sql::error::translate_datafusion_error(&e.to_string());
419 write!(f, "{translated}")
420 }
421 Self::SourceNotFound(name) => {
422 write!(f, "[{}] Source '{name}' not found", self.code())
423 }
424 Self::SinkNotFound(name) => {
425 write!(f, "[{}] Sink '{name}' not found", self.code())
426 }
427 Self::QueryNotFound(name) => {
428 write!(f, "[{}] Query '{name}' not found", self.code())
429 }
430 Self::SourceAlreadyExists(name) => {
431 write!(f, "[{}] Source '{name}' already exists", self.code())
432 }
433 Self::SinkAlreadyExists(name) => {
434 write!(f, "[{}] Sink '{name}' already exists", self.code())
435 }
436 Self::StreamNotFound(name) => {
437 write!(f, "[{}] Stream '{name}' not found", self.code())
438 }
439 Self::StreamAlreadyExists(name) => {
440 write!(f, "[{}] Stream '{name}' already exists", self.code())
441 }
442 Self::TableNotFound(name) => {
443 write!(f, "[{}] Table '{name}' not found", self.code())
444 }
445 Self::TableAlreadyExists(name) => {
446 write!(f, "[{}] Table '{name}' already exists", self.code())
447 }
448 Self::InsertError(msg) => {
449 write!(f, "[{}] Insert error: {msg}", self.code())
450 }
451 Self::SchemaMismatch(msg) => {
452 write!(f, "[{}] Schema mismatch: {msg}", self.code())
453 }
454 Self::InvalidOperation(msg) => {
455 write!(f, "[{}] Invalid operation: {msg}", self.code())
456 }
457 Self::SubscriptionReplayPruned { .. }
458 | Self::SubscriptionEpochNotCommitted { .. }
459 | Self::SubscriptionSequencePruned { .. }
460 | Self::SubscriptionSequenceNotPublished { .. } => self.fmt_subscription_start_error(f),
461 Self::Subscription(error) => write!(f, "[{}] {error}", self.code()),
462 #[cfg(feature = "cluster")]
463 Self::Topology(error) => write!(f, "{error}"),
464 #[cfg(feature = "cluster")]
465 Self::TopologySubmission { operation_id, source } => write!(f,
466 "{source}; topology operation {} may be admitted; query status and retry the same identity",
467 operation_id.get()),
468 Self::SqlParse(e) => write!(f, "SQL parse error: {e}"),
469 Self::Shutdown => write!(f, "[{}] Database is shut down", self.code()),
470 Self::Checkpoint(msg) => {
471 write!(f, "[{}] Checkpoint error: {msg}", self.code())
472 }
473 Self::CheckpointStore(e) => {
474 write!(f, "[{}] Checkpoint store error: {e}", self.code())
475 }
476 Self::UnresolvedConfigVar(msg) => {
477 write!(f, "[{}] Unresolved config variable: {msg}", self.code())
478 }
479 Self::Connector(msg) => {
480 write!(f, "[{}] Connector error: {msg}", self.code())
481 }
482 Self::ConnectorOp(e) => {
483 write!(f, "[{}] Connector error: {e}", self.code())
484 }
485 Self::Pipeline(_)
486 | Self::PipelineTerminal(_)
487 | Self::BackpressureFail(_)
488 | Self::GraphBufferBudgetExceeded { .. }
489 | Self::ShuffleNotReady(_)
490 | Self::ShuffleTerminal(_)
491 | Self::ShufflePartialSend(_)
492 | Self::ShuffleSendStalled(_)
493 | Self::StatefulOperatorPartialApply(_)
494 | Self::ReferenceTableQuotaExceeded { .. }
495 | Self::MaterializedViewQuotaExceeded { .. }
496 | Self::ManagedStateBudgetExceeded { .. }
497 | Self::GroupLimitExceeded { .. } => self.fmt_execution_error(f),
498 Self::QueryPipeline {
499 context,
500 translated,
501 } => write!(f, "Stream '{context}': {translated}"),
502 Self::MaterializedView(msg) => {
503 write!(f, "[{}] Materialized view error: {msg}", self.code())
504 }
505 Self::CommittedRead(error) => {
506 write!(f, "[{}] Committed read: {error}", self.code())
507 }
508 Self::Storage(msg) => {
509 write!(f, "[{}] Storage error: {msg}", self.code())
510 }
511 Self::Config(msg) => {
512 write!(f, "[{}] Config error: {msg}", self.code())
513 }
514 Self::Unsupported(msg) => {
515 write!(f, "[{}] Unsupported: {msg}", self.code())
516 }
517 }
518 }
519}
520
521impl DbError {
522 fn fmt_subscription_start_error(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523 match self {
524 Self::SubscriptionReplayPruned {
525 name,
526 requested,
527 earliest_retained,
528 } => write!(
529 f,
530 "[{}] Epoch {requested} for stream '{name}' is no longer retained (earliest retained is {earliest_retained})",
531 self.code()
532 ),
533 Self::SubscriptionEpochNotCommitted {
534 name,
535 requested,
536 latest_committed,
537 } => match latest_committed {
538 Some(latest) => write!(
539 f,
540 "[{}] Epoch {requested} for stream '{name}' is not committed (latest committed is {latest})",
541 self.code()
542 ),
543 None => write!(
544 f,
545 "[{}] Epoch {requested} for stream '{name}' is not committed (no committed epoch is available)",
546 self.code()
547 ),
548 },
549 Self::SubscriptionSequencePruned {
550 name,
551 requested_sequence,
552 earliest_retained_sequence,
553 } => match earliest_retained_sequence {
554 Some(earliest) => write!(
555 f,
556 "[{}] Sequence {requested_sequence} for stream '{name}' is no longer retained (earliest retained sequence is {earliest})",
557 self.code()
558 ),
559 None => write!(
560 f,
561 "[{}] Sequence {requested_sequence} for stream '{name}' is no longer retained (no replay history is retained)",
562 self.code()
563 ),
564 },
565 Self::SubscriptionSequenceNotPublished {
566 name,
567 requested_sequence,
568 next_sequence,
569 } => write!(
570 f,
571 "[{}] Sequence {requested_sequence} for stream '{name}' has not been published (next sequence is {next_sequence})",
572 self.code()
573 ),
574 _ => unreachable!("subscription-start formatting is dispatched only for start errors"),
575 }
576 }
577
578 fn fmt_execution_error(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
579 match self {
580 Self::Pipeline(msg) => {
581 write!(f, "[{}] Pipeline error: {msg}", self.code())
582 }
583 Self::PipelineTerminal(msg) => {
584 write!(f, "[{}] Terminal pipeline error: {msg}", self.code())
585 }
586 Self::BackpressureFail(msg) => {
587 write!(f, "[{}] Backpressure fail: {msg}", self.code())
588 }
589 Self::GraphBufferBudgetExceeded {
590 node, port, batches, bytes, max_batches, max_bytes,
591 } => write!(
592 f,
593 "[{}] Graph input budget exceeded at '{node}' port {port}: projected={batches} batches/{bytes} bytes, limits={max_batches} batches/{max_bytes:?} bytes; reduce batch size or increase graph input limits; terminal fault resolution is required before restarting",
594 self.code()
595 ),
596 Self::ShuffleNotReady(msg) => {
597 write!(f, "[{}] Shuffle target not ready: {msg}", self.code())
598 }
599 Self::ShuffleTerminal(msg) => {
600 write!(f, "[{}] Terminal shuffle routing error: {msg}", self.code())
601 }
602 Self::ShufflePartialSend(msg) => {
603 write!(f, "[{}] Shuffle partial send: {msg}", self.code())
604 }
605 Self::ShuffleSendStalled(msg) => {
606 write!(f, "[{}] Shuffle send stalled: {msg}", self.code())
607 }
608 Self::StatefulOperatorPartialApply(msg) => {
609 write!(
610 f,
611 "[{}] Stateful operator partial apply: {msg}",
612 self.code()
613 )
614 }
615 Self::ManagedStateBudgetExceeded {
616 context,
617 accounted_bytes,
618 limit_bytes,
619 } => write!(
620 f,
621 "[{}] Managed state budget exceeded during {context}: accounted={accounted_bytes} bytes, limit={limit_bytes} bytes",
622 self.code()
623 ),
624 Self::GroupLimitExceeded {
625 context,
626 groups,
627 limit,
628 } => write!(
629 f,
630 "[{}] Group limit exceeded during {context}: groups={groups}, limit={limit}",
631 self.code()
632 ),
633 Self::MaterializedViewQuotaExceeded { view, rows, bytes, max_rows, max_bytes } => write!(
634 f, "[{}] Materialized-view '{view}' quota exceeded: projected={rows} rows/{bytes} bytes, limits={max_rows} rows/{max_bytes} bytes", self.code()
635 ),
636 Self::ReferenceTableQuotaExceeded { table, rows, bytes, max_rows, max_bytes } => write!(
637 f, "[{}] Reference-table '{table}' quota exceeded: projected={rows} rows/{bytes} bytes, limits={max_rows} rows/{max_bytes} bytes", self.code()
638 ),
639 _ => unreachable!("execution formatting is dispatched only for execution errors"),
640 }
641 }
642}