fix: decode bytea hex from wal2json before writing to postgres - #1090
Conversation
|
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 |
wal2json renders a bytea column as bare hex digits ("deadbeef"). That text
travels to the writer and is handed to pgx as the parameter value for a bytea
column, so 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. 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 at the wal2json boundary, in processWALEvent, rather than in the writer.
walDataDeserialiser is the only place that knows the value came from wal2json,
so one pass covers every downstream call site, including the WHERE clause and
buildBulkDeleteSinglePK/buildBulkDeleteCompositePK, which bound the raw hex.
wal.Data carries both Columns and Identity, so both are walked.
It also puts the decode before the transformer processor rather than after it.
Transformers that declare ByteArrayDataType return a plain string, and decoding
downstream would have halved any even-length hex they produced — a
literal_string of "00000000" would have stored 4 bytes instead of 8. Doing it
here means transformers see the same []byte the snapshot path gives them.
Both the bare form and the postgres hex format ("\x...") are accepted, since
pkg/transformers documents the latter for this path. A value that is not valid
hex is left untouched rather than dropped, so an unexpected producer format
degrades to the previous behaviour instead of failing the batch.
Verified against two live PostgreSQL instances: INSERT, UPDATE and DELETE all
round-trip byte-identically, including a table with a bytea PRIMARY KEY, where
the delete and update match through the identity path that previously bound hex
text.
Fixes xataio#1089
2c87b53 to
84e887e
Compare
|
Thanks — you're right on both counts, and I've moved it.
The transformer hazard is the more important half and I had missed it entirely. Decoding after the transformer processor meant any The writer-side change is gone; Verified against two live PostgreSQL instances after the move, including the identity path specifically: a table with a Also rebased onto current |
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.