1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::Arc;
4use std::time::Duration;
5
6use arrow::array::{
7 Array, RecordBatch, TimestampMicrosecondArray, TimestampMillisecondArray,
8 TimestampNanosecondArray, TimestampSecondArray,
9};
10use arrow::datatypes::{DataType, Schema, TimeUnit};
11use datafusion::common::{DataFusionError, Result};
12use datum::{NotUsed, Source, StreamCompletion, StreamError, StreamResult, UniqueKillSwitch};
13
14use crate::{ChangelogBatch, stream_error};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18pub struct Watermark {
19 timestamp_ns: i64,
20}
21
22impl Watermark {
23 #[must_use]
25 pub const fn new(timestamp_ns: i64) -> Self {
26 Self { timestamp_ns }
27 }
28
29 #[must_use]
31 pub const fn timestamp_ns(self) -> i64 {
32 self.timestamp_ns
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct SqlBarrier {
39 epoch: u64,
40}
41
42impl SqlBarrier {
43 #[must_use]
45 pub const fn new(epoch: u64) -> Self {
46 Self { epoch }
47 }
48
49 #[must_use]
51 pub const fn epoch(self) -> u64 {
52 self.epoch
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum SqlEvent<T> {
59 Data(T),
61 Watermark(Watermark),
63 Barrier(SqlBarrier),
65}
66
67impl<T> SqlEvent<T> {
68 pub fn map_data_result<U, F>(self, f: F) -> StreamResult<SqlEvent<U>>
70 where
71 F: FnOnce(T) -> StreamResult<U>,
72 {
73 match self {
74 Self::Data(data) => f(data).map(SqlEvent::Data),
75 Self::Watermark(watermark) => Ok(SqlEvent::Watermark(watermark)),
76 Self::Barrier(barrier) => Ok(SqlEvent::Barrier(barrier)),
77 }
78 }
79
80 #[must_use]
82 pub const fn as_watermark(&self) -> Option<Watermark> {
83 match self {
84 Self::Watermark(watermark) => Some(*watermark),
85 Self::Data(_) | Self::Barrier(_) => None,
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum WatermarkStrategy {
93 BoundedOutOfOrderness {
95 max_out_of_orderness: Duration,
97 },
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct EventTimeConfig {
103 column: Arc<str>,
104 strategy: WatermarkStrategy,
105}
106
107impl EventTimeConfig {
108 #[must_use]
110 pub fn bounded_out_of_orderness(
111 column: impl Into<Arc<str>>,
112 max_out_of_orderness: Duration,
113 ) -> Self {
114 Self {
115 column: column.into(),
116 strategy: WatermarkStrategy::BoundedOutOfOrderness {
117 max_out_of_orderness,
118 },
119 }
120 }
121
122 #[must_use]
124 pub fn column(&self) -> &str {
125 &self.column
126 }
127
128 #[must_use]
130 pub const fn strategy(&self) -> &WatermarkStrategy {
131 &self.strategy
132 }
133
134 pub(crate) fn resolve(&self, schema: &Schema) -> Result<ResolvedEventTimeConfig> {
135 let column_index = schema.index_of(&self.column).map_err(|_| {
136 DataFusionError::Plan(format!(
137 "event-time column '{}' does not exist in table schema",
138 self.column
139 ))
140 })?;
141 let field = schema.field(column_index);
142 if !matches!(field.data_type(), DataType::Timestamp(_, _)) {
143 return Err(DataFusionError::Plan(format!(
144 "event-time column '{}' must be an Arrow Timestamp, found {:?}",
145 self.column,
146 field.data_type()
147 )));
148 }
149 if field.is_nullable() {
150 return Err(DataFusionError::Plan(format!(
151 "event-time column '{}' must be non-nullable",
152 self.column
153 )));
154 }
155
156 let max_out_of_orderness_ns = match &self.strategy {
157 WatermarkStrategy::BoundedOutOfOrderness {
158 max_out_of_orderness,
159 } => duration_to_ns(*max_out_of_orderness)?,
160 };
161
162 Ok(ResolvedEventTimeConfig {
163 column_index,
164 max_out_of_orderness_ns,
165 })
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub(crate) struct ResolvedEventTimeConfig {
171 column_index: usize,
172 max_out_of_orderness_ns: i64,
173}
174
175pub trait SqlEventPayload {
177 fn event_time_batch(&self) -> &RecordBatch;
179
180 fn event_time_partition(&self, _row: usize) -> Option<i64> {
186 None
187 }
188
189 fn event_time_active_partitions(&self) -> Option<Vec<i64>> {
194 None
195 }
196}
197
198impl SqlEventPayload for RecordBatch {
199 fn event_time_batch(&self) -> &RecordBatch {
200 self
201 }
202}
203
204impl SqlEventPayload for ChangelogBatch {
205 fn event_time_batch(&self) -> &RecordBatch {
206 self.batch()
207 }
208}
209
210#[must_use]
211pub fn data_events<T, Mat>(source: Source<T, Mat>) -> Source<SqlEvent<T>, Mat>
213where
214 T: Send + 'static,
215 Mat: Send + 'static,
216{
217 source.map(SqlEvent::Data)
218}
219
220pub fn assign_event_time_watermarks<T, Mat>(
225 source: Source<T, Mat>,
226 schema: &Schema,
227 config: EventTimeConfig,
228) -> Result<Source<SqlEvent<T>, Mat>>
229where
230 T: SqlEventPayload + Send + 'static,
231 Mat: Send + 'static,
232{
233 let resolved = config.resolve(schema)?;
234 Ok(assign_resolved_event_time_watermarks(source, resolved))
235}
236
237pub(crate) fn assign_resolved_event_time_watermarks<T, Mat>(
238 source: Source<T, Mat>,
239 config: ResolvedEventTimeConfig,
240) -> Source<SqlEvent<T>, Mat>
241where
242 T: SqlEventPayload + Send + 'static,
243 Mat: Send + 'static,
244{
245 source.try_stateful_map_concat(WatermarkGenerator::new(config), |generator, payload| {
246 generator.apply(payload)
247 })
248}
249
250pub fn map_sql_event_data<T, U, Mat, F>(
252 source: Source<SqlEvent<T>, Mat>,
253 f: F,
254) -> Source<SqlEvent<U>, Mat>
255where
256 T: Send + 'static,
257 U: Send + 'static,
258 Mat: Send + 'static,
259 F: Fn(T) -> StreamResult<U> + Send + Sync + 'static,
260{
261 source.try_map(move |event| event.map_data_result(&f))
262}
263
264#[derive(Clone)]
265struct WatermarkGenerator {
266 config: ResolvedEventTimeConfig,
267 max_seen_event_time_ns_by_partition: BTreeMap<i64, Option<i64>>,
268 last_emitted_watermark_ns: Option<i64>,
269}
270
271impl WatermarkGenerator {
272 fn new(config: ResolvedEventTimeConfig) -> Self {
273 Self {
274 config,
275 max_seen_event_time_ns_by_partition: BTreeMap::new(),
276 last_emitted_watermark_ns: None,
277 }
278 }
279
280 fn apply<T>(&mut self, payload: T) -> StreamResult<Vec<SqlEvent<T>>>
281 where
282 T: SqlEventPayload,
283 {
284 if let Some(active_partitions) = payload.event_time_active_partitions() {
285 for partition in active_partitions {
286 self.max_seen_event_time_ns_by_partition
287 .entry(partition)
288 .or_insert(None);
289 }
290 }
291 let batch_max_by_partition =
292 max_event_time_ns_by_partition(&payload, self.config.column_index)?;
293 let mut out = vec![SqlEvent::Data(payload)];
294 for (partition, batch_max) in batch_max_by_partition {
295 let entry = self
296 .max_seen_event_time_ns_by_partition
297 .entry(partition)
298 .or_insert(None);
299 *entry = Some(entry.map_or(batch_max, |previous| previous.max(batch_max)));
300 }
301
302 if let Some(min_seen) = self.min_seen_event_time_ns() {
303 let watermark_ns = min_seen
304 .checked_sub(self.config.max_out_of_orderness_ns)
305 .ok_or_else(|| stream_error("event-time watermark underflowed i64 nanoseconds"))?;
306 if self
307 .last_emitted_watermark_ns
308 .is_none_or(|previous| watermark_ns > previous)
309 {
310 self.last_emitted_watermark_ns = Some(watermark_ns);
311 out.push(SqlEvent::Watermark(Watermark::new(watermark_ns)));
312 }
313 }
314
315 Ok(out)
316 }
317
318 fn min_seen_event_time_ns(&self) -> Option<i64> {
319 let mut min_seen = None;
320 for value in self.max_seen_event_time_ns_by_partition.values() {
321 let value = (*value)?;
322 min_seen = Some(min_seen.map_or(value, |current: i64| current.min(value)));
323 }
324 min_seen
325 }
326}
327
328const DEFAULT_EVENT_TIME_PARTITION: i64 = 0;
329
330fn max_event_time_ns_by_partition<T>(
331 payload: &T,
332 column_index: usize,
333) -> StreamResult<BTreeMap<i64, i64>>
334where
335 T: SqlEventPayload,
336{
337 let batch = payload.event_time_batch();
338 if batch.num_rows() == 0 {
339 return Ok(BTreeMap::new());
340 }
341 if column_index >= batch.num_columns() {
342 return Err(stream_error(format!(
343 "event-time column index {column_index} is out of range for {} columns",
344 batch.num_columns()
345 )));
346 }
347
348 let column = batch.column(column_index);
349 match batch.schema().field(column_index).data_type() {
350 DataType::Timestamp(TimeUnit::Second, _) => {
351 timestamp_max_ns_by_partition::<TimestampSecondArray, T>(
352 column.as_any().downcast_ref::<TimestampSecondArray>(),
353 TimeUnit::Second,
354 payload,
355 )
356 }
357 DataType::Timestamp(TimeUnit::Millisecond, _) => {
358 timestamp_max_ns_by_partition::<TimestampMillisecondArray, T>(
359 column.as_any().downcast_ref::<TimestampMillisecondArray>(),
360 TimeUnit::Millisecond,
361 payload,
362 )
363 }
364 DataType::Timestamp(TimeUnit::Microsecond, _) => {
365 timestamp_max_ns_by_partition::<TimestampMicrosecondArray, T>(
366 column.as_any().downcast_ref::<TimestampMicrosecondArray>(),
367 TimeUnit::Microsecond,
368 payload,
369 )
370 }
371 DataType::Timestamp(TimeUnit::Nanosecond, _) => {
372 timestamp_max_ns_by_partition::<TimestampNanosecondArray, T>(
373 column.as_any().downcast_ref::<TimestampNanosecondArray>(),
374 TimeUnit::Nanosecond,
375 payload,
376 )
377 }
378 other => Err(stream_error(format!(
379 "event-time column must be an Arrow Timestamp, found {other:?}"
380 ))),
381 }
382}
383
384fn timestamp_max_ns_by_partition<A, T>(
385 array: Option<&A>,
386 unit: TimeUnit,
387 payload: &T,
388) -> StreamResult<BTreeMap<i64, i64>>
389where
390 A: Array + TimestampValues,
391 T: SqlEventPayload,
392{
393 let array = array.ok_or_else(|| stream_error("event-time column array type mismatch"))?;
394 let multiplier = timestamp_unit_multiplier(unit);
395 let mut max_by_partition = BTreeMap::new();
396 for row in 0..array.len() {
397 if array.is_null(row) {
398 return Err(stream_error(format!(
399 "event-time column contains null at row {row}"
400 )));
401 }
402 let value = array.value_at(row);
403 let ns = value
404 .checked_mul(multiplier)
405 .ok_or_else(|| stream_error("event-time value overflowed i64 nanoseconds"))?;
406 let partition = payload
407 .event_time_partition(row)
408 .unwrap_or(DEFAULT_EVENT_TIME_PARTITION);
409 max_by_partition
410 .entry(partition)
411 .and_modify(|previous: &mut i64| *previous = (*previous).max(ns))
412 .or_insert(ns);
413 }
414 Ok(max_by_partition)
415}
416
417trait TimestampValues {
418 fn value_at(&self, row: usize) -> i64;
419}
420
421impl TimestampValues for TimestampSecondArray {
422 fn value_at(&self, row: usize) -> i64 {
423 self.value(row)
424 }
425}
426
427impl TimestampValues for TimestampMillisecondArray {
428 fn value_at(&self, row: usize) -> i64 {
429 self.value(row)
430 }
431}
432
433impl TimestampValues for TimestampMicrosecondArray {
434 fn value_at(&self, row: usize) -> i64 {
435 self.value(row)
436 }
437}
438
439impl TimestampValues for TimestampNanosecondArray {
440 fn value_at(&self, row: usize) -> i64 {
441 self.value(row)
442 }
443}
444
445const fn timestamp_unit_multiplier(unit: TimeUnit) -> i64 {
446 match unit {
447 TimeUnit::Second => 1_000_000_000,
448 TimeUnit::Millisecond => 1_000_000,
449 TimeUnit::Microsecond => 1_000,
450 TimeUnit::Nanosecond => 1,
451 }
452}
453
454fn duration_to_ns(duration: Duration) -> Result<i64> {
455 i64::try_from(duration.as_nanos()).map_err(|_| {
456 DataFusionError::Plan(format!(
457 "event-time watermark delay {duration:?} exceeds i64 nanoseconds"
458 ))
459 })
460}
461
462pub struct ContinuousQueryHandle {
464 kill_switch: UniqueKillSwitch,
465 completion: StreamCompletion<NotUsed>,
466}
467
468impl ContinuousQueryHandle {
469 pub(crate) fn new(
470 kill_switch: UniqueKillSwitch,
471 completion: StreamCompletion<NotUsed>,
472 ) -> Self {
473 Self {
474 kill_switch,
475 completion,
476 }
477 }
478
479 pub fn cancel(&self) {
481 self.kill_switch.shutdown();
482 }
483
484 pub fn abort(&self, error: StreamError) {
486 self.kill_switch.abort(error);
487 }
488
489 pub fn wait(self) -> StreamResult<NotUsed> {
491 self.completion.wait()
492 }
493
494 #[must_use]
496 pub fn try_wait(&mut self) -> Option<StreamResult<NotUsed>> {
497 self.completion.try_wait()
498 }
499}
500
501impl fmt::Debug for ContinuousQueryHandle {
502 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503 f.debug_struct("ContinuousQueryHandle")
504 .finish_non_exhaustive()
505 }
506}