Skip to main content

laminar_sql/datafusion/
mod.rs

1//! DataFusion integration for SQL processing.
2
3/// Marker UDFs for the `ai_*` SQL functions (rewritten by the AI operator).
4pub mod ai_udf;
5mod bridge;
6mod channel_source;
7/// Lambda higher-order functions for arrays and maps (F-SCHEMA-015 Tier 3)
8pub mod complex_type_lambda;
9/// Array, Struct, and Map scalar UDFs (F-SCHEMA-015)
10pub mod complex_type_udf;
11mod exec;
12/// End-to-end streaming SQL execution
13pub mod execute;
14/// Format bridge UDFs for inline format conversion
15pub mod format_bridge_udf;
16/// LaminarDB streaming JSON extension UDFs (F-SCHEMA-013)
17pub mod json_extensions;
18/// SQL/JSON path query compiler and scalar UDFs
19pub mod json_path;
20/// JSON table-valued functions (array/object expansion)
21pub mod json_tvf;
22/// JSONB binary format types for JSON UDF evaluation
23pub mod json_types;
24/// PostgreSQL-compatible JSON aggregate UDAFs
25pub mod json_udaf;
26/// PostgreSQL-compatible JSON scalar UDFs
27pub mod json_udf;
28/// Live source provider for streaming execution with plan caching
29pub mod live_source;
30/// Lookup join plan node for DataFusion.
31pub mod lookup_join;
32/// Physical execution plan and extension planner for lookup joins.
33pub mod lookup_join_exec;
34/// Processing-time UDF for `PROCTIME()` support
35pub mod proctime_udf;
36mod source;
37mod table_provider;
38/// Dynamic watermark filter for scan-level late-data pruning
39/// Watermark UDF for current watermark access
40pub mod watermark_udf;
41/// Window function UDFs (TUMBLE, HOP, SESSION, CUMULATE)
42pub mod window_udf;
43
44pub use ai_udf::{ai_function_markers, AiFunctionMarker};
45pub use bridge::{BridgeSendError, BridgeSender, BridgeStream, BridgeTrySendError, StreamBridge};
46pub use channel_source::ChannelStreamSource;
47pub use complex_type_lambda::{
48    register_lambda_functions, ArrayFilter, ArrayReduce, ArrayTransform, MapFilter,
49    MapTransformValues,
50};
51pub use complex_type_udf::{
52    register_complex_type_functions, MapContainsKey, MapFromArrays, MapKeys, MapValues, StructDrop,
53    StructExtract, StructMerge, StructRename, StructSet,
54};
55pub use exec::StreamingScanExec;
56pub use execute::{execute_streaming_sql, DdlResult, QueryResult, StreamingSqlResult};
57pub use format_bridge_udf::{FromJsonUdf, ParseEpochUdf, ParseTimestampUdf, ToJsonUdf};
58pub use json_extensions::{
59    register_json_extensions, JsonInferSchema, JsonToColumns, JsonbDeepMerge, JsonbExcept,
60    JsonbFlatten, JsonbMerge, JsonbPick, JsonbRenameKeys, JsonbStripNulls, JsonbUnflatten,
61};
62pub use json_path::{CompiledJsonPath, JsonPathStep, JsonbPathExistsUdf, JsonbPathMatchUdf};
63pub use json_tvf::{
64    register_json_table_functions, JsonbArrayElementsTextTvf, JsonbArrayElementsTvf,
65    JsonbEachTextTvf, JsonbEachTvf, JsonbObjectKeysTvf,
66};
67pub use json_udaf::{JsonAgg, JsonObjectAgg};
68pub use json_udf::{
69    JsonBuildArray, JsonBuildObject, JsonTypeof, JsonbContainedBy, JsonbContains, JsonbExists,
70    JsonbExistsAll, JsonbExistsAny, JsonbGet, JsonbGetIdx, JsonbGetPath, JsonbGetPathText,
71    JsonbGetText, JsonbGetTextIdx, ToJsonb,
72};
73pub use live_source::{LiveSourceHandle, LiveSourceProvider};
74pub use lookup_join_exec::{
75    LookupJoinExec, LookupJoinExtensionPlanner, LookupSnapshot, LookupTableRegistry,
76    PartialLookupJoinExec, PartialLookupState, RegisteredLookup, VersionedLookupJoinExec,
77    VersionedLookupState,
78};
79pub use proctime_udf::ProcTimeUdf;
80pub use source::{SortColumn, StreamSource, StreamSourceRef};
81pub use table_provider::StreamingTableProvider;
82pub use watermark_udf::WatermarkUdf;
83pub use window_udf::{
84    CumulateWindowEnd, CumulateWindowStart, HopWindowEnd, HopWindowStart, SessionWindowStart,
85    TumbleWindowEnd, TumbleWindowStart,
86};
87
88use std::sync::atomic::AtomicI64;
89use std::sync::Arc;
90
91use datafusion::execution::SessionStateBuilder;
92use datafusion::prelude::*;
93use datafusion_expr::ScalarUDF;
94
95use crate::planner::streaming_optimizer::{StreamingPhysicalValidator, StreamingValidatorMode};
96
97/// Returns a base `SessionConfig` with identifier normalization disabled.
98///
99/// DataFusion's default behaviour lowercases all unquoted SQL identifiers
100/// (per the SQL standard). LaminarDB disables this so that mixed-case
101/// column names from external sources (Kafka, CDC, WebSocket) can be
102/// referenced without double-quoting.
103#[must_use]
104pub fn base_session_config() -> SessionConfig {
105    let mut config = SessionConfig::new();
106    config.options_mut().sql_parser.enable_ident_normalization = false;
107    // Single partition for streaming micro-batch execution. Multi-partition
108    // plans contain stateful operators (RepartitionExec) that cannot be
109    // reused across cycles, causing panics on cached physical plans.
110    config = config.with_target_partitions(1);
111    config
112}
113
114/// Creates a `DataFusion` session context with identifier normalization
115/// disabled.
116///
117/// Suitable for ad-hoc / non-streaming queries (filters, lookups).
118/// For streaming workloads prefer [`create_streaming_context`].
119#[must_use]
120pub fn create_session_context() -> SessionContext {
121    SessionContext::new_with_config(base_session_config())
122}
123
124/// Creates a `DataFusion` session context configured for streaming queries.
125///
126/// The context is configured with:
127/// - Batch size of 8192 (balanced for streaming throughput)
128/// - Single partition (streaming sources are typically not partitioned)
129/// - Identifier normalization disabled (mixed-case columns work unquoted)
130/// - All streaming UDFs registered (TUMBLE, HOP, SESSION, WATERMARK)
131/// - `StreamingPhysicalValidator` in `Reject` mode (blocks unsafe plans)
132///
133/// The watermark UDF is initialized with no watermark set (returns NULL).
134/// Use [`register_streaming_functions_with_watermark`] to provide a live
135/// watermark source.
136///
137/// # Example
138///
139/// ```rust,ignore
140/// let ctx = create_streaming_context();
141/// ctx.register_table("events", provider)?;
142/// let df = ctx.sql("SELECT * FROM events").await?;
143/// ```
144#[must_use]
145pub fn create_streaming_context() -> SessionContext {
146    create_streaming_context_with_validator(StreamingValidatorMode::Reject)
147}
148
149/// Creates a streaming context with a configurable validator mode.
150///
151/// Same as [`create_streaming_context`] but allows choosing how the
152/// [`StreamingPhysicalValidator`] handles plan violations.
153///
154/// Use [`StreamingValidatorMode::Off`] to get the previous behaviour
155/// (no plan-time validation).
156#[must_use]
157pub fn create_streaming_context_with_validator(mode: StreamingValidatorMode) -> SessionContext {
158    let config = base_session_config().with_batch_size(8192);
159
160    let ctx = if matches!(mode, StreamingValidatorMode::Off) {
161        SessionContext::new_with_config(config)
162    } else {
163        // Build a default state to get the standard optimizer rules, then
164        // prepend our streaming validator so it fires before DataFusion's
165        // built-in SanityCheckPlan (which produces generic error messages).
166        let default_state = SessionStateBuilder::new()
167            .with_config(config.clone())
168            .with_default_features()
169            .build();
170        let mut rules: Vec<
171            Arc<dyn datafusion::physical_optimizer::PhysicalOptimizerRule + Send + Sync>,
172        > = vec![Arc::new(StreamingPhysicalValidator::new(mode))];
173        rules.extend(default_state.physical_optimizers().iter().cloned());
174
175        let state = SessionStateBuilder::new()
176            .with_config(config)
177            .with_default_features()
178            .with_physical_optimizer_rules(rules)
179            .build();
180        SessionContext::new_with_state(state)
181    };
182
183    register_streaming_functions(&ctx);
184    ctx
185}
186
187/// Window-time, JSON, complex-type, lambda, and `proctime()` UDFs —
188/// every streaming UDF except `watermark()`. Pulled out of the public
189/// `register_streaming_functions*` entry points so they share a single
190/// list and stay in sync.
191fn register_non_watermark_udfs(ctx: &SessionContext) {
192    ctx.register_udf(ScalarUDF::new_from_impl(TumbleWindowStart::new()));
193    ctx.register_udf(ScalarUDF::new_from_impl(TumbleWindowEnd::new()));
194    ctx.register_udf(ScalarUDF::new_from_impl(HopWindowStart::new()));
195    ctx.register_udf(ScalarUDF::new_from_impl(HopWindowEnd::new()));
196    ctx.register_udf(ScalarUDF::new_from_impl(SessionWindowStart::new()));
197    ctx.register_udf(ScalarUDF::new_from_impl(CumulateWindowStart::new()));
198    ctx.register_udf(ScalarUDF::new_from_impl(CumulateWindowEnd::new()));
199    ctx.register_udf(ScalarUDF::new_from_impl(ProcTimeUdf::new()));
200    for marker in ai_function_markers() {
201        ctx.register_udf(marker);
202    }
203    register_json_functions(ctx);
204    register_json_extensions(ctx);
205    register_complex_type_functions(ctx);
206    register_lambda_functions(ctx);
207}
208
209/// Registers `LaminarDB` streaming UDFs with a session context. The
210/// `watermark()` UDF is registered in unset mode (always returns NULL);
211/// use [`register_streaming_functions_with_watermark`] to provide a
212/// live watermark source from Ring 0.
213pub fn register_streaming_functions(ctx: &SessionContext) {
214    register_non_watermark_udfs(ctx);
215    ctx.register_udf(ScalarUDF::new_from_impl(WatermarkUdf::unset()));
216}
217
218/// Registers streaming UDFs with a live watermark source — same as
219/// [`register_streaming_functions`] but `watermark()` reads
220/// `watermark_ms` (in milliseconds since epoch; values < 0 mean "no
221/// watermark", returning NULL).
222pub fn register_streaming_functions_with_watermark(
223    ctx: &SessionContext,
224    watermark_ms: Arc<AtomicI64>,
225) {
226    register_non_watermark_udfs(ctx);
227    ctx.register_udf(ScalarUDF::new_from_impl(WatermarkUdf::new(watermark_ms)));
228}
229
230/// Registers all PostgreSQL-compatible JSON UDFs and UDAFs
231/// with the given `SessionContext`.
232pub fn register_json_functions(ctx: &SessionContext) {
233    // Extraction operators
234    ctx.register_udf(ScalarUDF::new_from_impl(JsonbGet::new()));
235    ctx.register_udf(ScalarUDF::new_from_impl(JsonbGetIdx::new()));
236    ctx.register_udf(ScalarUDF::new_from_impl(JsonbGetText::new()));
237    ctx.register_udf(ScalarUDF::new_from_impl(JsonbGetTextIdx::new()));
238    ctx.register_udf(ScalarUDF::new_from_impl(JsonbGetPath::new()));
239    ctx.register_udf(ScalarUDF::new_from_impl(JsonbGetPathText::new()));
240
241    // Existence operators
242    ctx.register_udf(ScalarUDF::new_from_impl(JsonbExists::new()));
243    ctx.register_udf(ScalarUDF::new_from_impl(JsonbExistsAny::new()));
244    ctx.register_udf(ScalarUDF::new_from_impl(JsonbExistsAll::new()));
245
246    // Containment operators
247    ctx.register_udf(ScalarUDF::new_from_impl(JsonbContains::new()));
248    ctx.register_udf(ScalarUDF::new_from_impl(JsonbContainedBy::new()));
249
250    // Interrogation / construction
251    ctx.register_udf(ScalarUDF::new_from_impl(JsonTypeof::new()));
252    ctx.register_udf(ScalarUDF::new_from_impl(JsonBuildObject::new()));
253    ctx.register_udf(ScalarUDF::new_from_impl(JsonBuildArray::new()));
254    ctx.register_udf(ScalarUDF::new_from_impl(ToJsonb::new()));
255
256    // Aggregates
257    ctx.register_udaf(datafusion_expr::AggregateUDF::new_from_impl(JsonAgg::new()));
258    ctx.register_udaf(datafusion_expr::AggregateUDF::new_from_impl(
259        JsonObjectAgg::new(),
260    ));
261
262    // Format bridge functions
263    ctx.register_udf(ScalarUDF::new_from_impl(ParseEpochUdf::new()));
264    ctx.register_udf(ScalarUDF::new_from_impl(ParseTimestampUdf::new()));
265    ctx.register_udf(ScalarUDF::new_from_impl(ToJsonUdf::new()));
266    ctx.register_udf(ScalarUDF::new_from_impl(FromJsonUdf::new()));
267
268    // JSON path query functions (scalar)
269    ctx.register_udf(ScalarUDF::new_from_impl(JsonbPathExistsUdf::new()));
270    ctx.register_udf(ScalarUDF::new_from_impl(JsonbPathMatchUdf::new()));
271
272    // JSON table-valued functions
273    register_json_table_functions(ctx);
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use arrow_array::{Float64Array, Int64Array, RecordBatch};
280    use arrow_schema::{DataType, Field, Schema};
281    use datafusion::execution::FunctionRegistry;
282    use futures::StreamExt;
283    use std::sync::Arc;
284
285    fn test_schema() -> Arc<Schema> {
286        Arc::new(Schema::new(vec![
287            Field::new("id", DataType::Int64, false),
288            Field::new("value", DataType::Float64, true),
289        ]))
290    }
291
292    /// Take the sender from a `ChannelStreamSource`, panicking if already taken.
293    fn take_test_sender(source: &ChannelStreamSource) -> super::bridge::BridgeSender {
294        source.take_sender().expect("sender already taken")
295    }
296
297    fn test_batch(schema: &Arc<Schema>, ids: Vec<i64>, values: Vec<f64>) -> RecordBatch {
298        RecordBatch::try_new(
299            Arc::clone(schema),
300            vec![
301                Arc::new(Int64Array::from(ids)),
302                Arc::new(Float64Array::from(values)),
303            ],
304        )
305        .unwrap()
306    }
307
308    #[test]
309    fn test_create_streaming_context() {
310        let ctx = create_streaming_context();
311        let state = ctx.state();
312        let config = state.config();
313
314        assert_eq!(config.batch_size(), 8192);
315        assert_eq!(config.target_partitions(), 1);
316    }
317
318    #[tokio::test]
319    async fn test_full_query_pipeline() {
320        let ctx = create_streaming_context();
321        let schema = test_schema();
322
323        // Create source and take the sender (important for channel closure)
324        let source = Arc::new(ChannelStreamSource::new(Arc::clone(&schema)));
325        let sender = take_test_sender(&source);
326        let provider = StreamingTableProvider::new("events", source);
327        ctx.register_table("events", Arc::new(provider)).unwrap();
328
329        // Send test data
330        sender
331            .send(test_batch(&schema, vec![1, 2, 3], vec![10.0, 20.0, 30.0]))
332            .await
333            .unwrap();
334        sender
335            .send(test_batch(&schema, vec![4, 5], vec![40.0, 50.0]))
336            .await
337            .unwrap();
338        drop(sender); // Close the channel
339
340        // Execute query
341        let df = ctx.sql("SELECT * FROM events").await.unwrap();
342        let batches = df.collect().await.unwrap();
343
344        // Verify results
345        let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
346        assert_eq!(total_rows, 5);
347    }
348
349    #[tokio::test]
350    async fn test_query_with_projection() {
351        let ctx = create_streaming_context();
352        let schema = test_schema();
353
354        let source = Arc::new(ChannelStreamSource::new(Arc::clone(&schema)));
355        let sender = take_test_sender(&source);
356        let provider = StreamingTableProvider::new("events", source);
357        ctx.register_table("events", Arc::new(provider)).unwrap();
358
359        sender
360            .send(test_batch(&schema, vec![1, 2], vec![100.0, 200.0]))
361            .await
362            .unwrap();
363        drop(sender);
364
365        // Query only the id column
366        let df = ctx.sql("SELECT id FROM events").await.unwrap();
367        let batches = df.collect().await.unwrap();
368
369        assert_eq!(batches.len(), 1);
370        assert_eq!(batches[0].num_columns(), 1);
371        assert_eq!(batches[0].schema().field(0).name(), "id");
372    }
373
374    #[tokio::test]
375    async fn test_query_with_filter() {
376        let ctx = create_streaming_context();
377        let schema = test_schema();
378
379        let source = Arc::new(ChannelStreamSource::new(Arc::clone(&schema)));
380        let sender = take_test_sender(&source);
381        let provider = StreamingTableProvider::new("events", source);
382        ctx.register_table("events", Arc::new(provider)).unwrap();
383
384        sender
385            .send(test_batch(
386                &schema,
387                vec![1, 2, 3, 4, 5],
388                vec![10.0, 20.0, 30.0, 40.0, 50.0],
389            ))
390            .await
391            .unwrap();
392        drop(sender);
393
394        // Filter for value > 25
395        let df = ctx
396            .sql("SELECT * FROM events WHERE value > 25")
397            .await
398            .unwrap();
399        let batches = df.collect().await.unwrap();
400
401        let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
402        assert_eq!(total_rows, 3); // 30, 40, 50
403    }
404
405    #[tokio::test]
406    async fn test_unbounded_aggregation_rejected() {
407        // Aggregations on unbounded streams should be rejected by `DataFusion`.
408        // Streaming aggregations require windows, which are implemented.
409        let ctx = create_streaming_context();
410        let schema = test_schema();
411
412        let source = Arc::new(ChannelStreamSource::new(Arc::clone(&schema)));
413        let sender = take_test_sender(&source);
414        let provider = StreamingTableProvider::new("events", source);
415        ctx.register_table("events", Arc::new(provider)).unwrap();
416
417        sender
418            .send(test_batch(&schema, vec![1, 2, 3], vec![10.0, 20.0, 30.0]))
419            .await
420            .unwrap();
421        drop(sender);
422
423        // Aggregate query on unbounded stream should fail at execution
424        let df = ctx.sql("SELECT COUNT(*) as cnt FROM events").await.unwrap();
425
426        // Execution should fail because we can't aggregate an infinite stream
427        let result = df.collect().await;
428        assert!(
429            result.is_err(),
430            "Aggregation on unbounded stream should fail"
431        );
432    }
433
434    #[tokio::test]
435    async fn test_query_with_order_by() {
436        let ctx = create_streaming_context();
437        let schema = test_schema();
438
439        let source = Arc::new(ChannelStreamSource::new(Arc::clone(&schema)));
440        let sender = take_test_sender(&source);
441        let provider = StreamingTableProvider::new("events", source);
442        ctx.register_table("events", Arc::new(provider)).unwrap();
443
444        sender
445            .send(test_batch(&schema, vec![3, 1, 2], vec![30.0, 10.0, 20.0]))
446            .await
447            .unwrap();
448        drop(sender);
449
450        // Query with ORDER BY (`DataFusion` handles this with Sort operator)
451        let df = ctx.sql("SELECT id, value FROM events").await.unwrap();
452        let batches = df.collect().await.unwrap();
453
454        // Verify we got results (ordering may vary due to streaming nature)
455        let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
456        assert_eq!(total_rows, 3);
457    }
458
459    #[tokio::test]
460    async fn test_bridge_throughput() {
461        // Benchmark-style test for bridge performance
462        let schema = test_schema();
463        let bridge = StreamBridge::new(Arc::clone(&schema), 10000);
464        let sender = bridge.sender();
465        let mut stream = bridge.into_stream();
466
467        let batch_count = 1000;
468        let batch = test_batch(&schema, vec![1, 2, 3, 4, 5], vec![1.0, 2.0, 3.0, 4.0, 5.0]);
469
470        // Spawn sender task
471        let send_task = tokio::spawn(async move {
472            for _ in 0..batch_count {
473                sender.send(batch.clone()).await.unwrap();
474            }
475        });
476
477        // Receive all batches
478        let mut received = 0;
479        while let Some(result) = stream.next().await {
480            result.unwrap();
481            received += 1;
482            if received == batch_count {
483                break;
484            }
485        }
486
487        send_task.await.unwrap();
488        assert_eq!(received, batch_count);
489    }
490
491    // ── Integration Tests ──────────────────────────────────────────
492
493    #[test]
494    fn test_streaming_functions_registered() {
495        let ctx = create_streaming_context();
496        // Verify all 4 UDFs are registered
497        assert!(ctx.udf("tumble").is_ok(), "tumble UDF not registered");
498        assert!(ctx.udf("hop").is_ok(), "hop UDF not registered");
499        assert!(ctx.udf("session").is_ok(), "session UDF not registered");
500        assert!(ctx.udf("watermark").is_ok(), "watermark UDF not registered");
501    }
502
503    #[test]
504    fn test_streaming_functions_with_watermark() {
505        use std::sync::atomic::AtomicI64;
506
507        let ctx = create_session_context();
508        let wm = Arc::new(AtomicI64::new(42_000));
509        register_streaming_functions_with_watermark(&ctx, wm);
510
511        assert!(ctx.udf("tumble").is_ok());
512        assert!(ctx.udf("watermark").is_ok());
513    }
514
515    #[tokio::test]
516    async fn test_tumble_udf_via_datafusion() {
517        use arrow_array::{TimestampMicrosecondArray, TimestampMillisecondArray};
518        use arrow_schema::TimeUnit;
519
520        let ctx = create_streaming_context();
521
522        // Create schema with timestamp and value columns
523        let schema = Arc::new(Schema::new(vec![
524            Field::new(
525                "event_time",
526                DataType::Timestamp(TimeUnit::Millisecond, None),
527                false,
528            ),
529            Field::new("value", DataType::Float64, false),
530        ]));
531
532        let source = Arc::new(ChannelStreamSource::new(Arc::clone(&schema)));
533        let sender = take_test_sender(&source);
534        let provider = StreamingTableProvider::new("events", source);
535        ctx.register_table("events", Arc::new(provider)).unwrap();
536
537        // Send events across two 5-minute windows:
538        // Window [0, 300_000): timestamps 60_000, 120_000
539        // Window [300_000, 600_000): timestamps 360_000
540        let batch = RecordBatch::try_new(
541            Arc::clone(&schema),
542            vec![
543                Arc::new(TimestampMillisecondArray::from(vec![
544                    60_000i64, 120_000, 360_000,
545                ])),
546                Arc::new(Float64Array::from(vec![10.0, 20.0, 30.0])),
547            ],
548        )
549        .unwrap();
550        sender.send(batch).await.unwrap();
551        drop(sender);
552
553        // Verify the tumble UDF computes correct window starts via DataFusion
554        // (GROUP BY aggregation and ORDER BY on unbounded streams are handled by Ring 0)
555        let df = ctx
556            .sql(
557                "SELECT tumble(event_time, INTERVAL '5' MINUTE) as window_start, \
558                 value \
559                 FROM events",
560            )
561            .await
562            .unwrap();
563
564        let batches = df.collect().await.unwrap();
565        let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
566        assert_eq!(total_rows, 3);
567
568        // tumble() returns microsecond timestamps for lakehouse-sink compat;
569        // expected window starts are scaled accordingly (× 1000).
570        let ws_col = batches[0]
571            .column(0)
572            .as_any()
573            .downcast_ref::<TimestampMicrosecondArray>()
574            .expect("window_start should be TimestampMicrosecond");
575        // 60_000 and 120_000 ms → window [0, 300_000) ms, start = 0 µs.
576        assert_eq!(ws_col.value(0), 0);
577        assert_eq!(ws_col.value(1), 0);
578        // 360_000 ms → window [300_000, 600_000) ms, start = 300_000_000 µs.
579        assert_eq!(ws_col.value(2), 300_000_000);
580    }
581
582    #[tokio::test]
583    async fn test_logical_plan_from_windowed_query() {
584        use arrow_schema::TimeUnit;
585
586        let ctx = create_streaming_context();
587
588        let schema = Arc::new(Schema::new(vec![
589            Field::new(
590                "event_time",
591                DataType::Timestamp(TimeUnit::Millisecond, None),
592                false,
593            ),
594            Field::new("value", DataType::Float64, false),
595        ]));
596
597        let source = Arc::new(ChannelStreamSource::new(schema));
598        let _sender = source.take_sender();
599        let provider = StreamingTableProvider::new("events", source);
600        ctx.register_table("events", Arc::new(provider)).unwrap();
601
602        // Create a LogicalPlan for a windowed query
603        let df = ctx
604            .sql(
605                "SELECT tumble(event_time, INTERVAL '5' MINUTE) as w, \
606                 COUNT(*) as cnt \
607                 FROM events \
608                 GROUP BY tumble(event_time, INTERVAL '5' MINUTE)",
609            )
610            .await;
611
612        // Should succeed in creating the logical plan (UDFs are registered)
613        assert!(df.is_ok(), "Failed to create logical plan: {df:?}");
614    }
615
616    #[tokio::test]
617    async fn test_end_to_end_execute_streaming_sql() {
618        use crate::planner::StreamingPlanner;
619
620        let ctx = create_streaming_context();
621
622        let schema = Arc::new(Schema::new(vec![
623            Field::new("id", DataType::Int64, false),
624            Field::new("name", DataType::Utf8, true),
625        ]));
626
627        let source = Arc::new(ChannelStreamSource::new(Arc::clone(&schema)));
628        let sender = take_test_sender(&source);
629        let provider = StreamingTableProvider::new("items", source);
630        ctx.register_table("items", Arc::new(provider)).unwrap();
631
632        let batch = RecordBatch::try_new(
633            Arc::clone(&schema),
634            vec![
635                Arc::new(Int64Array::from(vec![1, 2, 3])),
636                Arc::new(arrow_array::StringArray::from(vec!["a", "b", "c"])),
637            ],
638        )
639        .unwrap();
640        sender.send(batch).await.unwrap();
641        drop(sender);
642
643        let mut planner = StreamingPlanner::new();
644        let result = execute_streaming_sql("SELECT id FROM items WHERE id > 1", &ctx, &mut planner)
645            .await
646            .unwrap();
647
648        match result {
649            StreamingSqlResult::Query(qr) => {
650                let mut stream = qr.stream;
651                let mut total = 0;
652                while let Some(batch) = stream.next().await {
653                    total += batch.unwrap().num_rows();
654                }
655                assert_eq!(total, 2); // id=2, id=3
656            }
657            StreamingSqlResult::Ddl(_) => panic!("Expected Query result"),
658        }
659    }
660
661    #[tokio::test]
662    async fn test_watermark_function_in_filter() {
663        use arrow_array::TimestampMillisecondArray;
664        use arrow_schema::TimeUnit;
665        use std::sync::atomic::AtomicI64;
666
667        // Create context with a specific watermark value
668        let config = base_session_config()
669            .with_batch_size(8192)
670            .with_target_partitions(1);
671        let ctx = SessionContext::new_with_config(config);
672        let wm = Arc::new(AtomicI64::new(200_000)); // watermark at 200s
673        register_streaming_functions_with_watermark(&ctx, wm);
674
675        let schema = Arc::new(Schema::new(vec![
676            Field::new(
677                "event_time",
678                DataType::Timestamp(TimeUnit::Millisecond, None),
679                false,
680            ),
681            Field::new("value", DataType::Float64, false),
682        ]));
683
684        let source = Arc::new(ChannelStreamSource::new(Arc::clone(&schema)));
685        let sender = take_test_sender(&source);
686        let provider = StreamingTableProvider::new("events", source);
687        ctx.register_table("events", Arc::new(provider)).unwrap();
688
689        // Events: 100s, 200s, 300s - watermark is at 200s
690        let batch = RecordBatch::try_new(
691            Arc::clone(&schema),
692            vec![
693                Arc::new(TimestampMillisecondArray::from(vec![
694                    100_000i64, 200_000, 300_000,
695                ])),
696                Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0])),
697            ],
698        )
699        .unwrap();
700        sender.send(batch).await.unwrap();
701        drop(sender);
702
703        // Filter events after watermark
704        let df = ctx
705            .sql("SELECT value FROM events WHERE event_time > watermark()")
706            .await
707            .unwrap();
708        let batches = df.collect().await.unwrap();
709        let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
710        // Only event at 300s is after watermark (200s)
711        assert_eq!(total_rows, 1);
712    }
713
714    #[tokio::test]
715    async fn test_date_trunc_available() {
716        let ctx = create_streaming_context();
717        let df = ctx
718            .sql("SELECT date_trunc('hour', TIMESTAMP '2026-01-15 14:30:00')")
719            .await
720            .unwrap();
721        let batches = df.collect().await.unwrap();
722        assert_eq!(batches.len(), 1);
723        assert_eq!(batches[0].num_rows(), 1);
724    }
725
726    #[tokio::test]
727    async fn test_date_bin_available() {
728        let ctx = create_streaming_context();
729        let df = ctx
730            .sql(
731                "SELECT date_bin(\
732                 INTERVAL '15 minutes', \
733                 TIMESTAMP '2026-01-15 14:32:00', \
734                 TIMESTAMP '2026-01-01 00:00:00')",
735            )
736            .await
737            .unwrap();
738        let batches = df.collect().await.unwrap();
739        assert_eq!(batches.len(), 1);
740        assert_eq!(batches[0].num_rows(), 1);
741    }
742
743    #[tokio::test]
744    async fn test_unnest_literal_array() {
745        let ctx = create_streaming_context();
746        let df = ctx
747            .sql("SELECT unnest(make_array(1, 2, 3)) AS val")
748            .await
749            .unwrap();
750        let batches = df.collect().await.unwrap();
751        let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
752        assert_eq!(total_rows, 3);
753    }
754
755    #[tokio::test]
756    async fn test_unnest_from_table_with_array_col() {
757        let ctx = create_streaming_context();
758        // Register a table with an array column
759        ctx.sql(
760            "CREATE TABLE arr_table (id INT, tags INT[]) \
761             AS VALUES (1, make_array(10, 20)), (2, make_array(30))",
762        )
763        .await
764        .unwrap();
765        let df = ctx
766            .sql("SELECT id, unnest(tags) AS tag FROM arr_table")
767            .await
768            .unwrap();
769        let batches = df.collect().await.unwrap();
770        let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
771        // Row 1: [10,20] → 2 rows, Row 2: [30] → 1 row = 3 total
772        assert_eq!(total_rows, 3);
773    }
774}