1use 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 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(), )
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 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 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 ExprImpl::Literal(Literal::new(None, field.data_type.clone()).into())
247 }
248 })
249 .collect_vec();
250
251 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 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 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 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 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 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 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 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}