Repository navigation
applier: pin bytea_output in the swap's drain - #251
Conversation
The drain under the swap lock pinned extra_float_digits but left bytea_output to the caller's session, so a bytea converted to text could land in escape form where the copy wrote hex, with no verifier after it.
Main made the drain's search_path the Flusher's own path; the drain now pins chunksql.Rendering under that path and restores bytea_output too.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Correctness review on Jash's behalf at head 3ec0c05. Verdict: no concrete blocker found in the full diff; surrounding-source and local-test verification are incomplete. FlushIn now captures both caller output settings, uses the shared rendering settings for the drain, and restores bytea_output with a parameterized transaction-local set_config. The integration test checks actual copied and drained text under an escape-configured caller, then checks that the caller's setting is restored. That targets the data inconsistency described in the PR rather than merely asserting a configuration statement ran.
Verification: read the description, full diff, visible issue comments and check results. All 16 checks succeeded, including PostgreSQL 14–18. No local build or tests ran. Exact-head surrounding-source API access failed to connect; the shared Rendering constant, unchanged error/transaction paths, repository instructions outside the diff and inline review threads were not independently verified. Generated by Codex. |
|
🤖 Product review at 3ec0c05. OSS adoption: Binary-to-text conversions become consistent across copy and final drain even when a role uses escape output. The fix requires no new flag or configuration change, and the integration test captures the observable stored-value difference. SchemaBot integration: FlushIn keeps its existing API and restores the caller's bytea_output on the successful path, so callers can use the same transaction without compensating for this setting. Downstream transaction/error handling was not independently verified. Generated by Codex. |
JashLal
left a comment
There was a problem hiding this comment.
🤖 Approving on behalf of Jash after the review at 3ec0c05. Findings and verification limitations remain as recorded above. This approval follows the authorized workflow and does not resolve them. No local build or tests ran. Generated by Codex.
|
🤖 Review 1/2 of 3ec0c05: adversarial correctness, 0 blocking, 2 non-blocking. Inspected the full diff against the merge base ( The change is correct as written. Invariants. CO-9 (decision reads resolve to the real catalog): upholds. The Non-blocking1. The restore's transaction scope is not pinned: a session-scoped restore passes every testThe restore uses Test case: passes on 3ec0c05, fails with the restore made session-scopedpackage applier_test
import (
"testing"
"github.com/jackc/pgx/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// The caller's bytea_output is the caller's own for the transaction only:
// once it commits, the connection reads the setting it had before.
func TestFlushInLeavesTheConnectionsBytea(t *testing.T) {
f := newFlushFixture(t)
f.createOrders(t, 10)
p := f.prepare(t, "orders")
conn, err := f.pool.Acquire(t.Context())
require.NoError(t, err)
defer conn.Release()
var before string
require.NoError(t, conn.QueryRow(t.Context(), `SELECT current_setting('bytea_output')`).Scan(&before))
tx, err := conn.BeginTx(t.Context(), pgx.TxOptions{AccessMode: pgx.ReadWrite})
require.NoError(t, err)
_, err = tx.Exec(t.Context(), `SET LOCAL bytea_output = 'escape'`)
require.NoError(t, err)
_, err = p.flush.FlushIn(t.Context(), tx, batch(image(5, col("qty", "500"), col("note", "n"))))
require.NoError(t, err)
require.NoError(t, tx.Commit(t.Context()))
var after string
require.NoError(t, conn.QueryRow(t.Context(), `SELECT current_setting('bytea_output')`).Scan(&after))
assert.Equal(t, before, after, "the connection's own bytea_output outlives the transaction")
}
2.
|
| Mutant | Caught by |
|---|---|
pin extra_float_digits only (pre-PR drain) |
TestFlushInConvertsAByteaToTextAsTheCopyDoes |
no bytea_output restore |
TestFlushInConvertsAByteaToTextAsTheCopyDoes |
restore bytea_output as the constant hex |
TestFlushInConvertsAByteaToTextAsTheCopyDoes |
restore bytea_output before the write |
TestFlushInConvertsAByteaToTextAsTheCopyDoes |
no extra_float_digits restore |
TestFlushInWritesOnlyWhenTheCallerCommits |
restore session-scoped (true to false) |
survives (finding 1) |
M8: pin bytea_output only, drop extra_float_digits |
survives (finding 2) |
This review was generated by Claude Code (claude-sonnet-5-5).
|
🤖 Review 2/2 of 3ec0c05: OSS adoption and SchemaBot integration lenses, 0 blocking, 2 non-blocking. OSS adoption. The fix needs no flag, option, or configuration: an adopter whose database or role sets SchemaBot integration. A caller that opens its own transaction, sets its own session GUCs, and calls Non-blocking1. The limitations page still lists three writers, not four
2. The restore costs one more statement under the swap's lock
This review was generated by Claude Code (claude-sonnet-5-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Stamping with comments: 0 blocking, 4 non-blocking (see the two review comments above).
This stamp was left by Claude Code (claude-sonnet-5-5).
Tests now pin the drain's float rendering and that the restore is transaction-local; the limitations page names the drain as a writer.
|
🤖 Adversarial review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — pull/251, follow-up commit All 5 findings are fixed in the follow-up commit; nothing is deferred or rejected.
Source: block/pg-sprite#251, review comments 6515359367, 6515359689, 6567760477 and 6567761385 and reviews 5708258200 and 5731560446 at head |
The drain that runs inside the swap transaction now renders binary values as text in the same format as the copy, the catch-up, and the verifier.
Why
A copy-and-swap copies a table into a new one, keeps it up to date with live changes, and at the swap applies the last few changes inside the same transaction that swaps the tables. That last step could write binary data converted to text in a different format than the rest of the copy, depending on how the database or role was configured. The new table then disagreed with itself, and nothing checks the table after the swap. This PR makes the last step use the same format as everything else and leaves the caller's own setting as it found it.
What
FlushInpinschunksql.Rendering(extra_float_digits = 3,bytea_output = 'hex'), the pinFlush, the copier, and the verifier's digest already use, instead ofextra_float_digitsalone.bytea_outputalongsideextra_float_digitsand restores both, transaction-local, in one statement before returning.bytea→textand afloat8→textconversion drained byFlushInland the copier's text under a caller set toescape/extra_float_digits = 0, the caller's settings are restored, and the connection keeps its own settings after the commit.docs/limitations.mdnames the swap's drain among the writers that pin both settings.Before / after
One source column
body byteais retyped withALTER COLUMN body TYPE text USING body::text, and the database is configured withbytea_output = 'escape'. Row 1 is copied by the chunk copy and row 2 changes late, so the swap's drain writes it; both hold the bytes\x01ff. Only the drain's output changes; the copy, the catch-up flush, and the verifier already wrote hex and are unchanged.