risingwave_meta/manager/iceberg_pk_index_sink/
mod.rs1mod committed_epoch;
24mod coordinator;
25mod manager;
26
27use std::collections::{BTreeMap, HashMap};
28
29use anyhow::anyhow;
30use iceberg::spec::SerializedDataFile;
31pub use manager::IcebergPkIndexSinkManager;
32pub(crate) use manager::IcebergPkIndexSinkRegistration;
33use risingwave_common::secret::LocalSecretManager;
34use risingwave_connector::sink::catalog::SinkId;
35use risingwave_connector::sink::iceberg::{ENABLE_PK_INDEX, IcebergConfig};
36use risingwave_connector::source::UPSTREAM_SOURCE_KEY;
37use risingwave_pb::catalog::PbSink;
38use risingwave_pb::stream_service::barrier_complete_response::IcebergPkIndexSinkMetadata as PbIcebergPkIndexSinkMetadata;
39
40#[derive(educe::Educe)]
41#[educe(Debug)]
42pub(crate) struct CompactionOverwrite {
43 pub sink_id: SinkId,
44 pub epoch: u64,
45 pub schema_id: i32,
46 pub partition_spec_id: i32,
47 #[educe(Debug(ignore))]
48 pub output_files: Vec<SerializedDataFile>,
49 pub input_file_paths: Vec<String>,
50 pub read_snapshot_id: i64,
51}
52
53#[derive(Debug)]
57pub(crate) struct IcebergPkIndexPreCommitMetadata {
58 pub sink_id: SinkId,
59 pub prev_epoch: u64,
60 pub reports: Vec<PbIcebergPkIndexSinkMetadata>,
61 pub compaction: Option<CompactionOverwrite>,
62}
63
64impl From<PbIcebergPkIndexSinkMetadata> for IcebergPkIndexPreCommitMetadata {
65 fn from(report: PbIcebergPkIndexSinkMetadata) -> Self {
66 Self {
67 sink_id: report.sink_id,
68 prev_epoch: report.prev_epoch,
69 reports: vec![report],
70 compaction: None,
71 }
72 }
73}
74
75impl From<CompactionOverwrite> for IcebergPkIndexPreCommitMetadata {
76 fn from(overwrite: CompactionOverwrite) -> Self {
77 Self {
78 sink_id: overwrite.sink_id,
79 prev_epoch: overwrite.epoch,
80 reports: vec![],
81 compaction: Some(overwrite),
82 }
83 }
84}
85
86pub(crate) fn group_pre_commit_metadata(
87 metadata: Vec<IcebergPkIndexPreCommitMetadata>,
88) -> anyhow::Result<Vec<IcebergPkIndexPreCommitMetadata>> {
89 let mut grouped = HashMap::<SinkId, IcebergPkIndexPreCommitMetadata>::new();
90 for metadata in metadata {
91 let IcebergPkIndexPreCommitMetadata {
92 sink_id,
93 prev_epoch,
94 reports,
95 compaction,
96 } = metadata;
97 let input = grouped
98 .entry(sink_id)
99 .or_insert_with(|| IcebergPkIndexPreCommitMetadata {
100 sink_id,
101 prev_epoch,
102 reports: Vec::new(),
103 compaction: None,
104 });
105 if input.prev_epoch != prev_epoch {
106 anyhow::bail!(
107 "iceberg v3 sink {} pre-commit metadata disagrees on prev_epoch: {} vs {}",
108 sink_id,
109 input.prev_epoch,
110 prev_epoch
111 );
112 }
113 input.reports.extend(reports);
114 if let Some(overwrite) = compaction
115 && input.compaction.replace(overwrite).is_some()
116 {
117 anyhow::bail!(
118 "iceberg v3 sink {} has multiple compaction overwrites in one barrier",
119 sink_id
120 );
121 }
122 }
123 Ok(grouped.into_values().collect())
124}
125
126pub fn is_iceberg_pk_index_sink(properties: &BTreeMap<String, String>) -> bool {
129 let connector_match = properties
130 .get(UPSTREAM_SOURCE_KEY)
131 .map(|v| v.eq_ignore_ascii_case("iceberg"))
132 .unwrap_or(false);
133 let pk_index_enabled = properties
134 .get(ENABLE_PK_INDEX)
135 .map(|v| v.eq_ignore_ascii_case("true"))
136 .unwrap_or(false);
137 connector_match && pk_index_enabled
138}
139
140pub fn build_iceberg_config(pb_sink: &PbSink) -> anyhow::Result<IcebergConfig> {
144 let properties: BTreeMap<String, String> = pb_sink.properties.clone().into_iter().collect();
145 let secret_refs: BTreeMap<_, _> = pb_sink.secret_refs.clone().into_iter().collect();
146 let with_secrets = LocalSecretManager::global()
147 .fill_secrets(properties, secret_refs)
148 .map_err(|e| anyhow!(e).context("fill secrets for iceberg"))?;
149 IcebergConfig::from_btreemap(with_secrets)
150 .map_err(|e| anyhow!(e).context("parse iceberg config"))
151}
152
153#[cfg(test)]
154mod tests {
155 use risingwave_pb::stream_service::PbIcebergPkIndexSinkRole;
156
157 use super::*;
158
159 fn report(
160 sink_id: u32,
161 prev_epoch: u64,
162 role: PbIcebergPkIndexSinkRole,
163 ) -> PbIcebergPkIndexSinkMetadata {
164 PbIcebergPkIndexSinkMetadata {
165 sink_id: SinkId::new(sink_id),
166 prev_epoch,
167 role: role as i32,
168 ..Default::default()
169 }
170 }
171
172 fn overwrite(sink_id: u32, epoch: u64) -> CompactionOverwrite {
173 CompactionOverwrite {
174 sink_id: SinkId::new(sink_id),
175 epoch,
176 schema_id: 3,
177 partition_spec_id: 4,
178 output_files: vec![],
179 input_file_paths: vec![],
180 read_snapshot_id: 1,
181 }
182 }
183
184 #[test]
185 fn group_pre_commit_metadata_combines_reports_and_compaction() {
186 let inputs = group_pre_commit_metadata(vec![
187 report(7, 60, PbIcebergPkIndexSinkRole::Writer).into(),
188 overwrite(7, 60).into(),
189 report(7, 60, PbIcebergPkIndexSinkRole::PositionDeleteMerger).into(),
190 ])
191 .unwrap();
192
193 assert_eq!(inputs.len(), 1);
194 let input = &inputs[0];
195 assert_eq!(input.sink_id, SinkId::new(7));
196 assert_eq!(input.prev_epoch, 60);
197 assert_eq!(input.reports.len(), 2);
198 let overwrite = input.compaction.as_ref().unwrap();
199 assert_eq!(overwrite.schema_id, 3);
200 assert_eq!(overwrite.partition_spec_id, 4);
201 }
202
203 #[test]
204 fn group_pre_commit_metadata_preserves_ordinary_only_sink() {
205 let inputs =
206 group_pre_commit_metadata(vec![report(8, 61, PbIcebergPkIndexSinkRole::Writer).into()])
207 .unwrap();
208
209 assert_eq!(inputs.len(), 1);
210 assert_eq!(inputs[0].sink_id, SinkId::new(8));
211 assert_eq!(inputs[0].prev_epoch, 61);
212 assert_eq!(inputs[0].reports.len(), 1);
213 assert!(inputs[0].compaction.is_none());
214 }
215
216 #[test]
217 fn group_pre_commit_metadata_rejects_duplicate_compaction() {
218 let error =
219 group_pre_commit_metadata(vec![overwrite(7, 60).into(), overwrite(7, 60).into()])
220 .unwrap_err();
221 assert!(error.to_string().contains("multiple compaction overwrites"));
222 }
223
224 #[test]
225 fn group_pre_commit_metadata_rejects_epoch_mismatch() {
226 let error = group_pre_commit_metadata(vec![
227 report(7, 59, PbIcebergPkIndexSinkRole::Writer).into(),
228 overwrite(7, 60).into(),
229 ])
230 .unwrap_err();
231 assert!(error.to_string().contains("disagrees on prev_epoch"));
232 }
233}