Skip to main content

risingwave_frontend/planner/
relation.rs

1// Copyright 2022 RisingWave Labs
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::collections::HashMap;
16use std::ops::Deref;
17use std::rc::Rc;
18
19use itertools::Itertools;
20use risingwave_common::bail_not_implemented;
21use risingwave_common::catalog::{
22    ColumnCatalog, Engine, Field, RISINGWAVE_ICEBERG_ROW_ID, ROW_ID_COLUMN_NAME, Schema,
23};
24use risingwave_common::session_config::IcebergQueryStorageMode;
25use risingwave_common::types::{DataType, Interval, ScalarImpl};
26use risingwave_connector::source::ConnectorProperties;
27use risingwave_connector::source::iceberg::IcebergTimeTravelInfo;
28use risingwave_sqlparser::ast::AsOf;
29
30use crate::TableCatalog;
31use crate::binder::{
32    BoundBaseTable, BoundGapFill, BoundIcebergMetadataTable, BoundJoin, BoundShare,
33    BoundShareInput, BoundSource, BoundSystemTable, BoundWatermark, BoundWindowTableFunction,
34    Relation, WindowTableFunctionKind,
35};
36use crate::catalog::source_catalog::SourceCatalog;
37use crate::error::{ErrorCode, Result};
38use crate::expr::{CastContext, Expr, ExprImpl, ExprType, FunctionCall, InputRef, Literal};
39use crate::optimizer::plan_node::generic::{GenericPlanRef, SourceNodeKind};
40use crate::optimizer::plan_node::utils::to_iceberg_time_travel_as_of;
41use crate::optimizer::plan_node::{
42    LogicalApply, LogicalGapFill, LogicalHopWindow, LogicalIcebergIntermediateScan,
43    LogicalIcebergMetadataScan, LogicalJoin, LogicalPlanRef as PlanRef, LogicalProject,
44    LogicalScan, LogicalShare, LogicalSource, LogicalSysScan, LogicalTableFunction, LogicalValues,
45};
46use crate::optimizer::property::Cardinality;
47use crate::planner::{PlanFor, Planner};
48use crate::utils::{ColIndexMapping, Condition};
49
50const ERROR_WINDOW_SIZE_ARG: &str =
51    "The size arg of window table function should be an interval literal.";
52
53impl Planner {
54    pub fn plan_relation(&mut self, relation: Relation) -> Result<PlanRef> {
55        match relation {
56            Relation::BaseTable(t) => self.plan_base_table(&t),
57            Relation::SystemTable(st) => self.plan_sys_table(*st),
58            Relation::IcebergMetadataTable(table) => self.plan_iceberg_metadata_table(*table),
59            // TODO: order is ignored in the subquery
60            Relation::Subquery(q) => Ok(self.plan_query(q.query)?.into_unordered_subplan()),
61            Relation::Join(join) => self.plan_join(*join),
62            Relation::Apply(join) => self.plan_apply(*join),
63            Relation::WindowTableFunction(tf) => self.plan_window_table_function(*tf),
64            Relation::Source(s) => self.plan_source(*s),
65            Relation::TableFunction {
66                expr: tf,
67                with_ordinality,
68            } => self.plan_table_function(tf, with_ordinality),
69            Relation::Watermark(tf) => self.plan_watermark(*tf),
70            Relation::Share(share) => self.plan_share(*share),
71            Relation::GapFill(bound_gap_fill) => self.plan_gap_fill(*bound_gap_fill),
72        }
73    }
74
75    pub(crate) fn plan_sys_table(&mut self, sys_table: BoundSystemTable) -> Result<PlanRef> {
76        Ok(LogicalSysScan::create(
77            sys_table.sys_table_catalog,
78            self.ctx(),
79            Cardinality::unknown(), // TODO(card): cardinality of system table
80        )
81        .into())
82    }
83
84    fn plan_iceberg_metadata_table(&mut self, table: BoundIcebergMetadataTable) -> Result<PlanRef> {
85        let timezone = self.ctx().get_session_timezone();
86        let time_travel_info = to_iceberg_time_travel_as_of(&table.as_of, &timezone)?;
87        let core = crate::optimizer::plan_node::generic::IcebergMetadataScan {
88            metadata_type: table.metadata_type,
89            properties: table.properties,
90            secret_refs: table.secret_refs,
91            time_travel_info,
92            ctx: self.ctx(),
93        };
94        Ok(LogicalIcebergMetadataScan::new(core).into())
95    }
96
97    pub(super) fn plan_base_table(&mut self, base_table: &BoundBaseTable) -> Result<PlanRef> {
98        let as_of = base_table.as_of.clone();
99        let scan = LogicalScan::from_base_table(base_table, self.ctx(), as_of.clone());
100
101        match base_table.table_catalog.engine {
102            Engine::Hummock => {
103                match as_of {
104                    None
105                    | Some(AsOf::ProcessTime)
106                    | Some(AsOf::TimestampNum(_))
107                    | Some(AsOf::TimestampString(_))
108                    | Some(AsOf::ProcessTimeWithInterval(_)) => {}
109                    Some(AsOf::VersionNum(_)) | Some(AsOf::VersionString(_)) => {
110                        bail_not_implemented!("As Of Version is not supported yet.")
111                    }
112                };
113                Ok(scan.into())
114            }
115            Engine::Iceberg => self.plan_iceberg_table(base_table, scan, as_of),
116        }
117    }
118
119    fn plan_iceberg_table(
120        &mut self,
121        base_table: &BoundBaseTable,
122        scan: LogicalScan,
123        as_of: Option<AsOf>,
124    ) -> Result<PlanRef> {
125        let is_append_only = base_table.table_catalog.append_only;
126        let iceberg_query_storage_mode = self
127            .ctx()
128            .session_ctx()
129            .config()
130            .iceberg_query_storage_mode();
131
132        enum PlanTarget {
133            TableScan,
134            Source,
135            IntermediateScan,
136        }
137        let plan_target = match self.plan_for() {
138            PlanFor::StreamIcebergEngineInternal => PlanTarget::TableScan,
139            PlanFor::BatchDql => match iceberg_query_storage_mode {
140                IcebergQueryStorageMode::Hummock => PlanTarget::TableScan,
141                _ => PlanTarget::IntermediateScan,
142            },
143            PlanFor::Stream => {
144                if is_append_only {
145                    PlanTarget::Source
146                } else {
147                    PlanTarget::TableScan
148                }
149            }
150            PlanFor::Batch => {
151                if is_append_only {
152                    PlanTarget::IntermediateScan
153                } else {
154                    PlanTarget::TableScan
155                }
156            }
157        };
158        match as_of {
159            None
160            | Some(AsOf::VersionNum(_))
161            | Some(AsOf::TimestampString(_))
162            | Some(AsOf::TimestampNum(_)) => {}
163            Some(AsOf::ProcessTime) | Some(AsOf::ProcessTimeWithInterval(_)) => {
164                bail_not_implemented!("As Of ProcessTime() is not supported yet.")
165            }
166            Some(AsOf::VersionString(_)) => {
167                bail_not_implemented!("As Of Version is not supported yet.")
168            }
169        }
170
171        if matches!(plan_target, PlanTarget::TableScan) {
172            return Ok(scan.into());
173        }
174
175        let source_catalog = self.get_iceberg_source_by_table_catalog(&base_table.table_catalog)
176            .ok_or_else(|| {
177                ErrorCode::BindError(format!(
178                    "failed to plan an iceberg engine table: {}. Can't find the corresponding iceberg source. Maybe you need to recreate the table",
179                    base_table.table_catalog.name()
180                    ))
181            })?;
182
183        // Build type mapping: source column name → Hummock table type.
184        // This lets the intermediate scan output Hummock types directly,
185        // while the Iceberg scan path still adds explicit casts after
186        // materialization to match the expected output types.
187        let mut table_column_type_mapping = HashMap::new();
188        let table_column_map: HashMap<&str, &DataType> = base_table
189            .table_catalog
190            .columns
191            .iter()
192            .map(|c| (c.name.as_str(), &c.column_desc.data_type))
193            .collect();
194        for source_col in &source_catalog.columns {
195            let source_name = source_col.name();
196            let table_name = if source_name == RISINGWAVE_ICEBERG_ROW_ID {
197                ROW_ID_COLUMN_NAME
198            } else {
199                source_name
200            };
201            if let Some(&table_type) = table_column_map.get(table_name)
202                && source_col.column_desc.data_type != *table_type
203            {
204                table_column_type_mapping.insert(source_name.to_owned(), table_type.clone());
205            }
206        }
207
208        let column_map: HashMap<String, (usize, ColumnCatalog)> = source_catalog
209            .columns
210            .clone()
211            .into_iter()
212            .enumerate()
213            .map(|(i, column)| (column.name().to_owned(), (i, column)))
214            .collect();
215        // For intermediate scan, it will output Hummock types directly. But for source, it still
216        // outputs original types. So only when the plan target is source and the column type is
217        // different, we need to add cast.
218        let exprs = scan
219            .table()
220            .column_schema()
221            .fields()
222            .iter()
223            .map(|field| {
224                let source_filed_name = if field.name == ROW_ID_COLUMN_NAME {
225                    RISINGWAVE_ICEBERG_ROW_ID
226                } else {
227                    &field.name
228                };
229                if let Some((i, source_column)) = column_map.get(source_filed_name) {
230                    let input_type = &source_column.column_desc.data_type;
231                    if matches!(plan_target, PlanTarget::Source) && input_type != &field.data_type {
232                        let mut input_ref =
233                            ExprImpl::InputRef(InputRef::new(*i, input_type.clone()).into());
234                        FunctionCall::cast_mut(
235                            &mut input_ref,
236                            &field.data_type,
237                            CastContext::Explicit,
238                        )
239                        .unwrap();
240                        input_ref
241                    } else {
242                        ExprImpl::InputRef(InputRef::new(*i, field.data_type.clone()).into())
243                    }
244                } else {
245                    // fields like `_rw_timestamp`, would not be found in source.
246                    ExprImpl::Literal(Literal::new(None, field.data_type.clone()).into())
247                }
248            })
249            .collect_vec();
250
251        // Build source→table column index mapping for Hummock rewrite.
252        // Must be built before source_catalog is moved into Rc.
253        let table_col_index: HashMap<&str, usize> = base_table
254            .table_catalog
255            .columns
256            .iter()
257            .enumerate()
258            .map(|(i, c)| (c.name.as_str(), i))
259            .collect();
260        let source_to_table_mapping = ColIndexMapping::new(
261            source_catalog
262                .columns
263                .iter()
264                .map(|c| {
265                    let table_name = if c.name() == RISINGWAVE_ICEBERG_ROW_ID {
266                        ROW_ID_COLUMN_NAME
267                    } else {
268                        c.name()
269                    };
270                    table_col_index.get(table_name).copied()
271                })
272                .collect(),
273            base_table.table_catalog.columns.len(),
274        );
275
276        let logical_source = LogicalSource::with_catalog(
277            Rc::new(source_catalog),
278            SourceNodeKind::CreateMViewOrBatch,
279            self.ctx(),
280            as_of,
281        )?;
282        if matches!(plan_target, PlanTarget::Source) {
283            return Ok(LogicalProject::new(logical_source.into(), exprs).into());
284        }
285
286        let logical_iceberg_intermediate_scan = self.plan_iceberg_intermediate_scan(
287            &logical_source,
288            table_column_type_mapping,
289            source_to_table_mapping,
290        )?;
291        Ok(LogicalProject::new(logical_iceberg_intermediate_scan, exprs).into())
292    }
293
294    pub(super) fn plan_source(&mut self, source: BoundSource) -> Result<PlanRef> {
295        if source.is_shareable_cdc_connector() {
296            Err(ErrorCode::InternalError(
297                "Should not create MATERIALIZED VIEW or SELECT directly on shared CDC source. HINT: create TABLE from the source instead.".to_owned(),
298            )
299            .into())
300        } else {
301            let as_of = source.as_of.clone();
302            match as_of {
303                None
304                | Some(AsOf::VersionNum(_))
305                | Some(AsOf::TimestampString(_))
306                | Some(AsOf::TimestampNum(_)) => {}
307                Some(AsOf::ProcessTime) | Some(AsOf::ProcessTimeWithInterval(_)) => {
308                    bail_not_implemented!("As Of ProcessTime() is not supported yet.")
309                }
310                Some(AsOf::VersionString(_)) => {
311                    bail_not_implemented!("As Of Version is not supported yet.")
312                }
313            }
314            let is_iceberg = source.catalog.is_iceberg_connector();
315
316            // validate the source has pk. We raise an error here to avoid panic in expect_stream_key later
317            // for a nicer error message.
318            if matches!(self.plan_for(), PlanFor::Stream) {
319                let has_pk =
320                    source.catalog.row_id_index.is_some() || !source.catalog.pk_col_ids.is_empty();
321                if !has_pk {
322                    // in older version, iceberg source doesn't have row_id, thus may hit this
323                    // only iceberg should hit this.
324                    debug_assert!(is_iceberg);
325                    if is_iceberg {
326                        return Err(ErrorCode::BindError(format!(
327                        "Cannot create a stream job from an iceberg source without a primary key.\nThe iceberg source might be created in an older version of RisingWave. Please try recreating the source.\nSource: {:?}",
328                        source.catalog
329                    ))
330                    .into());
331                    } else {
332                        return Err(ErrorCode::BindError(format!(
333                            "Cannot create a stream job from a source without a primary key.
334This is a bug. We would appreciate a bug report at:
335https://github.com/risingwavelabs/risingwave/issues/new?labels=type%2Fbug&template=bug_report.yml
336
337source: {:?}",
338                            source.catalog
339                        ))
340                        .into());
341                    }
342                }
343            }
344
345            let source = LogicalSource::with_catalog(
346                Rc::new(source.catalog),
347                SourceNodeKind::CreateMViewOrBatch,
348                self.ctx(),
349                as_of,
350            )?;
351            if is_iceberg && !matches!(self.plan_for(), PlanFor::Stream) {
352                let num_cols = source.core.column_catalog.len();
353                let intermediate_scan = self.plan_iceberg_intermediate_scan(
354                    &source,
355                    HashMap::new(),
356                    ColIndexMapping::identity(num_cols),
357                )?;
358                Ok(intermediate_scan)
359            } else {
360                Ok(source.into())
361            }
362        }
363    }
364
365    pub(super) fn plan_join(&mut self, join: BoundJoin) -> Result<PlanRef> {
366        let left = self.plan_relation(join.left)?;
367        let right = self.plan_relation(join.right)?;
368        let join_type = join.join_type;
369        let on_clause = join.cond;
370        if on_clause.has_subquery() {
371            bail_not_implemented!("Subquery in join on condition");
372        } else {
373            Ok(LogicalJoin::create(left, right, join_type, on_clause))
374        }
375    }
376
377    pub(super) fn plan_apply(&mut self, mut join: BoundJoin) -> Result<PlanRef> {
378        let join_type = join.join_type;
379        let on_clause = join.cond;
380        if on_clause.has_subquery() {
381            bail_not_implemented!("Subquery in join on condition");
382        }
383
384        let correlated_id = self.ctx.next_correlated_id();
385        let correlated_indices = join
386            .right
387            .collect_correlated_indices_by_depth_and_assign_id(0, correlated_id);
388        let left = self.plan_relation(join.left)?;
389        let right = self.plan_relation(join.right)?;
390
391        Ok(LogicalApply::create(
392            left,
393            right,
394            join_type,
395            Condition::with_expr(on_clause),
396            correlated_id,
397            correlated_indices,
398            false,
399        ))
400    }
401
402    pub(super) fn plan_window_table_function(
403        &mut self,
404        table_function: BoundWindowTableFunction,
405    ) -> Result<PlanRef> {
406        use WindowTableFunctionKind::*;
407        match table_function.kind {
408            Tumble => self.plan_tumble_window(
409                table_function.input,
410                table_function.time_col,
411                table_function.args,
412            ),
413            Hop => self.plan_hop_window(
414                table_function.input,
415                table_function.time_col,
416                table_function.args,
417            ),
418        }
419    }
420
421    pub(super) fn plan_table_function(
422        &mut self,
423        table_function: ExprImpl,
424        with_ordinality: bool,
425    ) -> Result<PlanRef> {
426        // TODO: maybe we can unify LogicalTableFunction with LogicalValues
427        match table_function {
428            ExprImpl::TableFunction(tf) => {
429                Ok(LogicalTableFunction::new(*tf, with_ordinality, self.ctx()).into())
430            }
431            expr => {
432                let schema = Schema {
433                    // TODO: should be named
434                    fields: vec![Field::unnamed(expr.return_type())],
435                };
436                let expr_return_type = expr.return_type();
437                let root = LogicalValues::create(vec![vec![expr]], schema, self.ctx());
438                let input_ref = ExprImpl::from(InputRef::new(0, expr_return_type.clone()));
439                let mut exprs = if let DataType::Struct(st) = expr_return_type {
440                    st.iter()
441                        .enumerate()
442                        .map(|(i, (_, ty))| {
443                            let idx = ExprImpl::literal_int(i.try_into().unwrap());
444                            let args = vec![input_ref.clone(), idx];
445                            FunctionCall::new_unchecked(ExprType::Field, args, ty.clone()).into()
446                        })
447                        .collect()
448                } else {
449                    vec![input_ref]
450                };
451                if with_ordinality {
452                    exprs.push(ExprImpl::literal_bigint(1));
453                }
454                Ok(LogicalProject::create(root, exprs))
455            }
456        }
457    }
458
459    pub(super) fn plan_share(&mut self, share: BoundShare) -> Result<PlanRef> {
460        match share.input {
461            BoundShareInput::Query(query) => {
462                let id = share.share_id;
463                match self.share_cache.get(&id) {
464                    None => {
465                        let result = self.plan_query(query)?.into_unordered_subplan();
466                        let logical_share = LogicalShare::create(result);
467                        self.share_cache.insert(id, logical_share.clone());
468                        Ok(logical_share)
469                    }
470                    Some(result) => Ok(result.clone()),
471                }
472            }
473            BoundShareInput::ChangeLog(relation) => {
474                let id = share.share_id;
475                let result = self.plan_changelog(relation)?;
476                let logical_share = LogicalShare::create(result);
477                self.share_cache.insert(id, logical_share.clone());
478                Ok(logical_share)
479            }
480        }
481    }
482
483    pub(super) fn plan_watermark(&mut self, _watermark: BoundWatermark) -> Result<PlanRef> {
484        todo!("plan watermark");
485    }
486
487    pub(super) fn plan_gap_fill(&mut self, gap_fill: BoundGapFill) -> Result<PlanRef> {
488        let input = self.plan_relation(gap_fill.input)?;
489        Ok(LogicalGapFill::new(
490            input,
491            gap_fill.time_col,
492            gap_fill.interval,
493            gap_fill.fill_strategies,
494            gap_fill.partition_by_cols,
495        )
496        .into())
497    }
498
499    fn collect_col_data_types_for_tumble_window(relation: &Relation) -> Result<Vec<DataType>> {
500        let col_data_types = match relation {
501            Relation::Source(s) => s
502                .catalog
503                .columns
504                .iter()
505                .map(|col| col.data_type().clone())
506                .collect(),
507            Relation::BaseTable(t) => t
508                .table_catalog
509                .columns
510                .iter()
511                .map(|col| col.data_type().clone())
512                .collect(),
513            Relation::Subquery(q) => q.query.schema().data_types(),
514            Relation::Share(share) => share
515                .input
516                .fields()?
517                .into_iter()
518                .map(|(_, f)| f.data_type)
519                .collect(),
520            r => {
521                return Err(ErrorCode::BindError(format!(
522                    "Invalid input relation to tumble: {r:?}"
523                ))
524                .into());
525            }
526        };
527        Ok(col_data_types)
528    }
529
530    fn plan_tumble_window(
531        &mut self,
532        input: Relation,
533        time_col: InputRef,
534        args: Vec<ExprImpl>,
535    ) -> Result<PlanRef> {
536        let mut args = args.into_iter();
537        let col_data_types: Vec<_> = Self::collect_col_data_types_for_tumble_window(&input)?;
538
539        match (args.next(), args.next(), args.next()) {
540            (Some(window_size @ ExprImpl::Literal(_)), None, None) => {
541                let mut exprs = Vec::with_capacity(col_data_types.len() + 2);
542                for (idx, col_dt) in col_data_types.iter().enumerate() {
543                    exprs.push(InputRef::new(idx, col_dt.clone()).into());
544                }
545                let window_start: ExprImpl = FunctionCall::new(
546                    ExprType::TumbleStart,
547                    vec![ExprImpl::InputRef(Box::new(time_col)), window_size.clone()],
548                )?
549                .into();
550                // TODO: `window_end` may be optimized to avoid double calculation of
551                // `tumble_start`, or we can depends on common expression
552                // optimization.
553                let window_end =
554                    FunctionCall::new(ExprType::Add, vec![window_start.clone(), window_size])?
555                        .into();
556                exprs.push(window_start);
557                exprs.push(window_end);
558                let base = self.plan_relation(input)?;
559                let project = LogicalProject::create(base, exprs);
560                Ok(project)
561            }
562            (
563                Some(window_size @ ExprImpl::Literal(_)),
564                Some(window_offset @ ExprImpl::Literal(_)),
565                None,
566            ) => {
567                let mut exprs = Vec::with_capacity(col_data_types.len() + 2);
568                for (idx, col_dt) in col_data_types.iter().enumerate() {
569                    exprs.push(InputRef::new(idx, col_dt.clone()).into());
570                }
571                let window_start: ExprImpl = FunctionCall::new(
572                    ExprType::TumbleStart,
573                    vec![
574                        ExprImpl::InputRef(Box::new(time_col)),
575                        window_size.clone(),
576                        window_offset,
577                    ],
578                )?
579                .into();
580                // TODO: `window_end` may be optimized to avoid double calculation of
581                // `tumble_start`, or we can depends on common expression
582                // optimization.
583                let window_end =
584                    FunctionCall::new(ExprType::Add, vec![window_start.clone(), window_size])?
585                        .into();
586                exprs.push(window_start);
587                exprs.push(window_end);
588                let base = self.plan_relation(input)?;
589                let project = LogicalProject::create(base, exprs);
590                Ok(project)
591            }
592            _ => Err(ErrorCode::BindError(ERROR_WINDOW_SIZE_ARG.to_owned()).into()),
593        }
594    }
595
596    fn plan_hop_window(
597        &mut self,
598        input: Relation,
599        time_col: InputRef,
600        args: Vec<ExprImpl>,
601    ) -> Result<PlanRef> {
602        let input = self.plan_relation(input)?;
603        let mut args = args.into_iter();
604        let Some((ExprImpl::Literal(window_slide), ExprImpl::Literal(window_size))) =
605            args.next_tuple()
606        else {
607            return Err(ErrorCode::BindError(ERROR_WINDOW_SIZE_ARG.to_owned()).into());
608        };
609
610        let Some(ScalarImpl::Interval(window_slide)) = *window_slide.get_data() else {
611            return Err(ErrorCode::BindError(ERROR_WINDOW_SIZE_ARG.to_owned()).into());
612        };
613        let Some(ScalarImpl::Interval(window_size)) = *window_size.get_data() else {
614            return Err(ErrorCode::BindError(ERROR_WINDOW_SIZE_ARG.to_owned()).into());
615        };
616
617        let window_offset = match (args.next(), args.next()) {
618            (Some(ExprImpl::Literal(window_offset)), None) => match *window_offset.get_data() {
619                Some(ScalarImpl::Interval(window_offset)) => window_offset,
620                _ => return Err(ErrorCode::BindError(ERROR_WINDOW_SIZE_ARG.to_owned()).into()),
621            },
622            (None, None) => Interval::from_month_day_usec(0, 0, 0),
623            _ => return Err(ErrorCode::BindError(ERROR_WINDOW_SIZE_ARG.to_owned()).into()),
624        };
625
626        if !window_size.is_positive() || !window_slide.is_positive() {
627            return Err(ErrorCode::BindError(format!(
628                "window_size {} and window_slide {} must be positive",
629                window_size, window_slide
630            ))
631            .into());
632        }
633
634        if window_size.exact_div(&window_slide).is_none() {
635            return Err(ErrorCode::BindError(format!("Invalid arguments for HOP window function: window_size {} cannot be divided by window_slide {}",window_size, window_slide)).into());
636        }
637
638        Ok(LogicalHopWindow::create(
639            input,
640            time_col,
641            window_slide,
642            window_size,
643            window_offset,
644        ))
645    }
646
647    fn plan_iceberg_intermediate_scan(
648        &self,
649        source: &LogicalSource,
650        table_column_type_mapping: HashMap<String, DataType>,
651        source_to_table_mapping: ColIndexMapping,
652    ) -> Result<PlanRef> {
653        // If time travel is not specified, we use current timestamp to get the latest snapshot
654        let timezone = self.ctx().get_session_timezone();
655        let mut time_travel_info = to_iceberg_time_travel_as_of(&source.core.as_of, &timezone)?;
656        if time_travel_info.is_none() {
657            time_travel_info = self
658                .fetch_current_snapshot_id(source)?
659                .map(IcebergTimeTravelInfo::Version);
660        }
661        let Some(time_travel_info) = time_travel_info else {
662            let mut schema = source.schema().clone();
663            for field in &mut schema.fields {
664                if let Some(target_type) = table_column_type_mapping.get(&field.name) {
665                    field.data_type = target_type.clone();
666                }
667            }
668            return Ok(LogicalValues::new(vec![], schema, self.ctx()).into());
669        };
670        let intermediate_scan = LogicalIcebergIntermediateScan::new(
671            source,
672            time_travel_info,
673            table_column_type_mapping,
674            source_to_table_mapping,
675        );
676        Ok(intermediate_scan.into())
677    }
678
679    fn fetch_current_snapshot_id(&self, source: &LogicalSource) -> Result<Option<i64>> {
680        let mut map = self.ctx.iceberg_snapshot_id_map();
681        let catalog = source.source_catalog().ok_or_else(|| {
682            crate::error::ErrorCode::InternalError(
683                "Iceberg source must have a valid source catalog".to_owned(),
684            )
685        })?;
686        let name = catalog.name.as_str();
687        if let Some(&snapshot_id) = map.get(name) {
688            return Ok(snapshot_id);
689        }
690
691        #[cfg(madsim)]
692        return Err(crate::error::ErrorCode::BindError(
693            "iceberg source time travel can't be used in the madsim mode".to_string(),
694        )
695        .into());
696
697        #[cfg(not(madsim))]
698        {
699            let ConnectorProperties::Iceberg(prop) =
700                ConnectorProperties::extract(catalog.with_properties.clone(), false)?
701            else {
702                return Err(crate::error::ErrorCode::InternalError(
703                    "Iceberg source must have Iceberg connector properties".to_owned(),
704                )
705                .into());
706            };
707
708            let snapshot_id = tokio::task::block_in_place(|| {
709                crate::utils::FRONTEND_RUNTIME.block_on(async {
710                    prop.load_table()
711                        .await
712                        .map(|table| table.metadata().current_snapshot_id())
713                })
714            })?;
715            map.insert(name.to_owned(), snapshot_id);
716            Ok(snapshot_id)
717        }
718    }
719
720    fn get_iceberg_source_by_table_catalog(
721        &self,
722        table_catalog: &TableCatalog,
723    ) -> Option<SourceCatalog> {
724        let catalog_reader = self.ctx.session_ctx().env().catalog_reader().read_guard();
725
726        let iceberg_source_name = table_catalog.iceberg_source_name()?;
727        let schema = catalog_reader
728            .get_schema_by_id(table_catalog.database_id, table_catalog.schema_id)
729            .ok()?;
730        let source_catalog = schema.get_source_by_name(&iceberg_source_name)?;
731        Some(source_catalog.deref().clone())
732    }
733}