1pub mod ai_udf;
5mod bridge;
6mod channel_source;
7pub mod complex_type_lambda;
9pub mod complex_type_udf;
11mod exec;
12pub mod execute;
14pub mod format_bridge_udf;
16pub mod json_extensions;
18pub mod json_path;
20pub mod json_tvf;
22pub mod json_types;
24pub mod json_udaf;
26pub mod json_udf;
28pub mod live_source;
30pub mod lookup_join;
32pub mod lookup_join_exec;
34pub mod proctime_udf;
36mod source;
37mod table_provider;
38pub mod watermark_udf;
41pub 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#[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 config = config.with_target_partitions(1);
111 config
112}
113
114#[must_use]
120pub fn create_session_context() -> SessionContext {
121 SessionContext::new_with_config(base_session_config())
122}
123
124#[must_use]
145pub fn create_streaming_context() -> SessionContext {
146 create_streaming_context_with_validator(StreamingValidatorMode::Reject)
147}
148
149#[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 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
187fn 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
209pub fn register_streaming_functions(ctx: &SessionContext) {
214 register_non_watermark_udfs(ctx);
215 ctx.register_udf(ScalarUDF::new_from_impl(WatermarkUdf::unset()));
216}
217
218pub 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
230pub fn register_json_functions(ctx: &SessionContext) {
233 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 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 ctx.register_udf(ScalarUDF::new_from_impl(JsonbContains::new()));
248 ctx.register_udf(ScalarUDF::new_from_impl(JsonbContainedBy::new()));
249
250 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 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 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 ctx.register_udf(ScalarUDF::new_from_impl(JsonbPathExistsUdf::new()));
270 ctx.register_udf(ScalarUDF::new_from_impl(JsonbPathMatchUdf::new()));
271
272 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 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 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 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); let df = ctx.sql("SELECT * FROM events").await.unwrap();
342 let batches = df.collect().await.unwrap();
343
344 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 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 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); }
404
405 #[tokio::test]
406 async fn test_unbounded_aggregation_rejected() {
407 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 let df = ctx.sql("SELECT COUNT(*) as cnt FROM events").await.unwrap();
425
426 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 let df = ctx.sql("SELECT id, value FROM events").await.unwrap();
452 let batches = df.collect().await.unwrap();
453
454 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 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 let send_task = tokio::spawn(async move {
472 for _ in 0..batch_count {
473 sender.send(batch.clone()).await.unwrap();
474 }
475 });
476
477 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 #[test]
494 fn test_streaming_functions_registered() {
495 let ctx = create_streaming_context();
496 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 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 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 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 let ws_col = batches[0]
571 .column(0)
572 .as_any()
573 .downcast_ref::<TimestampMicrosecondArray>()
574 .expect("window_start should be TimestampMicrosecond");
575 assert_eq!(ws_col.value(0), 0);
577 assert_eq!(ws_col.value(1), 0);
578 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 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 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); }
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 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)); 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 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 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 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 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 assert_eq!(total_rows, 3);
773 }
774}