use std::sync::Arc;
use arrow::array::{
ArrayRef, BooleanBuilder, Float32Builder, Float64Builder, Int8Builder, Int16Builder,
Int32Builder, Int64Builder, LargeStringBuilder, StringBuilder, UInt8Builder, UInt16Builder,
UInt32Builder, UInt64Builder,
};
use arrow::datatypes::{DataType, Field, SchemaRef};
use arrow::record_batch::RecordBatch;
use datafusion::common::{DataFusionError, Result};
use datum::{NotUsed, Source};
use datum_cdc::{
CdcHandle, CdcOffset, ChangeEvent, ChangeOperation, ColumnValue, RelationMetadata, RowData,
};
use crate::{ChangeOp, ChangelogBatch};
use super::{BatchEnvelope, EnvelopedBatch};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CdcSourcePosition {
lsn: CdcOffset,
}
impl CdcSourcePosition {
#[must_use]
pub const fn new(lsn: CdcOffset) -> Self {
Self { lsn }
}
#[must_use]
pub const fn lsn(&self) -> &CdcOffset {
&self.lsn
}
#[must_use]
pub fn into_lsn(self) -> CdcOffset {
self.lsn
}
}
pub type CdcChangelogBatch = EnvelopedBatch<ChangelogBatch, CdcSourcePosition>;
#[derive(Debug, Clone)]
pub struct CdcTableAdapter {
schema: SchemaRef,
}
impl CdcTableAdapter {
#[must_use]
pub fn new(schema: SchemaRef) -> Self {
Self { schema }
}
#[must_use]
pub fn schema(&self) -> SchemaRef {
Arc::clone(&self.schema)
}
pub fn map_change_event(&self, event: ChangeEvent) -> Result<CdcChangelogBatch> {
let schema_revision = event.relation.revision;
let position = CdcSourcePosition::new(event.lsn.clone());
let (ops, rows) = match event.op {
ChangeOperation::Insert => {
let after = required_row(event.after.as_ref(), "insert after row")?;
(vec![ChangeOp::Insert], vec![after])
}
ChangeOperation::Delete => {
let before = required_row(event.before.as_ref(), "delete before row")?;
(vec![ChangeOp::Delete], vec![before])
}
ChangeOperation::Update => {
let after = required_row(event.after.as_ref(), "update after row")?;
if let Some(before) = event.before.as_ref() {
(
vec![ChangeOp::UpdateDelete, ChangeOp::UpdateInsert],
vec![before, after],
)
} else {
(vec![ChangeOp::Delete, ChangeOp::Insert], vec![after, after])
}
}
ChangeOperation::Truncate => {
return Err(DataFusionError::Plan(
"CDC truncate events cannot be represented as row-level ChangelogBatch values"
.into(),
));
}
};
let batch = rows_to_record_batch(Arc::clone(&self.schema), &event.relation, &rows)?;
let changes = ChangelogBatch::try_new(ops, batch)?;
Ok(CdcChangelogBatch::new(
changes,
BatchEnvelope::new(position, schema_revision),
))
}
}
#[must_use]
pub fn cdc_changelog_source(
source: Source<ChangeEvent, CdcHandle>,
schema: SchemaRef,
) -> Source<CdcChangelogBatch, CdcHandle> {
let adapter = CdcTableAdapter::new(schema);
source.try_map(move |event| adapter.map_change_event(event).map_err(crate::stream_error))
}
#[must_use]
pub fn cdc_changelog_batch_source_uncontrolled(
source: Source<ChangeEvent, CdcHandle>,
schema: SchemaRef,
) -> Source<ChangelogBatch, NotUsed> {
cdc_changelog_source(source, schema)
.map(|batch| batch.into_batch())
.map_materialized_value(|_| NotUsed)
}
fn required_row<'a>(row: Option<&'a RowData>, context: &str) -> Result<&'a RowData> {
row.ok_or_else(|| DataFusionError::Plan(format!("CDC event is missing {context}")))
}
fn rows_to_record_batch(
schema: SchemaRef,
relation: &RelationMetadata,
rows: &[&RowData],
) -> Result<RecordBatch> {
if rows.is_empty() {
return Ok(RecordBatch::new_empty(schema));
}
let mut builders = schema
.fields()
.iter()
.map(|field| CdcColumnBuilder::try_new(field, rows.len()))
.collect::<Result<Vec<_>>>()?;
for row in rows {
for (field_index, field) in schema.fields().iter().enumerate() {
let value = row.get(relation, field.name()).ok_or_else(|| {
DataFusionError::Plan(format!(
"CDC row for {}.{} does not contain column {:?}",
relation.schema,
relation.table,
field.name()
))
})?;
builders[field_index].append(field, value)?;
}
}
let columns = builders
.into_iter()
.map(CdcColumnBuilder::finish)
.collect::<Vec<_>>();
RecordBatch::try_new(schema, columns).map_err(DataFusionError::from)
}
enum CdcColumnBuilder {
Boolean(BooleanBuilder),
Int8(Int8Builder),
Int16(Int16Builder),
Int32(Int32Builder),
Int64(Int64Builder),
UInt8(UInt8Builder),
UInt16(UInt16Builder),
UInt32(UInt32Builder),
UInt64(UInt64Builder),
Float32(Float32Builder),
Float64(Float64Builder),
Utf8(StringBuilder),
LargeUtf8(LargeStringBuilder),
}
impl CdcColumnBuilder {
fn try_new(field: &Field, rows: usize) -> Result<Self> {
match field.data_type() {
DataType::Boolean => Ok(Self::Boolean(BooleanBuilder::with_capacity(rows))),
DataType::Int8 => Ok(Self::Int8(Int8Builder::with_capacity(rows))),
DataType::Int16 => Ok(Self::Int16(Int16Builder::with_capacity(rows))),
DataType::Int32 => Ok(Self::Int32(Int32Builder::with_capacity(rows))),
DataType::Int64 => Ok(Self::Int64(Int64Builder::with_capacity(rows))),
DataType::UInt8 => Ok(Self::UInt8(UInt8Builder::with_capacity(rows))),
DataType::UInt16 => Ok(Self::UInt16(UInt16Builder::with_capacity(rows))),
DataType::UInt32 => Ok(Self::UInt32(UInt32Builder::with_capacity(rows))),
DataType::UInt64 => Ok(Self::UInt64(UInt64Builder::with_capacity(rows))),
DataType::Float32 => Ok(Self::Float32(Float32Builder::with_capacity(rows))),
DataType::Float64 => Ok(Self::Float64(Float64Builder::with_capacity(rows))),
DataType::Utf8 => Ok(Self::Utf8(StringBuilder::with_capacity(rows, rows * 8))),
DataType::LargeUtf8 => Ok(Self::LargeUtf8(LargeStringBuilder::with_capacity(
rows,
rows * 8,
))),
other => Err(DataFusionError::Plan(format!(
"CDC SQL adapter does not support Arrow field {:?} with type {other:?}",
field.name()
))),
}
}
fn append(&mut self, field: &Field, value: &ColumnValue) -> Result<()> {
match value {
ColumnValue::Null => self.append_null(field),
ColumnValue::ToastUnchanged => Err(DataFusionError::Plan(format!(
"CDC column {:?} is TOAST-unchanged; include a full before/after value or omit it from the SQL schema",
field.name()
))),
ColumnValue::Binary(_) => Err(DataFusionError::Plan(format!(
"CDC column {:?} arrived in binary pgoutput format, but datum-sql CDC decoding expects text values",
field.name()
))),
ColumnValue::Text(text) => self.append_text(field, text),
}
}
fn append_null(&mut self, field: &Field) -> Result<()> {
if !field.is_nullable() {
return Err(DataFusionError::Plan(format!(
"CDC column {:?} is NULL but the Arrow field is non-nullable",
field.name()
)));
}
match self {
Self::Boolean(builder) => builder.append_null(),
Self::Int8(builder) => builder.append_null(),
Self::Int16(builder) => builder.append_null(),
Self::Int32(builder) => builder.append_null(),
Self::Int64(builder) => builder.append_null(),
Self::UInt8(builder) => builder.append_null(),
Self::UInt16(builder) => builder.append_null(),
Self::UInt32(builder) => builder.append_null(),
Self::UInt64(builder) => builder.append_null(),
Self::Float32(builder) => builder.append_null(),
Self::Float64(builder) => builder.append_null(),
Self::Utf8(builder) => builder.append_null(),
Self::LargeUtf8(builder) => builder.append_null(),
}
Ok(())
}
fn append_text(&mut self, field: &Field, text: &str) -> Result<()> {
match self {
Self::Boolean(builder) => builder.append_value(parse_text(field, text)?),
Self::Int8(builder) => builder.append_value(parse_text(field, text)?),
Self::Int16(builder) => builder.append_value(parse_text(field, text)?),
Self::Int32(builder) => builder.append_value(parse_text(field, text)?),
Self::Int64(builder) => builder.append_value(parse_text(field, text)?),
Self::UInt8(builder) => builder.append_value(parse_text(field, text)?),
Self::UInt16(builder) => builder.append_value(parse_text(field, text)?),
Self::UInt32(builder) => builder.append_value(parse_text(field, text)?),
Self::UInt64(builder) => builder.append_value(parse_text(field, text)?),
Self::Float32(builder) => builder.append_value(parse_text(field, text)?),
Self::Float64(builder) => builder.append_value(parse_text(field, text)?),
Self::Utf8(builder) => builder.append_value(text),
Self::LargeUtf8(builder) => builder.append_value(text),
}
Ok(())
}
fn finish(mut self) -> ArrayRef {
match &mut self {
Self::Boolean(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::Int8(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::Int16(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::Int32(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::Int64(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::UInt8(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::UInt16(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::UInt32(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::UInt64(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::Float32(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::Float64(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::Utf8(builder) => Arc::new(builder.finish()) as ArrayRef,
Self::LargeUtf8(builder) => Arc::new(builder.finish()) as ArrayRef,
}
}
}
fn parse_text<T>(field: &Field, text: &str) -> Result<T>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
text.parse::<T>().map_err(|error| {
DataFusionError::Plan(format!(
"CDC column {:?} value {:?} cannot be parsed as {:?}: {error}",
field.name(),
text,
field.data_type()
))
})
}