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, thiserror::Error, PartialEq, Eq)]
150pub enum SubscriptionError {
151 #[error("subscription fell behind by {skipped} entries")]
153 Lagged {
154 skipped: u64,
156 },
157 #[error("subscription failed: {message}")]
159 Failed {
160 message: String,
162 },
163}
164
165pub struct TypedSubscription<T: FromBatch> {
167 portal: crate::subscription::SubscriptionPortal,
168 _phantom: PhantomData<T>,
169}
170
171impl<T: FromBatch> TypedSubscription<T> {
172 pub(crate) fn new(portal: crate::subscription::SubscriptionPortal) -> Self {
173 Self {
174 portal,
175 _phantom: PhantomData,
176 }
177 }
178
179 #[must_use]
181 pub fn schema(&self) -> SchemaRef {
182 self.portal.schema()
183 }
184
185 pub async fn next_frame(
190 &mut self,
191 ) -> Result<Option<TypedSubscriptionFrame<T>>, SubscriptionError> {
192 self.portal
193 .next_frame()
194 .await
195 .map(convert_typed_frame)
196 .transpose()
197 }
198
199 pub fn try_next_frame(
204 &mut self,
205 ) -> Result<Option<TypedSubscriptionFrame<T>>, SubscriptionError> {
206 self.portal
207 .try_next_frame()
208 .map(convert_typed_frame)
209 .transpose()
210 }
211
212 pub fn poll(&mut self) -> Result<Option<Vec<T>>, SubscriptionError> {
217 loop {
218 match self.try_next_frame()? {
219 Some(TypedSubscriptionFrame::Rows { rows, .. }) => return Ok(Some(rows)),
220 Some(TypedSubscriptionFrame::Barrier { .. }) => {}
221 None => return Ok(None),
222 }
223 }
224 }
225}
226
227fn convert_typed_frame<T: FromBatch>(
228 frame: crate::subscription::PortalFrame,
229) -> Result<TypedSubscriptionFrame<T>, SubscriptionError> {
230 match frame {
231 crate::subscription::PortalFrame::Batch {
232 batch,
233 sequence,
234 lease: _lease,
235 } => Ok(TypedSubscriptionFrame::Rows {
236 rows: T::from_batch_all(&batch),
237 sequence,
238 }),
239 crate::subscription::PortalFrame::Barrier {
240 sequence,
241 epoch,
242 checkpoint_id,
243 through_sequence,
244 } => Ok(TypedSubscriptionFrame::Barrier {
245 sequence,
246 epoch,
247 checkpoint_id,
248 through_sequence,
249 }),
250 crate::subscription::PortalFrame::Lagged(skipped) => {
251 Err(SubscriptionError::Lagged { skipped })
252 }
253 crate::subscription::PortalFrame::Error { message } => {
254 Err(SubscriptionError::Failed { message })
255 }
256 }
257}
258
259impl<T: FromBatch> std::fmt::Debug for TypedSubscription<T> {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 f.debug_struct("TypedSubscription")
262 .field("portal", &self.portal)
263 .finish()
264 }
265}
266
267#[cfg(test)]
268mod typed_subscription_tests {
269 use super::*;
270
271 #[derive(Debug)]
272 struct Row;
273
274 impl FromBatch for Row {
275 fn from_batch(_batch: &RecordBatch, _row: usize) -> Self {
276 Self
277 }
278
279 fn from_batch_all(_batch: &RecordBatch) -> Vec<Self> {
280 Vec::new()
281 }
282 }
283
284 #[test]
285 fn delivery_gap_is_not_converted_to_empty_poll() {
286 let error =
287 convert_typed_frame::<Row>(crate::subscription::PortalFrame::Lagged(7)).unwrap_err();
288 assert_eq!(error, SubscriptionError::Lagged { skipped: 7 });
289 }
290
291 #[test]
292 fn delivery_failure_preserves_detail() {
293 let error = convert_typed_frame::<Row>(crate::subscription::PortalFrame::Error {
294 message: "object dropped".into(),
295 })
296 .unwrap_err();
297 assert_eq!(
298 error,
299 SubscriptionError::Failed {
300 message: "object dropped".into()
301 }
302 );
303 }
304}
305
306pub struct SourceHandle<T: Record> {
308 entry: Arc<SourceEntry>,
309 _phantom: PhantomData<T>,
310}
311
312impl<T: Record> SourceHandle<T> {
313 pub(crate) fn new(entry: Arc<SourceEntry>) -> Result<Self, DbError> {
315 let rust_schema = T::schema();
316 let sql_schema = &entry.schema;
317
318 if rust_schema.fields().len() != sql_schema.fields().len() {
320 return Err(DbError::SchemaMismatch(format!(
321 "Rust type has {} fields but source '{}' has {} columns",
322 rust_schema.fields().len(),
323 entry.name,
324 sql_schema.fields().len()
325 )));
326 }
327
328 Ok(Self {
329 entry,
330 _phantom: PhantomData,
331 })
332 }
333
334 #[allow(clippy::needless_pass_by_value)]
339 pub fn push(&self, record: T) -> Result<(), laminar_core::streaming::StreamingError> {
340 let batch = record.to_record_batch();
341 self.entry.push_and_buffer(batch)
342 }
343
344 pub fn push_batch(&self, records: impl IntoIterator<Item = T>) -> usize {
346 const BATCH_SIZE: usize = 1024;
347 let mut count = 0;
348 let mut buffer = Vec::with_capacity(BATCH_SIZE);
349
350 for record in records {
351 buffer.push(record);
352 if buffer.len() >= BATCH_SIZE {
353 let batch = T::to_record_batch_from_iter(buffer.drain(..));
354 if self.push_arrow(batch).is_err() {
355 return count;
356 }
357 count += BATCH_SIZE;
358 }
359 }
360
361 if !buffer.is_empty() {
362 let len = buffer.len();
363 let batch = T::to_record_batch_from_iter(buffer);
364 if self.push_arrow(batch).is_ok() {
365 count += len;
366 }
367 }
368 count
369 }
370
371 pub fn push_arrow(
376 &self,
377 batch: RecordBatch,
378 ) -> Result<(), laminar_core::streaming::StreamingError> {
379 self.entry.push_and_buffer(batch)
380 }
381
382 pub fn watermark(&self, timestamp: i64) {
384 self.entry.source.watermark(timestamp);
385 }
386
387 #[must_use]
389 pub fn current_watermark(&self) -> i64 {
390 self.entry.source.current_watermark()
391 }
392
393 #[must_use]
395 pub fn pending(&self) -> usize {
396 self.entry.source.pending()
397 }
398
399 #[must_use]
401 pub fn capacity(&self) -> usize {
402 self.entry.source.capacity()
403 }
404
405 #[must_use]
407 pub fn is_backpressured(&self) -> bool {
408 crate::metrics::is_backpressured(self.pending(), self.capacity())
409 }
410
411 #[must_use]
413 pub fn name(&self) -> &str {
414 &self.entry.name
415 }
416
417 #[must_use]
419 pub fn schema(&self) -> &SchemaRef {
420 &self.entry.schema
421 }
422
423 #[must_use]
425 pub fn max_out_of_orderness(&self) -> Option<Duration> {
426 self.entry.max_out_of_orderness
427 }
428
429 pub fn set_event_time_column(&self, column: &str) {
431 self.entry.source.set_event_time_column(column);
432 }
433}
434
435impl<T: Record> std::fmt::Debug for SourceHandle<T> {
436 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437 f.debug_struct("SourceHandle")
438 .field("name", &self.entry.name)
439 .field("pending", &self.pending())
440 .finish()
441 }
442}
443
444pub struct UntypedSourceHandle {
446 entry: Arc<SourceEntry>,
447}
448
449impl UntypedSourceHandle {
450 pub(crate) fn new(entry: Arc<SourceEntry>) -> Self {
451 Self { entry }
452 }
453
454 pub fn push_arrow(
459 &self,
460 batch: RecordBatch,
461 ) -> Result<(), laminar_core::streaming::StreamingError> {
462 self.entry.push_and_buffer(batch)
463 }
464
465 pub fn watermark(&self, timestamp: i64) {
467 self.entry.source.watermark(timestamp);
468 }
469
470 #[must_use]
472 pub fn current_watermark(&self) -> i64 {
473 self.entry.source.current_watermark()
474 }
475
476 #[must_use]
478 pub fn pending(&self) -> usize {
479 self.entry.source.pending()
480 }
481
482 #[must_use]
484 pub fn capacity(&self) -> usize {
485 self.entry.source.capacity()
486 }
487
488 #[must_use]
490 pub fn is_backpressured(&self) -> bool {
491 crate::metrics::is_backpressured(self.pending(), self.capacity())
492 }
493
494 #[must_use]
496 pub fn name(&self) -> &str {
497 &self.entry.name
498 }
499
500 #[must_use]
502 pub fn schema(&self) -> &SchemaRef {
503 &self.entry.schema
504 }
505
506 #[must_use]
508 pub fn max_out_of_orderness(&self) -> Option<Duration> {
509 self.entry.max_out_of_orderness
510 }
511
512 pub fn set_event_time_column(&self, column: &str) {
514 self.entry.source.set_event_time_column(column);
515 }
516}
517
518impl std::fmt::Debug for UntypedSourceHandle {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 f.debug_struct("UntypedSourceHandle")
521 .field("name", &self.entry.name)
522 .finish()
523 }
524}
525
526#[derive(Debug, Clone, Copy, PartialEq, Eq)]
528pub enum PipelineNodeType {
529 Source,
531 Stream,
533 Sink,
535}
536
537#[derive(Debug, Clone)]
539pub struct PipelineNode {
540 pub name: String,
542 pub node_type: PipelineNodeType,
544 pub schema: Option<SchemaRef>,
546 pub sql: Option<String>,
548}
549
550#[derive(Debug, Clone)]
552pub struct PipelineEdge {
553 pub from: String,
555 pub to: String,
557}
558
559#[derive(Debug, Clone)]
561pub struct PipelineTopology {
562 pub nodes: Vec<PipelineNode>,
564 pub edges: Vec<PipelineEdge>,
566}
567
568#[derive(Debug, Clone)]
570pub struct StreamInfo {
571 pub name: String,
573 pub sql: Option<String>,
575}
576
577#[derive(Debug, Clone)]
579pub struct SourceInfo {
580 pub name: String,
582 pub schema: SchemaRef,
584 pub watermark_column: Option<String>,
586}
587
588#[derive(Debug, Clone)]
590pub struct SinkInfo {
591 pub name: String,
593}
594
595#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
598pub struct MaterializedViewInfo {
599 pub name: String,
601 pub sql: String,
603 pub state: String,
605}
606
607impl From<&laminar_core::mv::MaterializedView> for MaterializedViewInfo {
608 fn from(view: &laminar_core::mv::MaterializedView) -> Self {
609 Self {
610 name: view.name.clone(),
611 sql: view.sql.clone(),
612 state: format!("{:?}", view.state),
613 }
614 }
615}
616
617#[derive(Debug, Clone)]
619pub struct QueryInfo {
620 pub id: u64,
622 pub sql: String,
624 pub active: bool,
626}