risingwave_frontend/optimizer/plan_node/
logical_iceberg_intermediate_scan.rs1use std::collections::HashMap;
16
17use educe::Educe;
18use iceberg::expr::Predicate;
19use itertools::Itertools;
20use pretty_xmlish::{Pretty, XmlNode};
21use risingwave_common::catalog::{ColumnCatalog, Field};
22use risingwave_common::types::DataType;
23use risingwave_connector::source::iceberg::IcebergTimeTravelInfo;
24
25use super::generic::GenericPlanRef;
26use super::utils::{Distill, childless_record};
27use super::{
28 ColPrunable, ExprRewritable, Logical, LogicalPlanRef as PlanRef, PlanBase, PredicatePushdown,
29 ToBatch, ToStream, generic,
30};
31use crate::catalog::source_catalog::SourceCatalog;
32use crate::error::Result;
33use crate::optimizer::plan_node::expr_visitable::ExprVisitable;
34use crate::optimizer::plan_node::utils::column_names_pretty;
35use crate::optimizer::plan_node::{
36 ColumnPruningContext, LogicalFilter, LogicalProject, LogicalSource, PredicatePushdownContext,
37 RewriteStreamContext, ToStreamContext,
38};
39use crate::utils::{
40 ColIndexMapping, Condition, ExtractIcebergPredicateResult, extract_iceberg_predicate,
41};
42
43#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct HummockRewriteInfo {
48 pub origin_condition: Condition,
51 pub output_column_mapping: ColIndexMapping,
53}
54
55impl HummockRewriteInfo {
56 pub fn new(source_to_table_mapping: ColIndexMapping) -> Self {
59 Self {
60 origin_condition: Condition::true_cond(),
61 output_column_mapping: source_to_table_mapping,
62 }
63 }
64
65 pub fn add_predicate(&self, extracted_condition: Condition) -> Self {
67 let mut mapping = self.output_column_mapping.clone();
68 let remapped = extracted_condition.rewrite_expr(&mut mapping);
69 Self {
70 origin_condition: self.origin_condition.clone().and(remapped),
71 output_column_mapping: mapping,
72 }
73 }
74
75 pub fn prune_columns(&self, required_cols: &[usize]) -> Self {
78 let map = required_cols
79 .iter()
80 .map(|&idx| Some(self.output_column_mapping.map(idx)))
81 .collect();
82 Self {
83 origin_condition: self.origin_condition.clone(),
84 output_column_mapping: ColIndexMapping::new(
85 map,
86 self.output_column_mapping.target_size(),
87 ),
88 }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Educe)]
105#[educe(Hash)]
106pub struct LogicalIcebergIntermediateScan {
107 pub base: PlanBase<Logical>,
108 pub core: generic::Source,
109 #[educe(Hash(ignore))]
110 pub iceberg_predicate: Predicate,
111 pub time_travel_info: IcebergTimeTravelInfo,
112 #[educe(Hash(ignore))]
118 pub table_column_type_mapping: HashMap<String, DataType>,
119 #[educe(Hash(ignore))]
121 pub hummock_rewrite: HummockRewriteInfo,
122}
123
124impl Eq for LogicalIcebergIntermediateScan {}
125
126impl LogicalIcebergIntermediateScan {
127 pub fn new(
128 logical_source: &LogicalSource,
129 time_travel_info: IcebergTimeTravelInfo,
130 table_column_type_mapping: HashMap<String, DataType>,
131 source_to_table_mapping: ColIndexMapping,
136 ) -> Self {
137 assert!(logical_source.core.is_iceberg_connector());
138
139 let mut core = logical_source.core.clone();
140 for col in &mut core.column_catalog {
143 if let Some(target_type) = table_column_type_mapping.get(col.name()) {
144 col.column_desc.data_type = target_type.clone();
145 }
146 }
147 let hummock_rewrite = HummockRewriteInfo::new(source_to_table_mapping);
148 let base = PlanBase::new_logical_with_core(&core);
149 assert!(logical_source.output_exprs.is_none());
150 LogicalIcebergIntermediateScan {
151 base,
152 core,
153 iceberg_predicate: Predicate::AlwaysTrue,
154 time_travel_info,
155 table_column_type_mapping,
156 hummock_rewrite,
157 }
158 }
159
160 pub fn source_catalog(&self) -> Option<&SourceCatalog> {
161 self.core.catalog.as_deref()
162 }
163
164 fn iceberg_side_fields(&self) -> Vec<Field> {
168 let catalog = self
169 .core
170 .catalog
171 .as_ref()
172 .expect("iceberg intermediate scan must have a source catalog");
173 let by_name: HashMap<&str, &ColumnCatalog> =
174 catalog.columns.iter().map(|c| (c.name(), c)).collect();
175 self.core
176 .column_catalog
177 .iter()
178 .map(|col| {
179 let source_col = by_name
180 .get(col.name())
181 .expect("output column must exist in the source catalog");
182 Field::from(&source_col.column_desc)
183 })
184 .collect()
185 }
186
187 pub fn output_columns(&self) -> impl ExactSizeIterator<Item = &str> {
188 self.core.column_catalog.iter().map(|c| c.name.as_str())
189 }
190
191 pub fn add_predicate(
192 &self,
193 iceberg_predicate: Predicate,
194 extracted_condition: Condition,
195 ) -> Self {
196 LogicalIcebergIntermediateScan {
197 iceberg_predicate: self.iceberg_predicate.clone().and(iceberg_predicate),
198 hummock_rewrite: self.hummock_rewrite.add_predicate(extracted_condition),
199 ..self.clone()
200 }
201 }
202
203 pub fn has_type_mapping(&self) -> bool {
205 !self.table_column_type_mapping.is_empty()
206 }
207
208 pub fn clone_with_required_cols(&self, required_cols: &[usize]) -> Self {
209 assert!(!required_cols.is_empty());
210
211 let mut core = self.core.clone();
212 core.column_catalog = required_cols
213 .iter()
214 .map(|idx| core.column_catalog[*idx].clone())
215 .collect();
216 core.row_id_index = required_cols
217 .iter()
218 .position(|idx| Some(*idx) == self.core.row_id_index);
219
220 let base = PlanBase::new_logical_with_core(&core);
221
222 LogicalIcebergIntermediateScan {
223 base,
224 core,
225 iceberg_predicate: self.iceberg_predicate.clone(),
226 time_travel_info: self.time_travel_info.clone(),
227 table_column_type_mapping: self.table_column_type_mapping.clone(),
228 hummock_rewrite: self.hummock_rewrite.prune_columns(required_cols),
229 }
230 }
231}
232
233impl_plan_tree_node_for_leaf! { Logical, LogicalIcebergIntermediateScan }
234
235impl Distill for LogicalIcebergIntermediateScan {
236 fn distill<'a>(&self) -> XmlNode<'a> {
237 let verbose = self.base.ctx().is_explain_verbose();
238 let mut fields = Vec::with_capacity(if verbose { 4 } else { 2 });
239
240 if let Some(catalog) = self.source_catalog() {
241 fields.push(("source", Pretty::from(catalog.name.clone())));
242 } else {
243 fields.push(("source", Pretty::from("unknown")));
244 }
245 fields.push(("columns", column_names_pretty(self.schema())));
246
247 if verbose {
248 fields.push(("predicate", Pretty::debug(&self.iceberg_predicate)));
249 fields.push((
250 "output_column",
251 Pretty::debug(&self.output_columns().collect_vec()),
252 ));
253 fields.push(("time_travel_info", Pretty::debug(&self.time_travel_info)));
254 }
255
256 childless_record("LogicalIcebergIntermediateScan", fields)
257 }
258}
259
260impl ColPrunable for LogicalIcebergIntermediateScan {
261 fn prune_col(&self, required_cols: &[usize], _ctx: &mut ColumnPruningContext) -> PlanRef {
262 if required_cols.is_empty() {
263 LogicalProject::new(self.clone_with_required_cols(&[0]).into(), vec![]).into()
265 } else {
266 self.clone_with_required_cols(required_cols).into()
267 }
268 }
269}
270
271impl ExprRewritable<Logical> for LogicalIcebergIntermediateScan {}
272
273impl ExprVisitable for LogicalIcebergIntermediateScan {}
274
275impl PredicatePushdown for LogicalIcebergIntermediateScan {
276 fn predicate_pushdown(
277 &self,
278 predicate: Condition,
279 _ctx: &mut PredicatePushdownContext,
280 ) -> PlanRef {
281 let ExtractIcebergPredicateResult {
282 iceberg_predicate,
283 extracted_condition,
284 remaining_condition,
285 } = extract_iceberg_predicate(predicate, &self.iceberg_side_fields());
286 let plan = self
287 .add_predicate(iceberg_predicate, extracted_condition)
288 .into();
289 if remaining_condition.always_true() {
290 plan
291 } else {
292 LogicalFilter::create(plan, remaining_condition)
293 }
294 }
295}
296
297impl ToBatch for LogicalIcebergIntermediateScan {
298 fn to_batch(&self) -> Result<crate::optimizer::plan_node::BatchPlanRef> {
299 Err(crate::error::ErrorCode::InternalError(
302 "LogicalIcebergIntermediateScan should be converted to LogicalIcebergScan before to_batch".to_owned()
303 )
304 .into())
305 }
306}
307
308impl ToStream for LogicalIcebergIntermediateScan {
309 fn to_stream(
310 &self,
311 _ctx: &mut ToStreamContext,
312 ) -> Result<crate::optimizer::plan_node::StreamPlanRef> {
313 unreachable!("LogicalIcebergIntermediateScan is only for batch queries")
314 }
315
316 fn logical_rewrite_for_stream(
317 &self,
318 _ctx: &mut RewriteStreamContext,
319 ) -> Result<(PlanRef, ColIndexMapping)> {
320 unreachable!("LogicalIcebergIntermediateScan is only for batch queries")
321 }
322}