1use anyhow::Context;
16use pretty_xmlish::{Pretty, XmlNode};
17use risingwave_common::catalog::{Field, Schema};
18use risingwave_common::types::DataType;
19use risingwave_common::util::sort_util::OrderType;
20use risingwave_connector::sink::catalog::desc::SinkDesc;
21use risingwave_pb::stream_plan::stream_node::{NodeBody, PbStreamKind};
22use risingwave_pb::stream_plan::{
23 DispatcherType, IcebergWithPkIndexWriterNode, MergeNode, PbStreamNode,
24};
25
26use super::stream::prelude::*;
27use crate::TableCatalog;
28use crate::optimizer::plan_node::expr_visitable::ExprVisitable;
29use crate::optimizer::plan_node::utils::{Distill, TableCatalogBuilder, childless_record};
30use crate::optimizer::plan_node::{
31 ExprRewritable, PlanBase, PlanTreeNodeUnary, Stream, StreamNode, StreamPlanRef as PlanRef,
32};
33use crate::optimizer::property::{
34 Distribution, FunctionalDependencySet, MonotonicityMap, WatermarkColumns,
35};
36use crate::scheduler::SchedulerResult;
37use crate::stream_fragmenter::BuildFragmentGraphState;
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub struct StreamIcebergWithPkIndexWriter {
46 pub base: PlanBase<Stream>,
47 pub input: PlanRef,
48 pub sink_desc: SinkDesc,
49 pub pk_index_table: TableCatalog,
50}
51
52impl StreamIcebergWithPkIndexWriter {
53 pub fn from_stream_sink(sink: &super::StreamSink) -> Result<Self> {
54 let output_schema = output_schema();
55 let fd_set = FunctionalDependencySet::new(output_schema.len());
56 let dist = match sink.distribution() {
57 Distribution::Single => Distribution::Single,
58 _ => Distribution::SomeShard,
59 };
60 let base = PlanBase::new_stream(
61 sink.ctx(),
62 output_schema,
63 sink.stream_key().map(|v| v.to_vec()),
64 fd_set,
65 dist,
66 StreamKind::AppendOnly,
67 sink.emit_on_window_close(),
68 WatermarkColumns::new(),
69 MonotonicityMap::new(),
70 );
71 let pk_index_table = build_iceberg_pk_state_table(sink.sink_desc())?;
72 Ok(Self {
73 base,
74 input: sink.input(),
75 sink_desc: sink.sink_desc().clone(),
76 pk_index_table,
77 })
78 }
79}
80
81fn output_schema() -> Schema {
82 Schema::new(vec![
83 Field::with_name(DataType::Varchar, "file_path"),
84 Field::with_name(DataType::Int64, "position"),
85 ])
86}
87
88fn resolver_output_schema(sink_desc: &SinkDesc) -> Result<Schema> {
90 let downstream_pk = sink_desc
91 .downstream_pk
92 .as_deref()
93 .context("Missing downstream PK in Iceberg sink desc")?;
94 let mut fields: Vec<Field> = downstream_pk
95 .iter()
96 .map(|&idx| Field::from(&sink_desc.columns[idx].column_desc))
97 .collect();
98 fields.push(Field::with_name(DataType::Varchar, "file_path"));
99 fields.push(Field::with_name(DataType::Int64, "position"));
100 Ok(Schema::new(fields))
101}
102
103fn build_iceberg_pk_state_table(sink_desc: &SinkDesc) -> Result<TableCatalog> {
104 let mut builder = TableCatalogBuilder::default();
105
106 let downstream_pk = sink_desc
107 .downstream_pk
108 .as_deref()
109 .context("Missing downstream PK in Iceberg sink desc")?;
110 for &idx in downstream_pk {
111 builder.add_column(&Field::from(&sink_desc.columns[idx].column_desc));
112 }
113 builder.add_column(&Field::with_name(DataType::Varchar, "file_path"));
114 builder.add_column(&Field::with_name(DataType::Int64, "position"));
115
116 for idx in 0..downstream_pk.len() {
117 builder.add_order_column(idx, OrderType::ascending());
118 }
119
120 let res = builder.build((0..downstream_pk.len()).collect(), downstream_pk.len());
121 Ok(res)
122}
123
124impl Distill for StreamIcebergWithPkIndexWriter {
125 fn distill<'a>(&self) -> XmlNode<'a> {
126 let column_names = self
127 .sink_desc
128 .columns
129 .iter()
130 .map(|col| col.name_with_hidden().to_string())
131 .map(Pretty::from)
132 .collect();
133 let column_names = Pretty::Array(column_names);
134 let mut vec = Vec::with_capacity(2);
135 vec.push(("columns", column_names));
136 if let Some(pk) = &self.sink_desc.downstream_pk {
137 let column_names = pk
138 .iter()
139 .map(|&idx| self.sink_desc.columns[idx].name_with_hidden().to_string())
140 .map(Pretty::from)
141 .collect();
142 let column_names = Pretty::Array(column_names);
143 vec.push(("downstream_pk", column_names));
144 }
145
146 childless_record("StreamIcebergWithPkIndexWriter", vec)
147 }
148}
149
150impl PlanTreeNodeUnary<Stream> for StreamIcebergWithPkIndexWriter {
151 fn input(&self) -> PlanRef {
152 self.input.clone()
153 }
154
155 fn clone_with_input(&self, input: PlanRef) -> Self {
156 Self {
157 base: self.base.clone(),
158 input,
159 sink_desc: self.sink_desc.clone(),
160 pk_index_table: self.pk_index_table.clone(),
161 }
162 }
163}
164
165impl_plan_tree_node_for_unary! { Stream, StreamIcebergWithPkIndexWriter }
166
167impl StreamNode for StreamIcebergWithPkIndexWriter {
168 fn to_stream_prost_body(&self, _state: &mut BuildFragmentGraphState) -> NodeBody {
169 unreachable!(
170 "iceberg pk-index writer cannot be converted into a prost body -- call \
171 `adhoc_to_stream_prost` instead, since it declares a dormant second input."
172 )
173 }
174}
175
176impl StreamIcebergWithPkIndexWriter {
177 pub fn adhoc_to_stream_prost(
180 &self,
181 state: &mut BuildFragmentGraphState,
182 ) -> SchedulerResult<PbStreamNode> {
183 let pk_index_table = self
184 .pk_index_table
185 .clone()
186 .with_id(state.gen_table_id_wrapped());
187 let resolver_fields = resolver_output_schema(&self.sink_desc)
188 .context("build compaction resolver output schema")?
189 .to_prost();
190
191 let left_input = self.input.to_stream_prost(state)?;
192 let right_dispatcher = match self.distribution() {
193 Distribution::Single => DispatcherType::Simple,
194 _ => DispatcherType::Hash,
195 };
196 let right_input = PbStreamNode {
197 node_body: Some(NodeBody::Merge(Box::new(MergeNode {
198 upstream_fragment_id: 0.into(),
199 upstream_dispatcher_type: right_dispatcher.into(),
200 allow_no_initial_upstream: true,
201 ..Default::default()
202 }))),
203 identity: "IcebergCompactionResolverEdge".into(),
204 fields: resolver_fields,
205 stream_kind: PbStreamKind::AppendOnly as i32,
206 ..Default::default()
207 };
208
209 Ok(PbStreamNode {
210 node_body: Some(NodeBody::IcebergWithPkIndexWriter(Box::new(
211 IcebergWithPkIndexWriterNode {
212 sink_desc: Some(self.sink_desc.to_proto()),
213 pk_index_table: Some(pk_index_table.to_internal_table_prost()),
214 },
215 ))),
216 input: vec![left_input, right_input],
217 identity: self.distill_to_string(),
218 operator_id: self.id().to_stream_node_operator_id(),
219 stream_key: self
220 .stream_key()
221 .unwrap_or_default()
222 .iter()
223 .map(|x| *x as u32)
224 .collect(),
225 fields: self.schema().to_prost(),
226 stream_kind: self.stream_kind().to_protobuf() as i32,
227 })
228 }
229}
230
231impl ExprRewritable<Stream> for StreamIcebergWithPkIndexWriter {}
232
233impl ExprVisitable for StreamIcebergWithPkIndexWriter {}
234
235#[cfg(test)]
236mod tests {
237 use std::collections::BTreeMap;
238
239 use risingwave_common::catalog::{
240 ColumnCatalog, ColumnDesc, ColumnId, CreateType, DEFAULT_SUPER_USER_ID, StreamJobStatus,
241 };
242 use risingwave_common::util::sort_util::ColumnOrder;
243 use risingwave_connector::sink::catalog::{SinkId, SinkType};
244
245 use super::*;
246
247 fn test_sink_desc() -> SinkDesc {
248 SinkDesc {
249 id: SinkId::placeholder(),
250 name: "s".to_owned(),
251 definition: "".to_owned(),
252 columns: vec![
253 ColumnCatalog::visible(ColumnDesc::named("id", ColumnId::new(0), DataType::Int32)),
254 ColumnCatalog::visible(ColumnDesc::named("v1", ColumnId::new(1), DataType::Int32)),
255 ColumnCatalog::hidden(ColumnDesc::named(
256 "_row_id",
257 ColumnId::new(2),
258 DataType::Serial,
259 )),
260 ],
261 plan_pk: vec![ColumnOrder::new(2, OrderType::ascending())],
262 downstream_pk: Some(vec![1]),
263 distribution_key: vec![1],
264 properties: BTreeMap::new(),
265 secret_refs: BTreeMap::new(),
266 sink_type: SinkType::Upsert,
267 ignore_delete: false,
268 format_desc: None,
269 db_name: "dev".to_owned(),
270 sink_from_name: "t".to_owned(),
271 target_table: None,
272 extra_partition_col_idx: None,
273 create_type: CreateType::Foreground,
274 is_exactly_once: None,
275 auto_refresh_schema_from_table: None,
276 }
277 }
278
279 #[test]
280 fn test_build_iceberg_pk_state_table_uses_downstream_pk_columns() {
281 let table = build_iceberg_pk_state_table(&test_sink_desc()).unwrap();
282
283 assert_eq!(table.columns()[0].name(), "v1");
284 assert_eq!(table.columns()[1].name(), "file_path");
285 assert_eq!(table.columns()[2].name(), "position");
286 assert_eq!(table.pk().len(), 1);
287 assert_eq!(table.pk()[0].column_index, 0);
288 assert_eq!(table.distribution_key(), &[0]);
289 assert_eq!(table.read_prefix_len_hint, 1);
290 assert_eq!(table.owner, DEFAULT_SUPER_USER_ID);
291 assert_eq!(table.stream_job_status, StreamJobStatus::Creating);
292 }
293
294 #[test]
295 fn test_build_iceberg_pk_state_table_with_multi_column_pk() {
296 let mut desc = test_sink_desc();
299 desc.columns.push(ColumnCatalog::visible(ColumnDesc::named(
300 "order_id",
301 ColumnId::new(3),
302 DataType::Int64,
303 )));
304 desc.columns.push(ColumnCatalog::visible(ColumnDesc::named(
305 "shard_id",
306 ColumnId::new(4),
307 DataType::Int64,
308 )));
309 desc.downstream_pk = Some(vec![1, 3, 4]); let table = build_iceberg_pk_state_table(&desc).unwrap();
312
313 let names: Vec<_> = table
314 .columns()
315 .iter()
316 .map(|c| c.name().to_owned())
317 .collect();
318 assert_eq!(
319 names,
320 vec!["v1", "order_id", "shard_id", "file_path", "position"]
321 );
322 assert_eq!(table.pk().len(), 3);
323 assert_eq!(table.distribution_key(), &[0, 1, 2]);
324 assert_eq!(table.read_prefix_len_hint, 3);
325 }
326
327 #[test]
328 fn test_build_iceberg_pk_state_table_with_visible_extra_only() {
329 let mut desc = test_sink_desc();
331 desc.columns.push(ColumnCatalog::visible(ColumnDesc::named(
332 "order_id",
333 ColumnId::new(3),
334 DataType::Int64,
335 )));
336 desc.downstream_pk = Some(vec![1, 3]); let table = build_iceberg_pk_state_table(&desc).unwrap();
339
340 let names: Vec<_> = table
341 .columns()
342 .iter()
343 .map(|c| c.name().to_owned())
344 .collect();
345 assert_eq!(names, vec!["v1", "order_id", "file_path", "position"]);
346 assert_eq!(table.pk().len(), 2);
347 assert_eq!(table.distribution_key(), &[0, 1]);
348 assert_eq!(table.read_prefix_len_hint, 2);
349 }
350}