1use std::fmt;
2use std::sync::Arc;
3
4use arrow::record_batch::RecordBatch;
5use datafusion::common::{DataFusionError, Result};
6use datum::{Source, StreamError, StreamResult};
7
8use crate::connect::BatchEnvelope;
9use crate::{ChangelogBatch, SqlEventPayload, stream_error};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum SqlSourcePosition {
17 #[cfg(feature = "mq")]
18 Kafka(crate::connect::mq::KafkaSourcePosition),
20 #[cfg(feature = "cdc")]
21 Cdc(crate::connect::cdc::CdcSourcePosition),
23 Custom {
25 connector: Arc<str>,
27 position: Arc<str>,
29 },
30}
31
32impl SqlSourcePosition {
33 #[must_use]
35 pub fn custom(connector: impl Into<Arc<str>>, position: impl Into<Arc<str>>) -> Self {
36 Self::Custom {
37 connector: connector.into(),
38 position: position.into(),
39 }
40 }
41}
42
43type CommitCallback = dyn Fn() -> StreamResult<()> + Send + Sync;
44
45#[derive(Clone)]
49pub struct SourceCommit {
50 description: Arc<str>,
51 commit: Arc<CommitCallback>,
52}
53
54impl SourceCommit {
55 #[must_use]
57 pub fn from_fn<F>(description: impl Into<Arc<str>>, commit: F) -> Self
58 where
59 F: Fn() -> StreamResult<()> + Send + Sync + 'static,
60 {
61 Self {
62 description: description.into(),
63 commit: Arc::new(commit),
64 }
65 }
66
67 #[must_use]
69 pub fn noop() -> Self {
70 Self::from_fn("no source position", || Ok(()))
71 }
72
73 pub fn commit(&self) -> StreamResult<()> {
75 (self.commit)()
76 }
77
78 #[must_use]
80 pub fn description(&self) -> &str {
81 &self.description
82 }
83}
84
85impl fmt::Debug for SourceCommit {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 f.debug_struct("SourceCommit")
88 .field("description", &self.description)
89 .finish_non_exhaustive()
90 }
91}
92
93#[derive(Clone)]
95pub struct CommittableRecordBatch {
96 batch: RecordBatch,
97 envelope: BatchEnvelope<Option<SqlSourcePosition>>,
98 commit: SourceCommit,
99}
100
101impl CommittableRecordBatch {
102 #[must_use]
104 pub fn new(
105 batch: RecordBatch,
106 source_position: Option<SqlSourcePosition>,
107 schema_revision: u64,
108 commit: SourceCommit,
109 ) -> Self {
110 Self {
111 batch,
112 envelope: BatchEnvelope::new(source_position, schema_revision),
113 commit,
114 }
115 }
116
117 #[must_use]
119 pub fn plain(batch: RecordBatch) -> Self {
120 Self::new(batch, None, 0, SourceCommit::noop())
121 }
122
123 #[must_use]
125 pub fn batch(&self) -> &RecordBatch {
126 &self.batch
127 }
128
129 #[must_use]
131 pub fn envelope(&self) -> &BatchEnvelope<Option<SqlSourcePosition>> {
132 &self.envelope
133 }
134
135 #[must_use]
137 pub fn source_position(&self) -> Option<&SqlSourcePosition> {
138 self.envelope.source_position().as_ref()
139 }
140
141 #[must_use]
143 pub fn schema_revision(&self) -> u64 {
144 self.envelope.schema_revision()
145 }
146
147 #[must_use]
149 pub fn commit_handle(&self) -> &SourceCommit {
150 &self.commit
151 }
152
153 pub fn commit(&self) -> StreamResult<()> {
155 self.commit.commit()
156 }
157
158 pub(crate) fn try_map_batch<F>(self, f: F) -> Result<Self>
159 where
160 F: FnOnce(&RecordBatch) -> Result<RecordBatch>,
161 {
162 let batch = f(&self.batch)?;
163 Ok(Self {
164 batch,
165 envelope: self.envelope,
166 commit: self.commit,
167 })
168 }
169}
170
171impl fmt::Debug for CommittableRecordBatch {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 f.debug_struct("CommittableRecordBatch")
174 .field("batch", &self.batch)
175 .field("envelope", &self.envelope)
176 .field("commit", &self.commit)
177 .finish()
178 }
179}
180
181impl SqlEventPayload for CommittableRecordBatch {
182 fn event_time_batch(&self) -> &RecordBatch {
183 &self.batch
184 }
185
186 fn event_time_partition(&self, row: usize) -> Option<i64> {
187 match self.source_position()? {
188 #[cfg(feature = "mq")]
189 SqlSourcePosition::Kafka(position) => position.partition_for_row(row).map(i64::from),
190 #[cfg(feature = "cdc")]
191 SqlSourcePosition::Cdc(_) | SqlSourcePosition::Custom { .. } => None,
192 #[cfg(not(feature = "cdc"))]
193 SqlSourcePosition::Custom { .. } => None,
194 }
195 }
196
197 fn event_time_active_partitions(&self) -> Option<Vec<i64>> {
198 match self.source_position()? {
199 #[cfg(feature = "mq")]
200 SqlSourcePosition::Kafka(position) => {
201 if !position.has_event_time_partition_mapping() {
202 return None;
203 }
204 let partitions = position
205 .active_partitions()
206 .iter()
207 .map(|partition| i64::from(partition.partition))
208 .collect::<Vec<_>>();
209 (!partitions.is_empty()).then_some(partitions)
210 }
211 #[cfg(feature = "cdc")]
212 SqlSourcePosition::Cdc(_) | SqlSourcePosition::Custom { .. } => None,
213 #[cfg(not(feature = "cdc"))]
214 SqlSourcePosition::Custom { .. } => None,
215 }
216 }
217}
218
219type AppendWriter = dyn Fn(RecordBatch) -> StreamResult<()> + Send + Sync;
220type ChangelogWriter = dyn Fn(ChangelogBatch) -> StreamResult<()> + Send + Sync;
221
222#[derive(Clone)]
223pub(crate) struct RegisteredSqlSink {
224 append_writer: Arc<AppendWriter>,
225 changelog_writer: Option<Arc<ChangelogWriter>>,
226}
227
228impl RegisteredSqlSink {
229 pub(crate) fn append_only<F>(writer: F) -> Self
230 where
231 F: Fn(RecordBatch) -> StreamResult<()> + Send + Sync + 'static,
232 {
233 Self {
234 append_writer: Arc::new(writer),
235 changelog_writer: None,
236 }
237 }
238
239 pub(crate) fn changelog<F, G>(append_writer: F, changelog_writer: G) -> Self
240 where
241 F: Fn(RecordBatch) -> StreamResult<()> + Send + Sync + 'static,
242 G: Fn(ChangelogBatch) -> StreamResult<()> + Send + Sync + 'static,
243 {
244 Self {
245 append_writer: Arc::new(append_writer),
246 changelog_writer: Some(Arc::new(changelog_writer)),
247 }
248 }
249
250 #[cfg(feature = "mq")]
251 pub(crate) fn kafka_json(
252 settings: datum_mq::KafkaProducerSettings,
253 topic: String,
254 format: crate::connect::mq::JsonRowFormat,
255 ) -> Self {
256 Self::append_only(move |batch| {
257 let payloads = format.encode_record_batch(&batch).map_err(stream_error)?;
258 if payloads.is_empty() {
259 return Ok(());
260 }
261
262 let records = payloads.into_iter().map(|payload| {
263 datum_mq::ProducerRecord::new(topic.clone(), bytes::Bytes::from(payload))
264 });
265 let control = Source::from_iter(records)
266 .run_with(datum_mq::KafkaSink::plain(settings.clone()))
267 .map_err(stream_error)?;
268 control.drain_and_shutdown().map_err(stream_error)
269 })
270 }
271
272 #[must_use]
273 pub(crate) fn accepts_changelog(&self) -> bool {
274 self.changelog_writer.is_some()
275 }
276
277 pub(crate) fn write_append(&self, batch: RecordBatch) -> StreamResult<()> {
278 (self.append_writer)(batch)
279 }
280
281 pub(crate) fn write_changelog(&self, changes: ChangelogBatch) -> StreamResult<()> {
282 let Some(writer) = &self.changelog_writer else {
283 return Err(StreamError::Failed(
284 "datum-sql sink is not changelog-aware".to_owned(),
285 ));
286 };
287 writer(changes)
288 }
289}
290
291impl fmt::Debug for RegisteredSqlSink {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 f.debug_struct("RegisteredSqlSink")
294 .field("accepts_changelog", &self.accepts_changelog())
295 .finish_non_exhaustive()
296 }
297}
298
299pub(crate) enum AppendSinkSource {
300 Plain(Source<RecordBatch>),
301 Committable(Source<CommittableRecordBatch>),
302}
303
304impl AppendSinkSource {
305 pub(crate) fn into_committable(self) -> Source<CommittableRecordBatch> {
306 match self {
307 Self::Plain(source) => source.map(CommittableRecordBatch::plain),
308 Self::Committable(source) => source,
309 }
310 }
311}
312
313pub(crate) fn sink_registry_error(error: impl fmt::Display) -> DataFusionError {
314 DataFusionError::External(Box::new(SinkRegistryError(error.to_string())))
315}
316
317#[derive(Debug)]
318struct SinkRegistryError(String);
319
320impl fmt::Display for SinkRegistryError {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 f.write_str(&self.0)
323 }
324}
325
326impl std::error::Error for SinkRegistryError {}