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;
11
12use crate::catalog::SourceEntry;
13use crate::query_rows::QueryRows;
14use crate::DbError;
15
16#[derive(Debug)]
18pub enum ExecuteResult {
19 Ddl(DdlInfo),
21 Query(QueryHandle),
23 RowsAffected(u64),
25 Metadata(RecordBatch),
27}
28
29impl ExecuteResult {
30 pub fn into_query(self) -> Result<QueryHandle, DbError> {
35 match self {
36 Self::Query(q) => Ok(q),
37 _ => Err(DbError::InvalidOperation(
38 "Expected a query result".to_string(),
39 )),
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct DdlInfo {
47 pub statement_type: String,
49 pub object_name: String,
51 pub(crate) applied: bool,
54 #[cfg(feature = "cluster")]
56 pub topology_operation: Option<Box<laminar_core::cluster::control::TopologyAdmissionStatus>>,
57}
58
59#[derive(Debug)]
61pub struct QueryHandle {
62 pub(crate) id: u64,
63 pub(crate) schema: SchemaRef,
64 pub(crate) sql: String,
65 pub(crate) rows: Option<QueryRows>,
66 pub(crate) active: bool,
67 pub(crate) cancel_token: tokio_util::sync::CancellationToken,
68}
69
70impl QueryHandle {
71 #[must_use]
73 pub fn schema(&self) -> &SchemaRef {
74 &self.schema
75 }
76
77 #[must_use]
79 pub fn sql(&self) -> &str {
80 &self.sql
81 }
82
83 #[must_use]
85 pub fn id(&self) -> u64 {
86 self.id
87 }
88
89 #[must_use]
91 pub fn is_active(&self) -> bool {
92 self.active
93 }
94
95 pub fn subscribe_raw(&mut self) -> Result<QueryRows, DbError> {
103 self.rows
104 .take()
105 .ok_or_else(|| DbError::InvalidOperation("Subscription already consumed".to_string()))
106 }
107
108 pub fn cancel(&mut self) {
110 self.active = false;
111 self.rows = None;
112 self.cancel_token.cancel();
113 }
114}
115
116impl Drop for QueryHandle {
117 fn drop(&mut self) {
118 self.cancel_token.cancel();
119 }
120}
121
122pub trait FromBatch: Sized {
124 fn from_batch(batch: &RecordBatch, row: usize) -> Self;
126 fn from_batch_all(batch: &RecordBatch) -> Vec<Self>;
128}
129
130#[derive(Debug)]
132pub enum TypedSubscriptionFrame<T> {
133 Rows {
135 rows: Vec<T>,
137 sequence: u64,
139 },
140 Barrier {
142 sequence: u64,
144 epoch: u64,
146 checkpoint_id: u64,
148 through_sequence: u64,
150 },
151}
152
153#[derive(Debug)]
155pub struct TypedSubscriptionEnvelope<T> {
156 pub frame: TypedSubscriptionFrame<T>,
158 pub cluster: Option<crate::subscription::ClusterSubscriptionFrameMetadata>,
160}
161
162#[derive(Debug, thiserror::Error, PartialEq, Eq)]
164pub enum SubscriptionError {
165 #[error("subscription fell behind by {skipped} entries")]
167 Lagged {
168 skipped: u64,
170 },
171 #[error("subscription failed: {message}")]
173 Failed {
174 message: String,
176 },
177}
178
179pub struct TypedSubscription<T: FromBatch> {
181 portal: crate::subscription::SubscriptionPortal,
182 _phantom: PhantomData<T>,
183}
184
185impl<T: FromBatch> TypedSubscription<T> {
186 pub(crate) fn new(portal: crate::subscription::SubscriptionPortal) -> Self {
187 Self {
188 portal,
189 _phantom: PhantomData,
190 }
191 }
192
193 #[must_use]
195 pub fn schema(&self) -> SchemaRef {
196 self.portal.schema()
197 }
198
199 pub async fn next_frame(
204 &mut self,
205 ) -> Result<Option<TypedSubscriptionFrame<T>>, SubscriptionError> {
206 Ok(self.next_envelope().await?.map(|envelope| envelope.frame))
207 }
208
209 pub async fn next_envelope(
214 &mut self,
215 ) -> Result<Option<TypedSubscriptionEnvelope<T>>, SubscriptionError> {
216 let Some(envelope) = self.portal.next_envelope().await else {
217 return Ok(None);
218 };
219 Ok(Some(TypedSubscriptionEnvelope {
220 frame: convert_typed_frame(envelope.frame)?,
221 cluster: envelope.cluster,
222 }))
223 }
224
225 pub fn try_next_frame(
230 &mut self,
231 ) -> Result<Option<TypedSubscriptionFrame<T>>, SubscriptionError> {
232 Ok(self.try_next_envelope()?.map(|envelope| envelope.frame))
233 }
234
235 pub fn try_next_envelope(
240 &mut self,
241 ) -> Result<Option<TypedSubscriptionEnvelope<T>>, SubscriptionError> {
242 let Some(envelope) = self.portal.try_next_envelope() else {
243 return Ok(None);
244 };
245 Ok(Some(TypedSubscriptionEnvelope {
246 frame: convert_typed_frame(envelope.frame)?,
247 cluster: envelope.cluster,
248 }))
249 }
250
251 pub fn poll(&mut self) -> Result<Option<Vec<T>>, SubscriptionError> {
256 loop {
257 match self.try_next_frame()? {
258 Some(TypedSubscriptionFrame::Rows { rows, .. }) => return Ok(Some(rows)),
259 Some(TypedSubscriptionFrame::Barrier { .. }) => {}
260 None => return Ok(None),
261 }
262 }
263 }
264}
265
266fn convert_typed_frame<T: FromBatch>(
267 frame: crate::subscription::PortalFrame,
268) -> Result<TypedSubscriptionFrame<T>, SubscriptionError> {
269 match frame {
270 crate::subscription::PortalFrame::Batch {
271 batch,
272 sequence,
273 lease: _lease,
274 } => Ok(TypedSubscriptionFrame::Rows {
275 rows: T::from_batch_all(&batch),
276 sequence,
277 }),
278 crate::subscription::PortalFrame::Barrier {
279 sequence,
280 epoch,
281 checkpoint_id,
282 through_sequence,
283 } => Ok(TypedSubscriptionFrame::Barrier {
284 sequence,
285 epoch,
286 checkpoint_id,
287 through_sequence,
288 }),
289 crate::subscription::PortalFrame::Lagged(skipped) => {
290 Err(SubscriptionError::Lagged { skipped })
291 }
292 crate::subscription::PortalFrame::Error { message } => {
293 Err(SubscriptionError::Failed { message })
294 }
295 }
296}
297
298impl<T: FromBatch> std::fmt::Debug for TypedSubscription<T> {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 f.debug_struct("TypedSubscription")
301 .field("portal", &self.portal)
302 .finish()
303 }
304}
305
306#[cfg(test)]
307mod typed_subscription_tests {
308 use super::*;
309
310 #[derive(Debug)]
311 struct Row;
312
313 impl FromBatch for Row {
314 fn from_batch(_batch: &RecordBatch, _row: usize) -> Self {
315 Self
316 }
317
318 fn from_batch_all(_batch: &RecordBatch) -> Vec<Self> {
319 Vec::new()
320 }
321 }
322
323 #[test]
324 fn delivery_gap_is_not_converted_to_empty_poll() {
325 let error =
326 convert_typed_frame::<Row>(crate::subscription::PortalFrame::Lagged(7)).unwrap_err();
327 assert_eq!(error, SubscriptionError::Lagged { skipped: 7 });
328 }
329
330 #[test]
331 fn delivery_failure_preserves_detail() {
332 let error = convert_typed_frame::<Row>(crate::subscription::PortalFrame::Error {
333 message: "object dropped".into(),
334 })
335 .unwrap_err();
336 assert_eq!(
337 error,
338 SubscriptionError::Failed {
339 message: "object dropped".into()
340 }
341 );
342 }
343}
344
345pub struct SourceHandle<T: Record> {
347 entry: Arc<SourceEntry>,
348 _phantom: PhantomData<T>,
349}
350
351impl<T: Record> SourceHandle<T> {
352 pub(crate) fn new(entry: Arc<SourceEntry>) -> Result<Self, DbError> {
354 let rust_schema = T::schema();
355 let sql_schema = &entry.schema;
356
357 if rust_schema.fields().len() != sql_schema.fields().len() {
359 return Err(DbError::SchemaMismatch(format!(
360 "Rust type has {} fields but source '{}' has {} columns",
361 rust_schema.fields().len(),
362 entry.name,
363 sql_schema.fields().len()
364 )));
365 }
366
367 Ok(Self {
368 entry,
369 _phantom: PhantomData,
370 })
371 }
372
373 #[allow(clippy::needless_pass_by_value)]
379 pub fn push(&self, record: T) -> Result<(), laminar_core::streaming::StreamingError> {
380 let batch = record.to_record_batch();
381 self.entry.push_and_buffer(batch)
382 }
383
384 pub fn push_batch(&self, records: impl IntoIterator<Item = T>) -> usize {
388 const BATCH_SIZE: usize = 1024;
389 let mut count = 0;
390 let mut buffer = Vec::with_capacity(BATCH_SIZE);
391
392 for record in records {
393 buffer.push(record);
394 if buffer.len() >= BATCH_SIZE {
395 let batch = T::to_record_batch_from_iter(buffer.drain(..));
396 if self.push_arrow(batch).is_err() {
397 return count;
398 }
399 count += BATCH_SIZE;
400 }
401 }
402
403 if !buffer.is_empty() {
404 let len = buffer.len();
405 let batch = T::to_record_batch_from_iter(buffer);
406 if self.push_arrow(batch).is_ok() {
407 count += len;
408 }
409 }
410 count
411 }
412
413 pub fn push_arrow(
419 &self,
420 batch: RecordBatch,
421 ) -> Result<(), laminar_core::streaming::StreamingError> {
422 self.entry.push_and_buffer(batch)
423 }
424
425 pub fn watermark(&self, timestamp: i64) {
427 self.entry.source.watermark(timestamp);
428 }
429
430 #[must_use]
432 pub fn current_watermark(&self) -> i64 {
433 self.entry.source.current_watermark()
434 }
435
436 #[must_use]
438 pub fn pending(&self) -> usize {
439 self.entry.source.pending()
440 }
441
442 #[must_use]
444 pub fn capacity(&self) -> usize {
445 self.entry.source.capacity()
446 }
447
448 #[must_use]
450 pub fn is_backpressured(&self) -> bool {
451 self.entry.is_backpressured()
452 }
453
454 #[must_use]
456 pub fn name(&self) -> &str {
457 &self.entry.name
458 }
459
460 #[must_use]
462 pub fn schema(&self) -> &SchemaRef {
463 &self.entry.schema
464 }
465
466 #[must_use]
468 pub fn max_out_of_orderness(&self) -> Option<Duration> {
469 self.entry.max_out_of_orderness
470 }
471
472 pub fn set_event_time_column(&self, column: &str) {
474 self.entry.source.set_event_time_column(column);
475 }
476}
477
478impl<T: Record> std::fmt::Debug for SourceHandle<T> {
479 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480 f.debug_struct("SourceHandle")
481 .field("name", &self.entry.name)
482 .field("pending", &self.pending())
483 .finish()
484 }
485}
486
487pub struct UntypedSourceHandle {
489 entry: Arc<SourceEntry>,
490}
491
492impl UntypedSourceHandle {
493 pub(crate) fn new(entry: Arc<SourceEntry>) -> Self {
494 Self { entry }
495 }
496
497 pub fn push_arrow(
503 &self,
504 batch: RecordBatch,
505 ) -> Result<(), laminar_core::streaming::StreamingError> {
506 self.entry.push_and_buffer(batch)
507 }
508
509 pub fn watermark(&self, timestamp: i64) {
511 self.entry.source.watermark(timestamp);
512 }
513
514 #[must_use]
516 pub fn current_watermark(&self) -> i64 {
517 self.entry.source.current_watermark()
518 }
519
520 #[must_use]
522 pub fn pending(&self) -> usize {
523 self.entry.source.pending()
524 }
525
526 #[must_use]
528 pub fn capacity(&self) -> usize {
529 self.entry.source.capacity()
530 }
531
532 #[must_use]
534 pub fn is_backpressured(&self) -> bool {
535 self.entry.is_backpressured()
536 }
537
538 #[must_use]
540 pub fn name(&self) -> &str {
541 &self.entry.name
542 }
543
544 #[must_use]
546 pub fn schema(&self) -> &SchemaRef {
547 &self.entry.schema
548 }
549
550 #[must_use]
552 pub fn max_out_of_orderness(&self) -> Option<Duration> {
553 self.entry.max_out_of_orderness
554 }
555
556 pub fn set_event_time_column(&self, column: &str) {
558 self.entry.source.set_event_time_column(column);
559 }
560}
561
562impl std::fmt::Debug for UntypedSourceHandle {
563 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
564 f.debug_struct("UntypedSourceHandle")
565 .field("name", &self.entry.name)
566 .finish()
567 }
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572pub enum PipelineNodeType {
573 Source,
575 Stream,
577 Sink,
579}
580
581#[derive(Debug, Clone)]
583pub struct PipelineNode {
584 pub name: String,
586 pub node_type: PipelineNodeType,
588 pub schema: Option<SchemaRef>,
590 pub sql: Option<String>,
592}
593
594#[derive(Debug, Clone)]
596pub struct PipelineEdge {
597 pub from: String,
599 pub to: String,
601}
602
603#[derive(Debug, Clone)]
605pub struct PipelineTopology {
606 pub nodes: Vec<PipelineNode>,
608 pub edges: Vec<PipelineEdge>,
610}
611
612#[derive(Debug, Clone)]
614pub struct StreamInfo {
615 pub name: String,
617 pub sql: Option<String>,
619}
620
621#[derive(Debug, Clone)]
623pub struct SourceInfo {
624 pub name: String,
626 pub schema: SchemaRef,
628 pub watermark_column: Option<String>,
630}
631
632#[derive(Debug, Clone)]
634pub struct SinkInfo {
635 pub name: String,
637}
638
639#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
642pub struct MaterializedViewInfo {
643 pub name: String,
645 pub sql: String,
647 pub state: String,
649}
650
651impl From<&laminar_core::mv::MaterializedView> for MaterializedViewInfo {
652 fn from(view: &laminar_core::mv::MaterializedView) -> Self {
653 Self {
654 name: view.name.clone(),
655 sql: view.sql.clone(),
656 state: format!("{:?}", view.state),
657 }
658 }
659}
660
661#[derive(Debug, Clone)]
663pub struct QueryInfo {
664 pub id: u64,
666 pub sql: String,
668 pub active: bool,
670}