use alloc::string::String;
use alloc::vec::Vec;
pub use pg_walstream::Oid;
pub use pg_walstream::{ChangeEvent, ColumnValue, EventType, Lsn, ReplicaIdentity, RowData};
use crate::ChangesetFormat;
use crate::builders::{
ChangeDelete, DiffOps, DiffSetBuilder, Insert, PatchDelete, PatchsetFormat, Update,
};
use crate::encoding::Value;
use crate::schema::NamedColumns;
use crate::wire::{Sealed, WireAdapter, WireSource};
use core::fmt::Debug;
use core::hash::Hash;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ConversionError {
#[error("Column '{0}' not found in table schema")]
ColumnNotFound(String),
#[error("Table name mismatch: expected '{expected}', got '{actual}'")]
TableMismatch {
expected: String,
actual: String,
},
#[error("Table '{0}' not found in schema")]
TableNotFound(String),
#[error("Missing data in event")]
MissingData,
#[error("Unsupported value for column '{0}'")]
UnsupportedType(String),
#[error("Event type '{0}' cannot be converted to the requested operation")]
InvalidEventType(String),
#[error("Old data not available (check replica identity setting)")]
MissingOldData,
#[error("Decoder failed: {0}")]
Decode(#[from] crate::wire::DecodeError),
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PgWalstream;
impl Sealed for PgWalstream {}
impl WireSource for PgWalstream {
type Payload<'a> = PgWalstreamColumn<'a>;
type TypeKey = Oid;
fn type_key(payload: &Self::Payload<'_>) -> Self::TypeKey {
payload.oid
}
fn column_name<'a>(payload: &'a Self::Payload<'_>) -> &'a str {
payload.column_name
}
}
#[derive(Debug, Clone, Copy)]
pub struct PgWalstreamColumn<'a> {
pub column_name: &'a str,
pub oid: Oid,
pub type_modifier: i32,
pub data: &'a ColumnValue,
}
impl PgWalstreamColumn<'_> {
pub fn decoded_by<D, S, B>(
self,
decoder: &D,
) -> Result<crate::encoding::Value<S, B>, crate::wire::DecodeError>
where
D: crate::wire::Decoder<PgWalstream, S, B>,
{
decoder.decode(self)
}
}
use crate::wire::{Digestable, WireColumnTypes, WireSchema};
impl<T, S, B> Digestable<ChangesetFormat, T, S, B> for EventType
where
T: NamedColumns + WireColumnTypes<PgWalstream>,
S: Clone + Debug + Hash + Eq + AsRef<str> + Default,
B: Clone + Debug + Hash + Eq + AsRef<[u8]> + Default,
{
type Src = PgWalstream;
type Error = ConversionError;
fn digest_into<Sch, A>(
&self,
builder: DiffSetBuilder<ChangesetFormat, T, S, B>,
schema: &Sch,
adapter: &A,
) -> Result<DiffSetBuilder<ChangesetFormat, T, S, B>, ConversionError>
where
Sch: WireSchema<PgWalstream, Table = T>,
A: WireAdapter<PgWalstream, S, B>,
{
match self {
EventType::Insert {
table: name, data, ..
} => {
let table = resolve_table(schema, name.as_ref())?;
let insert = build_insert_from_pg(data, table, adapter)?;
Ok(DiffOps::insert(builder, insert))
}
EventType::Update {
table: name,
old_data,
new_data,
..
} => {
let table = resolve_table(schema, name.as_ref())?;
let update =
build_changeset_update_from_pg(old_data.as_ref(), new_data, table, adapter)?;
Ok(DiffOps::update(builder, update))
}
EventType::Delete {
table: name,
old_data,
..
} => {
let table = resolve_table(schema, name.as_ref())?;
let delete = build_changeset_delete_from_pg(old_data, table, adapter)?;
Ok(DiffOps::delete(builder, delete))
}
_ => Ok(builder),
}
}
}
impl<T, S, B> Digestable<PatchsetFormat, T, S, B> for EventType
where
T: NamedColumns + WireColumnTypes<PgWalstream>,
S: Clone + Debug + Hash + Eq + AsRef<str> + Default,
B: Clone + Debug + Hash + Eq + AsRef<[u8]> + Default,
{
type Src = PgWalstream;
type Error = ConversionError;
fn digest_into<Sch, A>(
&self,
builder: DiffSetBuilder<PatchsetFormat, T, S, B>,
schema: &Sch,
adapter: &A,
) -> Result<DiffSetBuilder<PatchsetFormat, T, S, B>, ConversionError>
where
Sch: WireSchema<PgWalstream, Table = T>,
A: WireAdapter<PgWalstream, S, B>,
{
match self {
EventType::Insert {
table: name, data, ..
} => {
let table = resolve_table(schema, name.as_ref())?;
let insert = build_insert_from_pg(data, table, adapter)?;
Ok(DiffOps::insert(builder, insert))
}
EventType::Update {
table: name,
new_data,
..
} => {
let table = resolve_table(schema, name.as_ref())?;
let update = build_patchset_update_from_pg(new_data, table, adapter)?;
Ok(DiffOps::update(builder, update))
}
EventType::Delete {
table: name,
old_data,
..
} => {
let table = resolve_table(schema, name.as_ref())?;
let delete = build_patch_delete_from_pg(old_data, table, adapter)?;
Ok(DiffOps::delete(builder, delete))
}
_ => Ok(builder),
}
}
}
fn resolve_table<'a, Sch>(schema: &'a Sch, name: &str) -> Result<&'a Sch::Table, ConversionError>
where
Sch: WireSchema<PgWalstream>,
{
schema
.get(name)
.ok_or_else(|| ConversionError::TableNotFound(name.into()))
}
fn build_insert_from_pg<T, S, B, A>(
data: &RowData,
table: &T,
adapter: &A,
) -> Result<Insert<T, S, B>, ConversionError>
where
T: NamedColumns + WireColumnTypes<PgWalstream>,
S: Clone + AsRef<str>,
B: Clone + AsRef<[u8]>,
A: WireAdapter<PgWalstream, S, B>,
{
let mut insert = Insert::from(table.clone());
for (name, value) in data.iter() {
let col_idx = table
.column_index(name.as_ref())
.ok_or_else(|| ConversionError::ColumnNotFound(name.as_ref().into()))?;
let payload = PgWalstreamColumn {
column_name: name.as_ref(),
oid: table.column_type_key(col_idx),
type_modifier: -1,
data: value,
};
let decoded = adapter.decode(payload)?;
insert = insert
.set(col_idx, decoded)
.map_err(|_| ConversionError::ColumnNotFound(name.as_ref().into()))?;
}
Ok(insert)
}
fn build_changeset_update_from_pg<T, S, B, A>(
old_data: Option<&RowData>,
new_data: &RowData,
table: &T,
adapter: &A,
) -> Result<Update<T, ChangesetFormat, S, B>, ConversionError>
where
T: NamedColumns + WireColumnTypes<PgWalstream>,
S: Clone + Debug + AsRef<str>,
B: Clone + Debug + AsRef<[u8]>,
A: WireAdapter<PgWalstream, S, B>,
{
let mut update: Update<T, ChangesetFormat, S, B> = Update::from(table.clone());
for (name, new_value) in new_data.iter() {
let col_idx = table
.column_index(name.as_ref())
.ok_or_else(|| ConversionError::ColumnNotFound(name.as_ref().into()))?;
let oid = table.column_type_key(col_idx);
let new_payload = PgWalstreamColumn {
column_name: name.as_ref(),
oid,
type_modifier: -1,
data: new_value,
};
let new_decoded = adapter.decode(new_payload)?;
if let Some(old) = old_data
&& let Some(old_value) = old.get(name.as_ref())
{
let old_payload = PgWalstreamColumn {
column_name: name.as_ref(),
oid,
type_modifier: -1,
data: old_value,
};
let old_decoded = adapter.decode(old_payload)?;
update = update
.set(col_idx, old_decoded, new_decoded)
.map_err(|_| ConversionError::ColumnNotFound(name.as_ref().into()))?;
continue;
}
update = update
.set_new(col_idx, new_decoded)
.map_err(|_| ConversionError::ColumnNotFound(name.as_ref().into()))?;
}
Ok(update)
}
fn build_patchset_update_from_pg<T, S, B, A>(
new_data: &RowData,
table: &T,
adapter: &A,
) -> Result<Update<T, PatchsetFormat, S, B>, ConversionError>
where
T: NamedColumns + WireColumnTypes<PgWalstream>,
S: Clone + AsRef<str>,
B: Clone + AsRef<[u8]>,
A: WireAdapter<PgWalstream, S, B>,
{
let mut update: Update<T, PatchsetFormat, S, B> = Update::from(table.clone());
for (name, value) in new_data.iter() {
let col_idx = table
.column_index(name.as_ref())
.ok_or_else(|| ConversionError::ColumnNotFound(name.as_ref().into()))?;
let payload = PgWalstreamColumn {
column_name: name.as_ref(),
oid: table.column_type_key(col_idx),
type_modifier: -1,
data: value,
};
let decoded = adapter.decode(payload)?;
update = update
.set(col_idx, decoded)
.map_err(|_| ConversionError::ColumnNotFound(name.as_ref().into()))?;
}
Ok(update)
}
fn build_changeset_delete_from_pg<T, S, B, A>(
old_data: &RowData,
table: &T,
adapter: &A,
) -> Result<ChangeDelete<T, S, B>, ConversionError>
where
T: NamedColumns + WireColumnTypes<PgWalstream>,
S: Clone + Default + AsRef<str>,
B: Clone + Default + AsRef<[u8]>,
A: WireAdapter<PgWalstream, S, B>,
{
let mut delete = ChangeDelete::from(table.clone());
for (name, value) in old_data.iter() {
let col_idx = table
.column_index(name.as_ref())
.ok_or_else(|| ConversionError::ColumnNotFound(name.as_ref().into()))?;
let payload = PgWalstreamColumn {
column_name: name.as_ref(),
oid: table.column_type_key(col_idx),
type_modifier: -1,
data: value,
};
let decoded = adapter.decode(payload)?;
delete = delete
.set(col_idx, decoded)
.map_err(|_| ConversionError::ColumnNotFound(name.as_ref().into()))?;
}
Ok(delete)
}
fn build_patch_delete_from_pg<T, S, B, A>(
old_data: &RowData,
table: &T,
adapter: &A,
) -> Result<PatchDelete<T, S, B>, ConversionError>
where
T: NamedColumns + WireColumnTypes<PgWalstream>,
S: Clone + AsRef<str>,
B: Clone + AsRef<[u8]>,
A: WireAdapter<PgWalstream, S, B>,
{
let num_pks = table.number_of_primary_keys();
let mut pk_slots: Vec<Option<Value<S, B>>> = alloc::vec![None; num_pks];
for (name, value) in old_data.iter() {
let col_idx = table
.column_index(name.as_ref())
.ok_or_else(|| ConversionError::ColumnNotFound(name.as_ref().into()))?;
if let Some(pk_idx) = table.primary_key_index(col_idx) {
let payload = PgWalstreamColumn {
column_name: name.as_ref(),
oid: table.column_type_key(col_idx),
type_modifier: -1,
data: value,
};
pk_slots[pk_idx] = Some(adapter.decode(payload)?);
}
}
let pk: Vec<Value<S, B>> = pk_slots
.into_iter()
.collect::<Option<Vec<_>>>()
.ok_or(ConversionError::MissingData)?;
Ok(PatchDelete::new(table.clone(), pk))
}