Skip to content

storage: version the PostgreSQL oid source cast per export#37458

Open
def- wants to merge 3 commits into
MaterializeInc:mainfrom
def-:pr-37440-followup
Open

storage: version the PostgreSQL oid source cast per export#37458
def- wants to merge 3 commits into
MaterializeInc:mainfrom
def-:pr-37440-followup

Conversation

@def-

@def- def- commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #37440 Sorry for only realizing so late that it's an issue after all! Without this PR bin/mzcompose --find platform-checks run default --scenario=UpgradeEntireMz --check=PgCdcOidRetraction fails like this after the upgrade:

> SELECT id, o FROM oid_retraction_table ORDER BY id;
2026-07-06T13:19:56.907798Z  WARN mz_testdrive::action::consistency: No Catalog state on disk, skipping consistency check
^^^ +++
2:1: error: executing query failed: db error: ERROR: Source error: source must be dropped and recreated due to failure: failed to cast raw column: invalid input syntax for type oid: number too large to fit in target type: "4294967295"

Since the source is not an errored state it's possible customers could run into this.

parse_oid was widened to the full u32 range (#37440), but the same helper backs CastStringToOid in the PostgreSQL source, whose eval behavior must stay stable across releases. Replication re-casts the old tuple on delete, so a value ingested pre-upgrade as a CastError would be retracted post-upgrade as a value. That leaves the error stuck and adds a phantom negative row.

The fix versions the cast per export:

  • CastStringToOid is kept on a frozen parse_oid_legacy that accepts only the i32 range, so exports created before the widening keep retracting cleanly across upgrades. The SQL, COPY, and pgwire paths keep the widened parse_oid.
  • Newly purified exports persist a cast_oid_full_range flag in their statement DETAILS and plan a new CastStringToOidFullRange variant that accepts the full u32 range, so fresh sources ingest valid oid values above i32::MAX. Column casts are regenerated from the persisted details on every plan, so deriving the choice from the details (rather than the binary version) keeps it stable for the lifetime of an export.

Tests: a unit snapshot pinning the frozen legacy behavior, error snapshot and SQL-cast parity tests for the full-range variant, a platform-check reproducing the cross-upgrade retraction, and a pg-cdc testdrive file covering snapshot, replication, and retraction of above-i32::MAX oids in scalar and array columns.

Follow-up to MaterializeInc#37440
Sorry for only realizing so late that it's an issue after all!

`parse_oid` was widened to the full `u32` range (MaterializeInc#37440), but the same
helper backs `CastStringToOid` in the PostgreSQL source, whose eval
behavior must stay stable across releases. Replication re-casts the old
tuple on delete, so a value ingested pre-upgrade as a `CastError` would
be retracted post-upgrade as a value. That leaves the error stuck and
adds a phantom negative row.

Keep the storage cast on a frozen `parse_oid_legacy` that accepts only
the `i32` range. The SQL, COPY, and pgwire paths keep the widened
`parse_oid`. Add a unit snapshot pinning the frozen behavior and a
platform-check reproducing the cross-upgrade retraction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Freezing CastStringToOid on the legacy i32 range fixed retraction
symmetry for existing sources, but also made fresh PostgreSQL sources
reject valid oid values above i32::MAX, undoing the source-side part of
the widening from MaterializeInc#37440 for all future sources.

Version the cast per export instead. Newly purified exports persist
cast_oid_full_range in their statement DETAILS and plan the new
CastStringToOidFullRange, which accepts the full u32 range via the
widened parse_oid. Exports whose details predate the flag decode as
false and keep the legacy i32-range cast, so their persisted rows keep
retracting cleanly. The cast choice derives from the persisted details
rather than the binary version, which keeps replanning stable across
restarts and upgrades.

Add unit coverage for the full-range variant (error snapshot and parity
with the SQL cast) and a pg-cdc testdrive file exercising snapshot,
replication, and retraction of above-i32::MAX oids in scalar and array
columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@def- def- requested a review from a team as a code owner July 6, 2026 17:51
@def- def- changed the title storage: keep the PostgreSQL oid source cast on the legacy i32 range storage: version the PostgreSQL oid source cast per export Jul 6, 2026
The per-row cast error that the above-i32::MAX row produces on binaries
with the legacy i32-range oid cast makes mz_source_statuses report the
table export itself as stalled, while the source stays running and
replication continues. The old assertion raced the running-to-stalled
transition: sequential scenarios usually queried before the stall
landed, but the parallel-mode shards and the earliest-release upgrade
scenario saw stalled and failed in Nightly.

Accept both states for the table export. Binaries whose exports carry
cast_oid_full_range ingest the row and report running, legacy-cast
binaries report stalled. The check's real assertion stays the validate
SELECT that proves the delete cleanly retracts the error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant