#![cfg(all(feature = "wal2json", feature = "pg-walstream"))]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::hash::{Hash, Hasher};
use sqlite_diff_rs::pg_walstream::{ColumnValue, EventType, PgWalstream};
use sqlite_diff_rs::wal2json::{Action, Column, MessageV2, Wal2Json};
use sqlite_diff_rs::{
DynTable, NamedColumns, PatchSet, SchemaWithPK, SimpleTable, TypeMap, Value, WireColumnTypes,
WireSchema,
};
#[derive(Debug, Clone)]
struct UsersTable {
inner: SimpleTable,
pg_oids: Vec<u32>,
pg_type_names: Vec<Arc<str>>,
}
impl PartialEq for UsersTable {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl Eq for UsersTable {}
impl Hash for UsersTable {
fn hash<H: Hasher>(&self, state: &mut H) {
self.inner.hash(state);
}
}
impl DynTable for UsersTable {
fn name(&self) -> &str {
self.inner.name()
}
fn number_of_columns(&self) -> usize {
self.inner.number_of_columns()
}
fn write_pk_flags(&self, buf: &mut [u8]) {
self.inner.write_pk_flags(buf);
}
}
impl SchemaWithPK for UsersTable {
fn number_of_primary_keys(&self) -> usize {
self.inner.number_of_primary_keys()
}
fn primary_key_index(&self, col_idx: usize) -> Option<usize> {
self.inner.primary_key_index(col_idx)
}
fn extract_pk<S, B>(
&self,
values: &impl sqlite_diff_rs::schema::IndexableValues<Text = S, Binary = B>,
) -> Vec<Value<S, B>>
where
S: Clone,
B: Clone,
{
self.inner.extract_pk(values)
}
}
impl NamedColumns for UsersTable {
fn column_index(&self, column_name: &str) -> Option<usize> {
NamedColumns::column_index(&self.inner, column_name)
}
}
impl WireColumnTypes<PgWalstream> for UsersTable {
fn column_type_key(&self, column_index: usize) -> u32 {
self.pg_oids[column_index]
}
}
impl WireColumnTypes<Wal2Json> for UsersTable {
fn column_type_key(&self, column_index: usize) -> Arc<str> {
Arc::clone(&self.pg_type_names[column_index])
}
}
#[derive(Debug, Clone)]
struct AppSchema {
users: UsersTable,
}
impl WireSchema<PgWalstream> for AppSchema {
type Table = UsersTable;
fn get(&self, table_name: &str) -> Option<&Self::Table> {
(table_name == self.users.name()).then_some(&self.users)
}
}
impl WireSchema<Wal2Json> for AppSchema {
type Table = UsersTable;
fn get(&self, table_name: &str) -> Option<&Self::Table> {
(table_name == self.users.name()).then_some(&self.users)
}
}
#[allow(clippy::type_complexity)]
fn scenario_scalar_row() -> (
AppSchema,
Vec<(&'static str, ColumnValue, serde_json::Value)>,
) {
let inner = SimpleTable::new(
"users",
&["id", "active", "handle", "credits", "price", "ts"],
&[0],
);
let pg_oids: Vec<u32> = alloc::vec![20, 16, 25, 20, 1700, 1184];
let pg_type_names: Vec<Arc<str>> = alloc::vec![
Arc::from("bigint"),
Arc::from("boolean"),
Arc::from("text"),
Arc::from("bigint"),
Arc::from("numeric"),
Arc::from("timestamp with time zone"),
];
let schema = AppSchema {
users: UsersTable {
inner,
pg_oids,
pg_type_names,
},
};
let vals: Vec<(&'static str, ColumnValue, serde_json::Value)> = alloc::vec![
(
"id",
ColumnValue::text("42"),
serde_json::Value::Number(42_i64.into())
),
(
"active",
ColumnValue::text("t"),
serde_json::Value::Bool(true)
),
(
"handle",
ColumnValue::text("alice"),
serde_json::Value::String("alice".into()),
),
(
"credits",
ColumnValue::text("100"),
serde_json::Value::Number(100_i64.into()),
),
(
"price",
ColumnValue::text("12.34"),
serde_json::Value::String("12.34".into()),
),
(
"ts",
ColumnValue::text("2024-01-15 10:30:00+00"),
serde_json::Value::String("2024-01-15 10:30:00+00".into()),
),
];
(schema, vals)
}
fn row_data(cols: &[(&'static str, ColumnValue)]) -> pg_walstream::RowData {
let mut row = pg_walstream::RowData::new();
for (name, value) in cols {
row.push(Arc::from(*name), value.clone());
}
row
}
fn columns_from_vals(
schema: &AppSchema,
vals: &[(&'static str, ColumnValue, serde_json::Value)],
) -> Vec<Column> {
vals.iter()
.map(|(name, _, value)| {
let idx = NamedColumns::column_index(&schema.users, name).unwrap();
Column {
name: (*name).to_string(),
type_name: schema.users.pg_type_names[idx].as_ref().to_string(),
value: value.clone(),
}
})
.collect()
}
#[test]
fn cross_format_insert_produces_byte_equal_patchset() {
let (schema, vals) = scenario_scalar_row();
let pg_event = EventType::Insert {
schema: Arc::from("public"),
table: Arc::from("users"),
relation_oid: 1,
data: row_data(
&vals
.iter()
.map(|(n, cv, _)| (*n, cv.clone()))
.collect::<Vec<_>>(),
),
};
let w2j_msg = MessageV2 {
action: Action::I,
schema: Some("public".to_string()),
table: Some("users".to_string()),
columns: Some(columns_from_vals(&schema, &vals)),
identity: None,
};
let pg_types: TypeMap<PgWalstream, String, Vec<u8>> = TypeMap::defaults();
let w2j_types: TypeMap<Wal2Json, String, Vec<u8>> = TypeMap::defaults();
let pg_patchset = PatchSet::<UsersTable, String, Vec<u8>>::new()
.digest(&pg_event, &schema, &pg_types)
.unwrap();
let w2j_patchset = PatchSet::<UsersTable, String, Vec<u8>>::new()
.digest(&w2j_msg, &schema, &w2j_types)
.unwrap();
let pg_bytes: Vec<u8> = pg_patchset.build();
let w2j_bytes: Vec<u8> = w2j_patchset.build();
assert_eq!(
pg_bytes, w2j_bytes,
"pg_walstream and wal2json digests diverge on the same insert"
);
}
#[test]
fn cross_format_delete_produces_byte_equal_patchset() {
let (schema, vals) = scenario_scalar_row();
let pg_event = EventType::Delete {
schema: Arc::from("public"),
table: Arc::from("users"),
relation_oid: 1,
old_data: row_data(
&vals
.iter()
.map(|(n, cv, _)| (*n, cv.clone()))
.collect::<Vec<_>>(),
),
replica_identity: pg_walstream::ReplicaIdentity::Default,
key_columns: alloc::vec![Arc::from("id")],
};
let w2j_msg = MessageV2 {
action: Action::D,
schema: Some("public".to_string()),
table: Some("users".to_string()),
columns: None,
identity: Some(columns_from_vals(&schema, &vals)),
};
let pg_types: TypeMap<PgWalstream, String, Vec<u8>> = TypeMap::defaults();
let w2j_types: TypeMap<Wal2Json, String, Vec<u8>> = TypeMap::defaults();
let pg_patchset = PatchSet::<UsersTable, String, Vec<u8>>::new()
.digest(&pg_event, &schema, &pg_types)
.unwrap();
let w2j_patchset = PatchSet::<UsersTable, String, Vec<u8>>::new()
.digest(&w2j_msg, &schema, &w2j_types)
.unwrap();
let pg_bytes: Vec<u8> = pg_patchset.build();
let w2j_bytes: Vec<u8> = w2j_patchset.build();
assert_eq!(
pg_bytes, w2j_bytes,
"pg_walstream and wal2json delete digests diverge"
);
}
#[test]
fn cross_format_update_produces_byte_equal_patchset() {
let (schema, mut vals) = scenario_scalar_row();
let old_vals: Vec<(&'static str, ColumnValue, serde_json::Value)> = vals.clone();
for (name, cv, json) in &mut vals {
if *name == "handle" {
*cv = ColumnValue::text("alicia");
*json = serde_json::Value::String("alicia".into());
}
}
let pg_event = EventType::Update {
schema: Arc::from("public"),
table: Arc::from("users"),
relation_oid: 1,
old_data: Some(row_data(
&old_vals
.iter()
.map(|(n, cv, _)| (*n, cv.clone()))
.collect::<Vec<_>>(),
)),
new_data: row_data(
&vals
.iter()
.map(|(n, cv, _)| (*n, cv.clone()))
.collect::<Vec<_>>(),
),
replica_identity: pg_walstream::ReplicaIdentity::Default,
key_columns: alloc::vec![Arc::from("id")],
};
let w2j_msg = MessageV2 {
action: Action::U,
schema: Some("public".to_string()),
table: Some("users".to_string()),
columns: Some(columns_from_vals(&schema, &vals)),
identity: Some(columns_from_vals(&schema, &old_vals)),
};
let pg_types: TypeMap<PgWalstream, String, Vec<u8>> = TypeMap::defaults();
let w2j_types: TypeMap<Wal2Json, String, Vec<u8>> = TypeMap::defaults();
let pg_patchset = PatchSet::<UsersTable, String, Vec<u8>>::new()
.digest(&pg_event, &schema, &pg_types)
.unwrap();
let w2j_patchset = PatchSet::<UsersTable, String, Vec<u8>>::new()
.digest(&w2j_msg, &schema, &w2j_types)
.unwrap();
let pg_bytes: Vec<u8> = pg_patchset.build();
let w2j_bytes: Vec<u8> = w2j_patchset.build();
assert_eq!(
pg_bytes, w2j_bytes,
"pg_walstream and wal2json update digests diverge"
);
}
#[test]
fn wal2json_v1_and_v2_insert_produce_byte_equal_patchset() {
use sqlite_diff_rs::wal2json::ChangeV1;
let (schema, vals) = scenario_scalar_row();
let v2_msg = MessageV2 {
action: Action::I,
schema: Some("public".to_string()),
table: Some("users".to_string()),
columns: Some(columns_from_vals(&schema, &vals)),
identity: None,
};
let v1_change = ChangeV1 {
kind: "insert".to_string(),
schema: "public".to_string(),
table: "users".to_string(),
columnnames: vals.iter().map(|(n, _, _)| (*n).to_string()).collect(),
columntypes: vals
.iter()
.map(|(n, _, _)| {
let idx = NamedColumns::column_index(&schema.users, n).unwrap();
schema.users.pg_type_names[idx].as_ref().to_string()
})
.collect(),
columnvalues: vals.iter().map(|(_, _, v)| v.clone()).collect(),
oldkeys: None,
};
let w2j_types: TypeMap<Wal2Json, String, Vec<u8>> = TypeMap::defaults();
let v2_patchset = PatchSet::<UsersTable, String, Vec<u8>>::new()
.digest(&v2_msg, &schema, &w2j_types)
.unwrap();
let v1_patchset = PatchSet::<UsersTable, String, Vec<u8>>::new()
.digest(&v1_change, &schema, &w2j_types)
.unwrap();
let v2_bytes: Vec<u8> = v2_patchset.build();
let v1_bytes: Vec<u8> = v1_patchset.build();
assert_eq!(
v2_bytes, v1_bytes,
"wal2json v1 and v2 digests diverge on the same insert"
);
}