1use std::marker::PhantomData;
4use std::sync::Arc;
5use std::time::Duration;
6
7use arrow::array::RecordBatch;
8use arrow::datatypes::SchemaRef;
9
10use laminar_core::streaming::{Record, Subscription};
11
12use crate::catalog::{ArrowRecord, SourceEntry};
13use crate::DbError;
14
15#[derive(Debug)]
17pub enum ExecuteResult {
18 Ddl(DdlInfo),
20 Query(QueryHandle),
22 RowsAffected(u64),
24 Metadata(RecordBatch),
26}
27
28impl ExecuteResult {
29 pub fn into_query(self) -> Result<QueryHandle, DbError> {
34 match self {
35 Self::Query(q) => Ok(q),
36 _ => Err(DbError::InvalidOperation(
37 "Expected a query result".to_string(),
38 )),
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct DdlInfo {
46 pub statement_type: String,
48 pub object_name: String,
50 pub(crate) applied: bool,
53}
54
55#[derive(Debug)]
57pub struct QueryHandle {
58 pub(crate) id: u64,
59 pub(crate) schema: SchemaRef,
60 pub(crate) sql: String,
61 pub(crate) subscription: Option<Subscription<ArrowRecord>>,
62 pub(crate) active: bool,
63 pub(crate) cancel_token: tokio_util::sync::CancellationToken,
64}
65
66impl QueryHandle {
67 #[must_use]
69 pub fn schema(&self) -> &SchemaRef {
70 &self.schema
71 }
72
73 #[must_use]
75 pub fn sql(&self) -> &str {
76 &self.sql
77 }
78
79 #[must_use]
81 pub fn id(&self) -> u64 {
82 self.id
83 }
84
85 #[must_use]
87 pub fn is_active(&self) -> bool {
88 self.active
89 }
90
91 pub fn subscribe_raw(&mut self) -> Result<Subscription<ArrowRecord>, DbError> {
98 self.subscription
99 .take()
100 .ok_or_else(|| DbError::InvalidOperation("Subscription already consumed".to_string()))
101 }
102
103 pub fn cancel(&mut self) {
105 self.active = false;
106 self.subscription = None;
107 self.cancel_token.cancel();
108 }
109}
110
111impl Drop for QueryHandle {
112 fn drop(&mut self) {
113 self.cancel_token.cancel();
114 }
115}
116
117pub trait FromBatch: Sized {
119 fn from_batch(batch: &RecordBatch, row: usize) -> Self;
121 fn from_batch_all(batch: &RecordBatch) -> Vec<Self>;
123}
124
125#[derive(Debug)]
127pub enum TypedSubscriptionFrame<T> {
128 Rows {
130 rows: Vec<T>,
132 sequence: u64,
134 },
135 Barrier {
137 sequence: u64,
139 epoch: u64,
141 checkpoint_id: u64,
143 through_sequence: u64,
145 },
146}
147
148#[derive(Debug)]
150pub struct TypedSubscriptionEnvelope<T> {
151 pub frame: TypedSubscriptionFrame<T>,
153 pub cluster: Option<crate::subscription::ClusterSubscriptionFrameMetadata>,
155}
156
157#[derive(Debug, thiserror::Error, PartialEq, Eq)]
159pub enum SubscriptionError {
160 #[error("subscription fell behind by {skipped} entries")]
162 Lagged {
163 skipped: u64,
165 },
166 #[error("subscription failed: {message}")]
168 Failed {
169 message: String,
171 },
172}
173
174pub struct TypedSubscription<T: FromBatch> {
176 portal: crate::subscription::SubscriptionPortal,
177 _phantom: PhantomData<T>,
178}
179
180impl<T: FromBatch> TypedSubscription<T> {
181 pub(crate) fn new(portal: crate::subscription::SubscriptionPortal) -> Self {
182 Self {
183 portal,
184 _phantom: PhantomData,
185 }
186 }
187
188 #[must_use]
190 pub fn schema(&self) -> SchemaRef {
191 self.portal.schema()
192 }
193
194 pub async fn next_frame(
199 &mut self,
200 ) -> Result<Option<TypedSubscriptionFrame<T>>, SubscriptionError> {
201 Ok(self.next_envelope().await?.map(|envelope| envelope.frame))
202 }
203
204 pub async fn next_envelope(
209 &mut self,
210 ) -> Result<Option<TypedSubscriptionEnvelope<T>>, SubscriptionError> {
211 let Some(envelope) = self.portal.next_envelope().await else {
212 return Ok(None);
213 };
214 Ok(Some(TypedSubscriptionEnvelope {
215 frame: convert_typed_frame(envelope.frame)?,
216 cluster: envelope.cluster,
217 }))
218 }
219
220 pub fn try_next_frame(
225 &mut self,
226 ) -> Result<Option<TypedSubscriptionFrame<T>>, SubscriptionError> {
227 Ok(self.try_next_envelope()?.map(|envelope| envelope.frame))
228 }
229
230 pub fn try_next_envelope(
235 &mut self,
236 ) -> Result<Option<TypedSubscriptionEnvelope<T>>, SubscriptionError> {
237 let Some(envelope) = self.portal.try_next_envelope() else {
238 return Ok(None);
239 };
240 Ok(Some(TypedSubscriptionEnvelope {
241 frame: convert_typed_frame(envelope.frame)?,
242 cluster: envelope.cluster,
243 }))
244 }
245
246 pub fn poll(&mut self) -> Result<Option<Vec<T>>, SubscriptionError> {
251 loop {
252 match self.try_next_frame()? {
253 Some(TypedSubscriptionFrame::Rows { rows, .. }) => return Ok(Some(rows)),
254 Some(TypedSubscriptionFrame::Barrier { .. }) => {}
255 None => return Ok(None),
256 }
257 }
258 }
259}
260
261fn convert_typed_frame<T: FromBatch>(
262 frame: crate::subscription::PortalFrame,
263) -> Result<TypedSubscriptionFrame<T>, SubscriptionError> {
264 match frame {
265 crate::subscription::PortalFrame::Batch {
266 batch,
267 sequence,
268 lease: _lease,
269 } => Ok(TypedSubscriptionFrame::Rows {
270 rows: T::from_batch_all(&batch),
271 sequence,
272 }),
273 crate::subscription::PortalFrame::Barrier {
274 sequence,
275 epoch,
276 checkpoint_id,
277 through_sequence,
278 } => Ok(TypedSubscriptionFrame::Barrier {
279 sequence,
280 epoch,
281 checkpoint_id,
282 through_sequence,
283 }),
284 crate::subscription::PortalFrame::Lagged(skipped) => {
285 Err(SubscriptionError::Lagged { skipped })
286 }
287 crate::subscription::PortalFrame::Error { message } => {
288 Err(SubscriptionError::Failed { message })
289 }
290 }
291}
292
293impl<T: FromBatch> std::fmt::Debug for TypedSubscription<T> {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 f.debug_struct("TypedSubscription")
296 .field("portal", &self.portal)
297 .finish()
298 }
299}
300
301#[cfg(test)]
302mod typed_subscription_tests {
303 use super::*;
304
305 #[derive(Debug)]
306 struct Row;
307
308 impl FromBatch for Row {
309 fn from_batch(_batch: &RecordBatch, _row: usize) -> Self {
310 Self
311 }
312
313 fn from_batch_all(_batch: &RecordBatch) -> Vec<Self> {
314 Vec::new()
315 }
316 }
317
318 #[test]
319 fn delivery_gap_is_not_converted_to_empty_poll() {
320 let error =
321 convert_typed_frame::<Row>(crate::subscription::PortalFrame::Lagged(7)).unwrap_err();
322 assert_eq!(error, SubscriptionError::Lagged { skipped: 7 });
323 }
324
325 #[test]
326 fn delivery_failure_preserves_detail() {
327 let error = convert_typed_frame::<Row>(crate::subscription::PortalFrame::Error {
328 message: "object dropped".into(),
329 })
330 .unwrap_err();
331 assert_eq!(
332 error,
333 SubscriptionError::Failed {
334 message: "object dropped".into()
335 }
336 );
337 }
338}
339
340pub struct SourceHandle<T: Record> {
342 entry: Arc<SourceEntry>,
343 _phantom: PhantomData<T>,
344}
345
346impl<T: Record> SourceHandle<T> {
347 pub(crate) fn new(entry: Arc<SourceEntry>) -> Result<Self, DbError> {
349 let rust_schema = T::schema();
350 let sql_schema = &entry.schema;
351
352 if rust_schema.fields().len() != sql_schema.fields().len() {
354 return Err(DbError::SchemaMismatch(format!(
355 "Rust type has {} fields but source '{}' has {} columns",
356 rust_schema.fields().len(),
357 entry.name,
358 sql_schema.fields().len()
359 )));
360 }
361
362 Ok(Self {
363 entry,
364 _phantom: PhantomData,
365 })
366 }
367
368 #[allow(clippy::needless_pass_by_value)]
373 pub fn push(&self, record: T) -> Result<(), laminar_core::streaming::StreamingError> {
374 let batch = record.to_record_batch();
375 self.entry.push_and_buffer(batch)
376 }
377
378 pub fn push_batch(&self, records: impl IntoIterator<Item = T>) -> usize {
380 const BATCH_SIZE: usize = 1024;
381 let mut count = 0;
382 let mut buffer = Vec::with_capacity(BATCH_SIZE);
383
384 for record in records {
385 buffer.push(record);
386 if buffer.len() >= BATCH_SIZE {
387 let batch = T::to_record_batch_from_iter(buffer.drain(..));
388 if self.push_arrow(batch).is_err() {
389 return count;
390 }
391 count += BATCH_SIZE;
392 }
393 }
394
395 if !buffer.is_empty() {
396 let len = buffer.len();
397 let batch = T::to_record_batch_from_iter(buffer);
398 if self.push_arrow(batch).is_ok() {
399 count += len;
400 }
401 }
402 count
403 }
404
405 pub fn push_arrow(
410 &self,
411 batch: RecordBatch,
412 ) -> Result<(), laminar_core::streaming::StreamingError> {
413 self.entry.push_and_buffer(batch)
414 }
415
416 pub fn watermark(&self, timestamp: i64) {
418 self.entry.source.watermark(timestamp);
419 }
420
421 #[must_use]
423 pub fn current_watermark(&self) -> i64 {
424 self.entry.source.current_watermark()
425 }
426
427 #[must_use]
429 pub fn pending(&self) -> usize {
430 self.entry.source.pending()
431 }
432
433 #[must_use]
435 pub fn capacity(&self) -> usize {
436 self.entry.source.capacity()
437 }
438
439 #[must_use]
441 pub fn is_backpressured(&self) -> bool {
442 crate::metrics::is_backpressured(self.pending(), self.capacity())
443 }
444
445 #[must_use]
447 pub fn name(&self) -> &str {
448 &self.entry.name
449 }
450
451 #[must_use]
453 pub fn schema(&self) -> &SchemaRef {
454 &self.entry.schema
455 }
456
457 #[must_use]
459 pub fn max_out_of_orderness(&self) -> Option<Duration> {
460 self.entry.max_out_of_orderness
461 }
462
463 pub fn set_event_time_column(&self, column: &str) {
465 self.entry.source.set_event_time_column(column);
466 }
467}
468
469impl<T: Record> std::fmt::Debug for SourceHandle<T> {
470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 f.debug_struct("SourceHandle")
472 .field("name", &self.entry.name)
473 .field("pending", &self.pending())
474 .finish()
475 }
476}
477
478pub struct UntypedSourceHandle {
480 entry: Arc<SourceEntry>,
481}
482
483impl UntypedSourceHandle {
484 pub(crate) fn new(entry: Arc<SourceEntry>) -> Self {
485 Self { entry }
486 }
487
488 pub fn push_arrow(
493 &self,
494 batch: RecordBatch,
495 ) -> Result<(), laminar_core::streaming::StreamingError> {
496 self.entry.push_and_buffer(batch)
497 }
498
499 pub fn watermark(&self, timestamp: i64) {
501 self.entry.source.watermark(timestamp);
502 }
503
504 #[must_use]
506 pub fn current_watermark(&self) -> i64 {
507 self.entry.source.current_watermark()
508 }
509
510 #[must_use]
512 pub fn pending(&self) -> usize {
513 self.entry.source.pending()
514 }
515
516 #[must_use]
518 pub fn capacity(&self) -> usize {
519 self.entry.source.capacity()
520 }
521
522 #[must_use]
524 pub fn is_backpressured(&self) -> bool {
525 crate::metrics::is_backpressured(self.pending(), self.capacity())
526 }
527
528 #[must_use]
530 pub fn name(&self) -> &str {
531 &self.entry.name
532 }
533
534 #[must_use]
536 pub fn schema(&self) -> &SchemaRef {
537 &self.entry.schema
538 }
539
540 #[must_use]
542 pub fn max_out_of_orderness(&self) -> Option<Duration> {
543 self.entry.max_out_of_orderness
544 }
545
546 pub fn set_event_time_column(&self, column: &str) {
548 self.entry.source.set_event_time_column(column);
549 }
550}
551
552impl std::fmt::Debug for UntypedSourceHandle {
553 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
554 f.debug_struct("UntypedSourceHandle")
555 .field("name", &self.entry.name)
556 .finish()
557 }
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum PipelineNodeType {
563 Source,
565 Stream,
567 Sink,
569}
570
571#[derive(Debug, Clone)]
573pub struct PipelineNode {
574 pub name: String,
576 pub node_type: PipelineNodeType,
578 pub schema: Option<SchemaRef>,
580 pub sql: Option<String>,
582}
583
584#[derive(Debug, Clone)]
586pub struct PipelineEdge {
587 pub from: String,
589 pub to: String,
591}
592
593#[derive(Debug, Clone)]
595pub struct PipelineTopology {
596 pub nodes: Vec<PipelineNode>,
598 pub edges: Vec<PipelineEdge>,
600}
601
602#[derive(Debug, Clone)]
604pub struct StreamInfo {
605 pub name: String,
607 pub sql: Option<String>,
609}
610
611#[derive(Debug, Clone)]
613pub struct SourceInfo {
614 pub name: String,
616 pub schema: SchemaRef,
618 pub watermark_column: Option<String>,
620}
621
622#[derive(Debug, Clone)]
624pub struct SinkInfo {
625 pub name: String,
627}
628
629#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
632pub struct MaterializedViewInfo {
633 pub name: String,
635 pub sql: String,
637 pub state: String,
639}
640
641impl From<&laminar_core::mv::MaterializedView> for MaterializedViewInfo {
642 fn from(view: &laminar_core::mv::MaterializedView) -> Self {
643 Self {
644 name: view.name.clone(),
645 sql: view.sql.clone(),
646 state: format!("{:?}", view.state),
647 }
648 }
649}
650
651#[derive(Debug, Clone)]
653pub struct QueryInfo {
654 pub id: u64,
656 pub sql: String,
658 pub active: bool,
660}