Skip to main content

query/optimizer/
windowed_sort.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::sync::Arc;
16
17use arrow_schema::DataType;
18use datafusion::physical_optimizer::PhysicalOptimizerRule;
19use datafusion::physical_plan::coalesce_partitions::CoalescePartitionsExec;
20use datafusion::physical_plan::coop::CooperativeExec;
21use datafusion::physical_plan::filter::FilterExec;
22use datafusion::physical_plan::projection::ProjectionExec;
23use datafusion::physical_plan::repartition::RepartitionExec;
24use datafusion::physical_plan::sorts::sort::SortExec;
25use datafusion::physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec;
26use datafusion::physical_plan::{ChildrenPropertiesMode, ExecutionPlan, ReplaceChildrenOptions};
27use datafusion_common::Result as DataFusionResult;
28use datafusion_common::tree_node::{Transformed, TreeNode};
29use datafusion_physical_expr::expressions::{CastExpr, Column as PhysicalColumn};
30use datafusion_physical_expr::{PhysicalExpr, ScalarFunctionExpr};
31use store_api::region_engine::PartitionRange;
32use table::table::scan::RegionScanExec;
33
34use crate::part_sort::PartSortExec;
35use crate::window_sort::WindowedSortExec;
36
37/// Optimize rule for windowed sort.
38///
39/// This is expected to run after [`ScanHint`] and [`ParallelizeScan`].
40/// It would change the original sort to a custom plan. To make sure
41/// other rules are applied correctly, this rule can be run as later as
42/// possible.
43///
44/// [`ScanHint`]: crate::optimizer::scan_hint::ScanHintRule
45/// [`ParallelizeScan`]: crate::optimizer::parallelize_scan::ParallelizeScan
46#[derive(Debug)]
47pub struct WindowedSortPhysicalRule;
48
49impl PhysicalOptimizerRule for WindowedSortPhysicalRule {
50    fn optimize(
51        &self,
52        plan: Arc<dyn ExecutionPlan>,
53        config: &datafusion::config::ConfigOptions,
54    ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
55        Self::do_optimize(plan, config)
56    }
57
58    fn name(&self) -> &str {
59        "WindowedSortRule"
60    }
61
62    fn schema_check(&self) -> bool {
63        false
64    }
65}
66
67impl WindowedSortPhysicalRule {
68    fn do_optimize(
69        plan: Arc<dyn ExecutionPlan>,
70        _config: &datafusion::config::ConfigOptions,
71    ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
72        let result = plan
73            .transform_down(|plan| {
74                if let Some(sort_exec) = plan.downcast_ref::<SortExec>() {
75                    // TODO: support multiple expr in windowed sort
76                    if sort_exec.expr().len() != 1 {
77                        return Ok(Transformed::no(plan));
78                    }
79
80                    let preserve_partitioning = sort_exec.preserve_partitioning();
81
82                    let sort_input = remove_repartition(sort_exec.input().clone())?.data;
83
84                    // Gets scanner info from the input without repartition before filter.
85                    let Some(scanner_info) = fetch_partition_range(sort_input.clone())? else {
86                        return Ok(Transformed::no(plan));
87                    };
88                    // Windowed sorting requires partition ranges, which file-engine scans lack.
89                    if scanner_info.partition_ranges.is_empty() {
90                        return Ok(Transformed::no(plan));
91                    }
92                    let input_schema = sort_input.schema();
93
94                    let first_sort_expr = sort_exec.expr().first();
95                    if let Some(column_expr) = first_sort_expr.expr.downcast_ref::<PhysicalColumn>()
96                        && matches!(
97                            input_schema.field(column_expr.index()).data_type(),
98                            DataType::Timestamp(_, _)
99                        )
100                        && is_time_index_expr(&sort_input, &first_sort_expr.expr)?
101                        && sort_exec.fetch().is_none()
102                    // skip if there is a limit, as dyn filter along is good enough in this case
103                    {
104                    } else {
105                        return Ok(Transformed::no(plan));
106                    }
107
108                    // PartSortExec is unnecessary if:
109                    // - there is no tag column, and
110                    // - the sort is ascending on the time index column
111                    let new_input = if scanner_info.tag_columns.is_empty()
112                        && !first_sort_expr.options.descending
113                    {
114                        sort_input
115                    } else {
116                        Arc::new(PartSortExec::try_new(
117                            first_sort_expr.clone(),
118                            sort_exec.fetch(),
119                            scanner_info.partition_ranges.clone(),
120                            sort_input,
121                        )?)
122                    };
123
124                    let windowed_sort_exec = WindowedSortExec::try_new(
125                        first_sort_expr.clone(),
126                        sort_exec.fetch(),
127                        scanner_info.partition_ranges,
128                        new_input,
129                    )?;
130
131                    if !preserve_partitioning {
132                        let order_preserving_merge = SortPreservingMergeExec::new(
133                            sort_exec.expr().clone(),
134                            Arc::new(windowed_sort_exec),
135                        );
136                        return Ok(Transformed {
137                            data: Arc::new(order_preserving_merge),
138                            transformed: true,
139                            tnr: datafusion_common::tree_node::TreeNodeRecursion::Stop,
140                        });
141                    } else {
142                        return Ok(Transformed {
143                            data: Arc::new(windowed_sort_exec),
144                            transformed: true,
145                            tnr: datafusion_common::tree_node::TreeNodeRecursion::Stop,
146                        });
147                    }
148                }
149
150                Ok(Transformed::no(plan))
151            })?
152            .data;
153
154        Ok(result)
155    }
156}
157
158#[derive(Debug)]
159struct ScannerInfo {
160    partition_ranges: Vec<Vec<PartitionRange>>,
161    tag_columns: Vec<String>,
162}
163
164fn fetch_partition_range(input: Arc<dyn ExecutionPlan>) -> DataFusionResult<Option<ScannerInfo>> {
165    let mut partition_ranges = None;
166    let mut tag_columns = None;
167
168    input.transform_up(|plan| {
169        if plan.is::<CooperativeExec>() {
170            return Ok(Transformed::no(plan));
171        }
172
173        // Unappliable case, reset the state.
174        if plan.is::<RepartitionExec>()
175            || plan.is::<CoalescePartitionsExec>()
176            || plan.is::<SortExec>()
177            || plan.is::<WindowedSortExec>()
178        {
179            partition_ranges = None;
180        }
181
182        // only a very limited set of plans can exist between region scan and sort exec
183        // other plans might make this optimize wrong, so be safe here by limiting it
184        if !(plan.is::<ProjectionExec>() || plan.is::<FilterExec>()) {
185            partition_ranges = None;
186        }
187
188        if let Some(region_scan_exec) = plan.downcast_ref::<RegionScanExec>() {
189            // `PerSeries` distribution is not supported in windowed sort.
190            if region_scan_exec.distribution()
191                == Some(store_api::storage::TimeSeriesDistribution::PerSeries)
192            {
193                partition_ranges = None;
194                return Ok(Transformed::no(plan));
195            }
196
197            partition_ranges = Some(region_scan_exec.get_uncollapsed_partition_ranges());
198            tag_columns = Some(region_scan_exec.tag_columns());
199
200            region_scan_exec.with_distinguish_partition_range(true);
201        }
202
203        Ok(Transformed::no(plan))
204    })?;
205
206    let result = (|| {
207        Some(ScannerInfo {
208            partition_ranges: partition_ranges?,
209            tag_columns: tag_columns?,
210        })
211    })();
212
213    Ok(result)
214}
215
216fn is_time_index_expr(
217    plan: &Arc<dyn ExecutionPlan>,
218    expr: &Arc<dyn PhysicalExpr>,
219) -> DataFusionResult<bool> {
220    if let Some(column_expr) = expr.downcast_ref::<PhysicalColumn>() {
221        return is_time_index_column(plan, column_expr);
222    }
223
224    if let Some(cast_expr) = expr.downcast_ref::<CastExpr>() {
225        return if matches!(cast_expr.cast_type(), DataType::Timestamp(_, _)) {
226            is_time_index_expr(plan, cast_expr.expr())
227        } else {
228            Ok(false)
229        };
230    }
231
232    if let Some(scalar_function_expr) = expr.downcast_ref::<ScalarFunctionExpr>() {
233        return if is_supported_time_index_wrapper(scalar_function_expr)
234            && scalar_function_expr.args().len() == 1
235        {
236            is_time_index_expr(plan, &scalar_function_expr.args()[0])
237        } else {
238            Ok(false)
239        };
240    }
241
242    Ok(false)
243}
244
245fn is_time_index_column(
246    plan: &Arc<dyn ExecutionPlan>,
247    column_expr: &PhysicalColumn,
248) -> DataFusionResult<bool> {
249    if let Some(projection) = plan.downcast_ref::<ProjectionExec>() {
250        let Some(projection_expr) = projection.expr().get(column_expr.index()) else {
251            return Ok(false);
252        };
253        return is_time_index_expr(projection.input(), &projection_expr.expr);
254    }
255
256    if let Some(filter) = plan.downcast_ref::<FilterExec>() {
257        let child_column_expr = filter
258            .projection()
259            .as_ref()
260            .and_then(|projection| projection.get(column_expr.index()).copied())
261            .map(|input_index| {
262                PhysicalColumn::new(
263                    filter.input().schema().field(input_index).name(),
264                    input_index,
265                )
266            })
267            .unwrap_or_else(|| column_expr.clone());
268        let child_expr = Arc::new(child_column_expr) as Arc<dyn PhysicalExpr>;
269        return is_time_index_expr(filter.input(), &child_expr);
270    }
271
272    if let Some(region_scan_exec) = plan.downcast_ref::<RegionScanExec>() {
273        let schema = plan.schema();
274        let column_field = schema.field(column_expr.index());
275        return Ok(
276            matches!(column_field.data_type(), DataType::Timestamp(_, _))
277                && *column_field.name() == region_scan_exec.time_index(),
278        );
279    }
280
281    let Some(child) = passthrough_child(plan.as_ref()) else {
282        return Ok(false);
283    };
284    let child_expr = Arc::new(column_expr.clone()) as Arc<dyn PhysicalExpr>;
285    is_time_index_expr(&child, &child_expr)
286}
287
288fn passthrough_child(plan: &dyn ExecutionPlan) -> Option<Arc<dyn ExecutionPlan>> {
289    if plan.is::<CoalescePartitionsExec>()
290        || plan.is::<RepartitionExec>()
291        || plan.is::<CooperativeExec>()
292    {
293        return schema_preserving_child(plan);
294    }
295
296    None
297}
298
299fn schema_preserving_child(plan: &dyn ExecutionPlan) -> Option<Arc<dyn ExecutionPlan>> {
300    let child = plan.children().first().cloned().cloned()?;
301    (plan.schema().as_ref() == child.schema().as_ref()).then_some(child)
302}
303
304fn is_supported_time_index_wrapper(expr: &ScalarFunctionExpr) -> bool {
305    (expr.name().eq_ignore_ascii_case("to_timestamp")
306        || expr.name().eq_ignore_ascii_case("to_timestamp_seconds")
307        || expr.name().eq_ignore_ascii_case("to_timestamp_millis")
308        || expr.name().eq_ignore_ascii_case("to_timestamp_micros")
309        || expr.name().eq_ignore_ascii_case("to_timestamp_nanos"))
310        && matches!(expr.return_type(), DataType::Timestamp(_, _))
311}
312
313/// Removes the repartition plan between the filter and region scan.
314fn remove_repartition(
315    plan: Arc<dyn ExecutionPlan>,
316) -> DataFusionResult<Transformed<Arc<dyn ExecutionPlan>>> {
317    plan.transform_down(|plan| {
318        if plan.is::<FilterExec>() {
319            // Checks child.
320            let maybe_repartition = plan.children()[0];
321            if maybe_repartition.is::<RepartitionExec>() {
322                let maybe_scan = maybe_repartition.children()[0];
323                if maybe_scan.is::<RegionScanExec>() {
324                    let new_filter = plan.clone().replace_children(
325                        vec![maybe_scan.clone()],
326                        ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
327                    )?;
328                    return Ok(Transformed::yes(new_filter));
329                }
330            }
331        }
332
333        Ok(Transformed::no(plan))
334    })
335}
336
337#[cfg(test)]
338mod test {
339    use std::sync::Arc;
340
341    use api::v1::SemanticType;
342    use arrow_schema::{Field, TimeUnit};
343    use common_recordbatch::RecordBatches;
344    use datafusion::config::ConfigOptions;
345    use datafusion::physical_plan::filter::FilterExecBuilder;
346    use datafusion_common::ScalarValue;
347    use datafusion_functions::datetime::to_timestamp_millis;
348    use datafusion_physical_expr::expressions::{CastExpr, Literal};
349    use datatypes::data_type::ConcreteDataType;
350    use datatypes::schema::{ColumnSchema, Schema};
351    use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
352    use store_api::region_engine::SinglePartitionScanner;
353    use store_api::storage::{RegionId, ScanRequest};
354
355    use super::*;
356
357    #[test]
358    fn test_keep_ordinary_sort_when_scan_has_no_partition_ranges() {
359        let ordering = datafusion_physical_expr::LexOrdering::new([
360            datafusion_physical_expr::PhysicalSortExpr {
361                expr: Arc::new(PhysicalColumn::new("ts", 1)),
362                options: Default::default(),
363            },
364        ])
365        .unwrap();
366        let sort = Arc::new(SortExec::new(ordering, new_region_scan()));
367        let optimized =
368            WindowedSortPhysicalRule::do_optimize(sort, &ConfigOptions::default()).unwrap();
369        assert!(optimized.is::<SortExec>());
370    }
371
372    #[test]
373    fn test_is_time_index_expr_tracks_aliases_through_projection() {
374        let scan = new_region_scan();
375        let projection = Arc::new(
376            ProjectionExec::try_new(
377                vec![(
378                    Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>,
379                    "alias_ts".to_string(),
380                )],
381                scan,
382            )
383            .unwrap(),
384        ) as Arc<dyn ExecutionPlan>;
385        let expr = Arc::new(PhysicalColumn::new("alias_ts", 0)) as Arc<dyn PhysicalExpr>;
386
387        assert!(is_time_index_expr(&projection, &expr).unwrap());
388    }
389
390    #[test]
391    fn test_is_time_index_expr_tracks_multi_level_aliases() {
392        let scan = new_region_scan();
393        let first_projection = Arc::new(
394            ProjectionExec::try_new(
395                vec![(
396                    Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>,
397                    "alias_1".to_string(),
398                )],
399                scan,
400            )
401            .unwrap(),
402        ) as Arc<dyn ExecutionPlan>;
403        let second_projection = Arc::new(
404            ProjectionExec::try_new(
405                vec![(
406                    Arc::new(PhysicalColumn::new("alias_1", 0)) as Arc<dyn PhysicalExpr>,
407                    "alias_2".to_string(),
408                )],
409                first_projection,
410            )
411            .unwrap(),
412        ) as Arc<dyn ExecutionPlan>;
413        let expr = Arc::new(PhysicalColumn::new("alias_2", 0)) as Arc<dyn PhysicalExpr>;
414
415        assert!(is_time_index_expr(&second_projection, &expr).unwrap());
416    }
417
418    #[test]
419    fn test_is_time_index_expr_tracks_wrapped_aliases_through_projection() {
420        let scan = new_region_scan();
421        let config = Arc::new(ConfigOptions::default());
422        let return_field = Arc::new(Field::new(
423            "ts",
424            DataType::Timestamp(TimeUnit::Millisecond, None),
425            false,
426        ));
427        let projection = Arc::new(
428            ProjectionExec::try_new(
429                vec![(
430                    Arc::new(ScalarFunctionExpr::new(
431                        "to_timestamp_millis",
432                        to_timestamp_millis(config.as_ref()),
433                        vec![Arc::new(PhysicalColumn::new("ts", 1))],
434                        return_field,
435                        config,
436                    )) as Arc<dyn PhysicalExpr>,
437                    "ts".to_string(),
438                )],
439                scan,
440            )
441            .unwrap(),
442        ) as Arc<dyn ExecutionPlan>;
443        let expr = Arc::new(PhysicalColumn::new("ts", 0)) as Arc<dyn PhysicalExpr>;
444
445        assert!(is_time_index_expr(&projection, &expr).unwrap());
446    }
447
448    #[test]
449    fn test_is_time_index_expr_tracks_cast_aliases_through_projection() {
450        let scan = new_region_scan();
451        let projection = Arc::new(
452            ProjectionExec::try_new(
453                vec![(
454                    Arc::new(CastExpr::new(
455                        Arc::new(PhysicalColumn::new("ts", 1)),
456                        DataType::Timestamp(TimeUnit::Millisecond, None),
457                        None,
458                    )) as Arc<dyn PhysicalExpr>,
459                    "ts_ms".to_string(),
460                )],
461                scan,
462            )
463            .unwrap(),
464        ) as Arc<dyn ExecutionPlan>;
465        let expr = Arc::new(PhysicalColumn::new("ts_ms", 0)) as Arc<dyn PhysicalExpr>;
466
467        assert!(is_time_index_expr(&projection, &expr).unwrap());
468    }
469
470    #[test]
471    fn test_is_time_index_expr_rejects_unsupported_wrappers() {
472        let scan = new_region_scan();
473        let config = Arc::new(ConfigOptions::default());
474        let return_field = Arc::new(Field::new(
475            "ts",
476            DataType::Timestamp(TimeUnit::Millisecond, None),
477            false,
478        ));
479        let projection = Arc::new(
480            ProjectionExec::try_new(
481                vec![(
482                    Arc::new(ScalarFunctionExpr::new(
483                        "date_trunc",
484                        to_timestamp_millis(config.as_ref()),
485                        vec![Arc::new(PhysicalColumn::new("ts", 1))],
486                        return_field,
487                        config,
488                    )) as Arc<dyn PhysicalExpr>,
489                    "ts".to_string(),
490                )],
491                scan,
492            )
493            .unwrap(),
494        ) as Arc<dyn ExecutionPlan>;
495        let expr = Arc::new(PhysicalColumn::new("ts", 0)) as Arc<dyn PhysicalExpr>;
496
497        assert!(!is_time_index_expr(&projection, &expr).unwrap());
498    }
499
500    #[test]
501    fn test_is_supported_time_index_wrapper_ignores_function_name_case() {
502        let config = Arc::new(ConfigOptions::default());
503        let return_field = Arc::new(Field::new(
504            "ts",
505            DataType::Timestamp(TimeUnit::Millisecond, None),
506            false,
507        ));
508        let expr = ScalarFunctionExpr::new(
509            "To_Timestamp_Millis",
510            to_timestamp_millis(config.as_ref()),
511            vec![Arc::new(PhysicalColumn::new("ts", 1))],
512            return_field,
513            config,
514        );
515
516        assert!(is_supported_time_index_wrapper(&expr));
517    }
518
519    #[test]
520    fn test_is_time_index_expr_rejects_non_timestamp_casts() {
521        let scan = new_region_scan();
522        let cast_expr = Arc::new(CastExpr::new(
523            Arc::new(PhysicalColumn::new("ts", 1)),
524            DataType::Timestamp(TimeUnit::Millisecond, None),
525            None,
526        )) as Arc<dyn PhysicalExpr>;
527        assert!(is_time_index_expr(&scan, &cast_expr).unwrap());
528
529        let non_timestamp_cast = Arc::new(CastExpr::new(
530            Arc::new(PhysicalColumn::new("ts", 1)),
531            DataType::Int64,
532            None,
533        )) as Arc<dyn PhysicalExpr>;
534        assert!(!is_time_index_expr(&scan, &non_timestamp_cast).unwrap());
535    }
536
537    #[test]
538    fn test_is_time_index_expr_tracks_time_index_through_filter() {
539        let scan = new_region_scan();
540        let filter = Arc::new(
541            FilterExec::try_new(
542                Arc::new(Literal::new(ScalarValue::Boolean(Some(true)))),
543                scan,
544            )
545            .unwrap(),
546        ) as Arc<dyn ExecutionPlan>;
547        let expr = Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>;
548
549        assert!(is_time_index_expr(&filter, &expr).unwrap());
550    }
551
552    #[test]
553    fn test_is_time_index_expr_tracks_time_index_through_passthrough_wrapper_and_filter_projection()
554    {
555        let scan = new_region_scan();
556        let projected_filter = Arc::new(
557            FilterExecBuilder::new(
558                Arc::new(Literal::new(ScalarValue::Boolean(Some(true)))),
559                scan,
560            )
561            .apply_projection(Some(vec![1]))
562            .unwrap()
563            .build()
564            .unwrap(),
565        ) as Arc<dyn ExecutionPlan>;
566        let cooperative =
567            Arc::new(CooperativeExec::new(projected_filter)) as Arc<dyn ExecutionPlan>;
568        let expr = Arc::new(PhysicalColumn::new("ts", 0)) as Arc<dyn PhysicalExpr>;
569
570        assert!(is_time_index_expr(&cooperative, &expr).unwrap());
571    }
572
573    #[test]
574    fn test_schema_preserving_child_rejects_schema_changing_projection() {
575        let scan = new_region_scan();
576        let projection = ProjectionExec::try_new(
577            vec![(
578                Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>,
579                "ts".to_string(),
580            )],
581            scan,
582        )
583        .unwrap();
584
585        assert!(schema_preserving_child(&projection).is_none());
586    }
587
588    #[test]
589    fn test_cooperative_exec_satisfies_passthrough_schema_contract() {
590        let child = new_region_scan();
591        let plan = Arc::new(CooperativeExec::new(child.clone())) as Arc<dyn ExecutionPlan>;
592
593        assert_passthrough_schema_contract(plan, child);
594    }
595
596    #[test]
597    fn test_repartition_exec_satisfies_passthrough_schema_contract() {
598        let child = new_region_scan();
599        let plan = Arc::new(
600            RepartitionExec::try_new(
601                child.clone(),
602                datafusion_physical_expr::Partitioning::RoundRobinBatch(2),
603            )
604            .unwrap(),
605        ) as Arc<dyn ExecutionPlan>;
606
607        assert_passthrough_schema_contract(plan, child);
608    }
609
610    #[test]
611    fn test_coalesce_partitions_exec_satisfies_passthrough_schema_contract() {
612        let child = new_region_scan();
613        let plan = Arc::new(CoalescePartitionsExec::new(child.clone())) as Arc<dyn ExecutionPlan>;
614
615        assert_passthrough_schema_contract(plan, child);
616    }
617
618    fn assert_passthrough_schema_contract(
619        plan: Arc<dyn ExecutionPlan>,
620        child: Arc<dyn ExecutionPlan>,
621    ) {
622        assert_eq!(plan.schema().as_ref(), child.schema().as_ref());
623
624        let passthrough = passthrough_child(plan.as_ref()).expect("wrapper should preserve schema");
625        assert_eq!(passthrough.schema().as_ref(), child.schema().as_ref());
626    }
627
628    fn new_region_scan() -> Arc<dyn ExecutionPlan> {
629        let schema = Arc::new(Schema::new(vec![
630            ColumnSchema::new("value", ConcreteDataType::int32_datatype(), false),
631            ColumnSchema::new(
632                "ts",
633                ConcreteDataType::timestamp_nanosecond_datatype(),
634                false,
635            ),
636        ]));
637        let recordbatches = RecordBatches::try_new(schema.clone(), vec![]).unwrap();
638        let stream = recordbatches.as_stream();
639
640        let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 1));
641        builder
642            .push_column_metadata(ColumnMetadata {
643                column_schema: ColumnSchema::new(
644                    "value",
645                    ConcreteDataType::int32_datatype(),
646                    false,
647                ),
648                semantic_type: SemanticType::Field,
649                column_id: 1,
650            })
651            .push_column_metadata(ColumnMetadata {
652                column_schema: ColumnSchema::new(
653                    "ts",
654                    ConcreteDataType::timestamp_nanosecond_datatype(),
655                    false,
656                ),
657                semantic_type: SemanticType::Timestamp,
658                column_id: 2,
659            });
660
661        let scanner = Box::new(SinglePartitionScanner::new(
662            stream,
663            false,
664            Arc::new(builder.build().unwrap()),
665            None,
666        ));
667        Arc::new(RegionScanExec::new(scanner, ScanRequest::default(), None).unwrap())
668    }
669}