fix: decode bytea hex from wal2json before writing to postgres - #1090
fix: decode bytea hex from wal2json before writing to postgres#1090siriusfreak wants to merge 2 commits into
Conversation
wal2json renders a bytea column as bare hex digits ("deadbeef"), but
filterRowColumnsForAction has no case for bytea, so that Go string reaches pgx
as the parameter value for a bytea column and its ASCII characters are stored as
the column contents: '\xdeadbeef' on the source becomes '\x6465616462656566' on
the target.
Nothing else about the row changes, so row counts and keys still match and the
corruption only surfaces when something parses the column — for JSON payloads
stored in bytea, as a parse error on the first byte. Snapshots are unaffected
because pgx hands those values over as []byte, which means a freshly loaded
target looks correct and degrades as replication runs.
Decode on the write path instead, accepting both the bare form wal2json emits
and the postgres hex format that pkg/transformers already documents for the
replication path. A value that is not valid hex is passed through untouched, so
an unexpected producer format degrades to the previous behaviour rather than
failing the batch.
Fixes xataio#1089
|
LLM miss to disclose. Claude Code, Opus 5 |
CoverageTotal: 59.5% (±0.0% vs Coverage in packages changed by this PR:
|
|
Thank you for the contribution. The issue you have identified is real. However, the fix doec not cover all of the problematic areas. I would prefer to decode at the
// wal_pg_listener.go, processWALEvent
if err := l.walDataDeserialiser(msg.Data, event.Data); err != nil {
return fmt.Errorf("error unmarshaling wal data: %w", err)
}
decodeByteaColumns(event.Data)
This also removes a hazard in the current placement. The transformer processor wraps the writer ( column_transformers:
payload: # bytea column
name: literal_string
parameters:
literal: "00000000" # stores 4 bytes, not 8 |
Fixes #1089.
wal2json renders a
byteacolumn as bare hex digits:filterRowColumnsForActionspecial-casesjsonb/json, ranges,tsvectorand arrays but has no case forbytea, so that Go string is handed to pgx as the parameter value for abyteacolumn and its eight ASCII characters are stored as the column contents.\xdeadbeefon the source becomes\x6465616462656566on the target.Nothing else about the row changes, so row counts and keys still match and the corruption only surfaces when something parses the column. Snapshots are unaffected — pgx hands those values over as
[]byte— so a freshly loaded target looks correct and then degrades as replication runs, which makes this easy to miss.The change
deserializeByteaValueon the write path, applied whereverserializeJSONBValuealready is (the row values and theWHEREclause, soUPDATEandDELETEare covered as well asINSERT).It accepts both the bare form wal2json emits and the postgres
\xhex format, sincepkg/transformers/encrypted_aes_siv_transformer.godocuments the latter for this path. Values that are already[]byte, as they are during snapshots, fall through untouched.A value that is not valid hex is passed through rather than dropped, so an unexpected producer format degrades to the previous behaviour instead of failing the batch.
While here:
decodeByteaHexin that transformer requires the\xprefix and returnserrEncryptedAESSIVByteaNotHexwithout one, while wal2json emits the bare form — that path looks affected too, but it is a separate concern and I left it alone.Verification
Unit tests cover both hex forms, an empty value, an already-decoded
[]byte, invalid and odd-length hex,nil, and a non-byteacolumn.Also checked end to end against two live PostgreSQL instances with the repro from #1089, on
INSERT, onUPDATE(which exercises theWHEREpath), and on an emptybytea:Before the change those three arrive as
\x303130326666,\x37623232...and an empty value respectively.