diff --git a/.agents/skills/mz-debug-ci/SKILL.md b/.agents/skills/mz-debug-ci/SKILL.md index 3cf2f4e109556..6f57daae35050 100644 --- a/.agents/skills/mz-debug-ci/SKILL.md +++ b/.agents/skills/mz-debug-ci/SKILL.md @@ -1,132 +1,200 @@ --- name: mz-debug-ci description: > - Investigate CI failures on PR via gh + bk CLI. Trigger: failing checks, + Investigate CI failures on PR via gh + Buildkite MCP or bk CLI. Trigger: failing checks, Buildkite failures, CI issues — "why is CI red", "build broken", "checks failing", "what went wrong in CI", "nightly broke", "tests failing on this PR", or pasted Buildkite URL. Also PR number + why failing. -argument-hint: +argument-hint: --- -Investigate CI failures for a Materialize PR. +Investigate CI failures for a Materialize PR or Buildkite build. -## Prerequisites - -This skill requires both `gh` (GitHub CLI) and `bk` (Buildkite CLI) to be installed *and authenticated*. Before doing anything else, verify both: - -```bash -which gh && gh auth status -which bk && bk auth status -``` +If the input is a Buildkite build URL rather than a PR (a scheduled nightly on +main, a release-qualification build), start directly at "Listing a +build's failed jobs directly", taking `` and `` +straight from the URL: +`https://buildkite.com/materialize//builds/`, +then continue from Step 2. -If either tool is missing or unauthenticated, **stop immediately** and tell the user what to fix (`bk configure` or `bk auth login` for Buildkite). Do not attempt to use the REST API directly or any other workaround — this workflow only works with these CLI tools. +## Prerequisites -Both `gh` and `bk` make network requests that are blocked by the default sandbox. All Bash commands in this workflow must use `dangerouslyDisableSandbox: true`. +This skill requires `gh` (GitHub CLI), authenticated, plus one of two ways +to reach Buildkite: the Buildkite MCP server's tools (preferred, next +section) or `bk` (Buildkite CLI), authenticated. If any of these turns out +to be missing or unauthenticated, **stop immediately** and tell the user +what to fix (`bk configure` or `bk auth login` for the Buildkite CLI, or the +MCP setup below). Do not attempt to use the REST API directly or any other +workaround. + +## Buildkite MCP: preferred when available + +When the Buildkite MCP server's tools are available in the session (tool +names like `get_build_failure_summary`, `search_logs`, ...), prefer them +over `bk` everywhere below: structured JSON with the +job-state semantics already applied, and no log cleanup needed. The server documents its +own tools; this section adds only what it does not know: + +- `org_slug` is `materialize`. `get_build_failure_summary` replaces "Listing + a build's failed jobs directly", Step 2, and often Step 3; + `search_logs`/`read_logs`/`tail_logs` replace the `bk job log` pipelines; + the artifact and annotation tools replace the corresponding `bk` commands. + Everything Materialize-specific (annotation contents, `bin/ci-failures`, + `bin/ci-shards`, Steps 4-5) applies unchanged. +- The summary's job list is bounded by the response's `job_limit` (10 on the + hosted server, where a larger `max_jobs` is silently clamped), with terminal + problem jobs sorted before downstream broken ones. On `jobs_truncated: + true`, get the definitive failed-job list from `list_jobs` with + `state: "failed,timed_out"` and `include_retried_jobs: false`. That last + parameter is what preserves triage semantics: the server defaults it to + true, which counts every failed attempt of a retried job separately, + including jobs that passed on retry. The summary bounds annotation content + too: on `content_truncated: true`, fetch the full set with + `list_annotations`. + +If the MCP is absent, read `bk-fallback.md` in this skill's directory for +the `bk` command recipes and continue with those. At the end, mention to +the user that they can set the server up (hosted read-only endpoint: +`claude mcp add --scope user --transport http buildkite https://mcp.buildkite.com/mcp/readonly`; +it saves tool calls but costs somewhat more tokens). ## Search existing failures by pattern -Search for already recorded failures in CI with a short, stable error substring: +Search for already recorded failures in CI with a short, stable error +substring. The output is JSON whose `content` blobs are huge, so project it +down, and read the total from `.meta.totalRowCount`, which ignores the row +cap and is the "how chronic is this?" number: ```bash -bin/ci-failures 'foobar' +bin/ci-failures 'foobar' 2>/dev/null | jq -r '.meta.totalRowCount, + (.data[] | [.build_date[0:10], .build_identifier, .test_suite, .issue] | @tsv)' ``` -Use `--search` for the global search. Narrow results with `--branch main`, -`--version`, `--issue`, `--test`, `--build`, `--start-date`, or `--end-date`. -The command explains how to create a token when needed. +The positional pattern matches failure content. `--search ` replaces +the positional pattern and matches across build, test, issue, content, and +branch at once. Narrow results with `--branch main`, `--version`, `--issue`, +`--test` (the Buildkite job name), `--build`, `--start-date`, or +`--end-date`; `--size` raises the row cap (default 31, max 100). -## Step 1: Extract PR number +## Step 1: Find the build -Parse `$ARGUMENTS` to get the PR number. Handle both formats: -- Plain number: `35192` -- Full URL: `https://github.com/MaterializeInc/materialize/pull/35192` - -## Step 2: Find the build - -Use `gh` to get the PR's branch name and then find the Buildkite build: - -```bash -# Get the branch name for the PR -gh pr view --json headRefName --jq .headRefName -``` +Parse `$ARGUMENTS` to get the PR number (a plain number or a +`https://github.com/MaterializeInc/materialize/pull/` URL), then +list the failing checks: -Alternatively, list failing checks directly: ```bash gh pr checks 2>&1 ``` -Lines containing `fail` have tab-separated fields: -``` -name fail 0 https://buildkite.com/materialize//builds/# description -``` - -Extract from the URL: -- **Pipeline**: path segment after `materialize/` (usually `test`) -- **Build number**: the number after `builds/` -- **Job ID**: the UUID after `#` - -## Step 3: Check annotations first - -**Before diving into logs**, fetch the build annotations. They contain pre-extracted error messages, stack traces, and links to known flaky test issues — this saves significant time compared to grepping through raw logs. - -```bash -bk api /pipelines//builds//annotations --no-pager 2>/dev/null -``` - -Note that `bk` can't be piped through `2>&1`. - -The response is JSON. Each annotation has: -- `style`: `"error"` for failures -- `body_html`: HTML containing the error summary, including: - - The specific test/job that failed - - The actual error message or stack trace in `
` blocks
-  - Links to known flaky test issues (look for GitHub issue links like `database-issues/#NNNN`)
-  - Main branch history showing if this test passes on main (flaky test indicator)
-
-Parse the error annotations to get a quick overview of all failures before fetching any logs.
-
-## Step 4: Fetch logs when needed
-
-Only fetch full logs when annotations don't provide enough detail. Triage in this order:
-
-1. **clippy** — compilation/lint errors that often explain everything
-2. **lint-and-rustfmt** — formatting and lint-check failures
-3. **cargo-test** — unit/integration test failures
-4. **fast-sql-logic-tests** — SLT failures
-5. **testdrive** — integration test failures (often cascading)
-6. **Everything else** (checks-parallel, cluster-tests, dbt, etc.)
-
-To fetch a job's log:
-```bash
-bk job log  -p  -b  --no-timestamps --no-pager 2>/dev/null | tail -100
-```
-
-For large logs, first grep for errors to find the relevant section:
-```bash
-bk job log  -p  -b  --no-timestamps --no-pager 2>/dev/null | grep -B2 -A5 'error\|FAIL\|panicked'
-```
-
-Fetch multiple job logs in parallel when they are independent (e.g., clippy + lint at the same time).
+Failing rows link to
+`https://buildkite.com/materialize//builds/#`,
+the identifiers used throughout this skill.
+
+### Listing a build's failed jobs directly
+
+Buildkite mirrors each job's result into GitHub as a
+`buildkite//` commit status (never-dispatched jobs post
+nothing), but `gh pr checks` reads the PR's current head commit only. A build
+therefore shows up there only if it ran on exactly that commit. For everything
+else (scheduled nightlies on main, release-qualification builds, a PR nightly
+from before the latest push), list the genuinely failed jobs via the API:
+`list_jobs` as described in the MCP section, or with `bk` the listing recipe
+in `bk-fallback.md`. Either way, also grab the build's commit and branch:
+Step 5's known-vs-new checks need the commit, and neighboring-build lookups
+need the branch.
+
+Job-state semantics matter here:
+- The failure states are `failed` and `timed_out`, the same set as
+  `BUILDKITE_RELEVANT_FAILED_BUILD_STEP_STATES` in
+  `misc/python/materialize/buildkite_insights/buildkite_api/buildkite_constants.py`.
+- `broken` is not a failure. Buildkite marks a job `broken` when its
+  configuration prevents it from running, for example a branch filter or an
+  `if:` condition that evaluates false, so the job never dispatches. Every
+  build contains broken jobs (a routine main nightly has dozens), so counting
+  them massively over-counts failures.
+- Jobs with `soft_failed: true` are allowed to fail without making the build
+  red; no current CI step sets it, so filtering it is defensive.
+- `waiting_failed` is not a failure either: the job's dependency (usually a
+  build step) failed, so it never started. Its presence is informative,
+  though. A PR build full of `waiting_failed` jobs ran no tests at all, so
+  report that fixing the visible failures is necessary but no proof the
+  build then goes green.
+- Retried jobs (automatic or manual) appear only as their latest attempt, so
+  a job that failed and then passed on retry counts as passed. That is the
+  right verdict for build triage; fetch the earlier attempts only when an
+  in-depth investigation needs them. A manual retry that failed again is
+  itself a signal: someone already tried to shake the failure off and it
+  reproduced.
+
+To find a PR's builds that are not on its current head in the first place (an
+older nightly, a pre-push run), get the branch with
+`gh pr view  --json headRefName` and filter Buildkite builds by
+branch; the MCP's `list_builds` takes a `branch:` filter, and fork branches
+are named `:` on Buildkite.
+
+## Step 2: Check annotations first
+
+**Before diving into logs**, fetch the build annotations. They contain pre-extracted error messages, stack traces, and links to known flaky test issues — this saves significant time compared to grepping through raw logs. With the MCP they arrive in `get_build_failure_summary` (or `list_annotations`); with `bk`, use the annotations recipe in `bk-fallback.md`.
+
+Besides the error itself, an error annotation (`style` `"error"`) carries
+known-issue links (Linear keys like `CPU-170`, or legacy
+`database-issues/#NNNN`) and the job's main-branch history, a flaky-test
+indicator.
+
+Things to know when reconciling annotations against jobs:
+
+- Error annotations persist from failed attempts even when a retry later
+  passed, so a build can have more error annotations than failed jobs. A
+  `bin/ci-annotate-errors` annotation's `context` field is `-error`,
+  and that job id can belong to a retried attempt that the default job
+  listing does not contain. (Other producers use other contexts, e.g.
+  `images-not-public` from the build step.)
+- Only jobs running through the mzcompose/cloudtest plugins get annotations
+  and `bin/ci-failures` rows (both come from `bin/ci-annotate-errors`). Plain
+  shell steps (lint, Security advisories, and similar checks) produce neither,
+  so a real failure there has no annotation at all. Go straight to its job
+  log.
+- Info-style annotations (" succeeded with known error logs") mean the
+  job passed and every logged error matched an open known issue; they are
+  not failures. When a job went red via "Test succeeded, but unknown errors
+  found in logs" (see Step 3), the unknown and potential-regression entries
+  in its annotation are what turned it red. The reverse also exists: a
+  failed job turns green (" would have failed with known error logs")
+  when all its logged errors match issues marked `ci-ignore-failure: true`.
+
+## Step 3: Fetch logs when needed
+
+Only fetch full logs when annotations don't provide enough detail. On PR test
+builds, read compile and lint job logs first (clippy, lint-and-rustfmt): they
+often explain every downstream failure.
+
+Fetch and search logs with the MCP's `search_logs`/`read_logs`/`tail_logs`,
+or with `bk` use the job-log recipes in `bk-fallback.md`. The log tail can be a
+red herring: some jobs print long non-error output after the actual error
+(cargo deny's dependency tree runs hundreds of lines past the advisory), so
+when the tail shows no error, search or grep for the error instead of
+tailing more.
+
+NOTE: for mzcompose-based jobs (testdrive, SQLsmith/SQLancer, platform checks,
+...) the job console log holds only the harness's output. The services' own
+output (environmentd/clusterd panics and errors) goes to `services.log` and
+similar files inside the job's log artifacts. A panic that is absent from
+the job's console log can still be the failure, so never conclude "this
+error did not happen in this run" from it alone. Check the annotations or
+`bin/ci-failures` instead, both are fed by `bin/ci-annotate-errors`, which
+scans the uploaded log artifacts (`services.log`, `run.log`, junit XML, ...)
+at the end of each job. The same scan can fail a job whose own workflow
+passed: the console log then reads `Test succeeded, but unknown errors found
+in logs, marking as failed`, and the cause is whatever the scan found in the
+artifacts, not a test failure. Alternatively, download the log artifacts and
+grep them directly.
 
 ### Artifacts
 
 Jobs upload artifacts (junit XML, service logs, coredumps, ...). Use the
-`bk artifacts` subcommands:
-
-```bash
-# All artifacts of a build (each entry has id, job_id, path)
-bk artifacts list  -p  --no-pager 2>/dev/null
-# Artifacts of a single job
-bk artifacts list  -p  --job-uuid  --no-pager 2>/dev/null
-# Download one artifact into the current directory
-bk artifacts download  --build  -p 
-```
-
-Do not use `bk api` for artifacts. The plural
-`/pipelines//builds//artifacts` endpoint works but
-silently returns only the first 30 entries, and its `.../download` endpoint
-fails to parse.
+MCP's `list_artifacts_for_build`/`list_artifacts_for_job`/`get_artifact`,
+or with `bk` use the artifacts recipes in `bk-fallback.md`.
 
 ### Shard contents
 
@@ -136,47 +204,84 @@ shows it:
 
 ```bash
 # Mapping of every sharded job to what it ran, with a link per job whose
-# `#` fragment is the `` for `bk job log` (omit links with --no-url)
+# `#` fragment is the `` for fetching job logs (omit links with --no-url)
 bin/ci-shards https://buildkite.com/materialize//builds/
 
-# What a single job ran (job link as copied from the Buildkite UI)
-bin/ci-shards 'https://buildkite.com/materialize//builds/#'
-
 # Which job(s) ran a specific item (test file, scenario, or workflow,
 # `workflow_foo_bar` function names are matched as `foo-bar`)
 bin/ci-shards https://buildkite.com/materialize//builds/ numeric.td
 ```
 
-## Step 5: Categorize failures
+## Step 4: Categorize failures
 
 Use these Materialize-specific patterns to diagnose:
 
 ### Clippy errors
-Code lint issues in changed files. Common ones: `as_conversions`, `needless_borrow`, `clone_on_ref_ptr`. Fix the code, not the lint config.
+Code lint issues in changed files. Fix the code, not the lint config.
 
 ### `check-test-flags` lint failure
 A new configuration flag was introduced but not registered in the required places:
 - `misc/python/materialize/parallel_workload/action.py` (FlipFlagsAction)
 - `misc/python/materialize/mzcompose/__init__.py` (get_variable_system_parameters / get_minimal_system_parameters / UNINTERESTING_SYSTEM_PARAMETERS)
 
-### Cargo test failures
-Read the panic message or assertion diff. Common patterns:
-- `unwrap_err() on Ok` → test expected an error but the code now succeeds
-- `assertion left == right failed` → behavioral change in output
-
 ### Testdrive cascades
 After one test crashes environmentd, all subsequent tests in that shard fail with `Name or service not known` or `connection closed`. **Only the first failure in a shard matters** — everything after it is a cascade. Look for the first `error:` or `FAIL` in the log.
 
 Testdrive shards with the same number (e.g., `testdrive-10` and `testdrive-with-alloydb-10`) run the same tests — if both fail, it's likely to be the same root cause.
 
 ### SLT failures
-Check whether it's wrong output (behavioral change) vs. connection error (crash/timeout). Wrong output means the query semantics changed.
-
-## Step 6: Summarize
-
-Group failures by **root cause**, not by job name. Typically many failing jobs share just 1-2 root causes. Present the summary as:
-
-1. **Root cause A** — description, which jobs it affects, what to fix
-2. **Root cause B** — description, which jobs it affects, what to fix
-
-Distinguish between issues that are clearly caused by the PR's changes vs. pre-existing flaky tests. The annotations often link to known flaky test issues (GitHub `database-issues` links) — use these to identify pre-existing flakes vs. regressions introduced by the PR.
+Check whether it's wrong output (behavioral change) vs. connection error (crash/timeout). Wrong output means the query semantics changed. An `InconsistentViewOutcome` failure comes from CI's `--auto-index-selects` mode; the mz-test skill explains the mechanism and the exemption lists.
+
+### Timeouts
+A `timed_out` job hit its step budget (`timeout_in_minutes` in
+`ci//pipeline.template.yml`). The annotation records only the fact
+("test timed out"), never the cause. Find the cause in the console log: the
+last completed unit of work, plus anything marked as still running when the
+job was canceled (nextest prints `SLOW [>1200.000s]` lines naming the stuck
+tests). If no such marker appears, compare wall-clock against the job's last
+passing run to tell a gradually outgrown budget from a hung test.
+
+### Rarer classes
+
+If the failure matches one of these, read `rare-failures.md` in this
+skill's directory for its triage pattern: `New regression against
+` (feature benchmark), a cargo-fuzz crash, a Miri failure, a
+limits-test failure, or `ImagesNotPublicError`.
+
+## Step 5: Summarize
+
+Group failures by **root cause**, not by job name — typically many failing
+jobs share just 1-2 root causes. Per root cause, report a description, the
+affected jobs, and what to fix.
+
+Distinguish between issues that are clearly caused by the change under test vs. pre-existing flaky tests. The annotations often link to known flaky test issues — use these to identify pre-existing flakes vs. new regressions.
+
+More ways to establish known vs new:
+
+- Check whether main already fixed it. Any build can run code that is behind
+  main: a nightly investigated the morning after, but also a fresh PR based
+  on an older main. From an up-to-date main checkout run
+  `git log ..HEAD -- ` or
+  `git log -S ''`. A fix can also sit in a not-yet-merged PR,
+  invisible to git log: look at recently opened PRs for one that already
+  addresses the failure (a PR's file list shows which of the build's root
+  causes it covers). When citing a later build as evidence
+  of a fix, confirm the specific job's state there is `passed`: a build can
+  be green because the job was `broken` and never ran.
+- Known-issue tracking lives in Linear. Annotations and `bin/ci-failures`
+  cite issue keys like `CPU-170` or `SS-361`, which resolve to
+  `https://linear.app/materializeinc/issue/`. Legacy references point at
+  GitHub `MaterializeInc/database-issues` instead.
+- Annotations and `bin/ci-failures` classify a failure three ways; for the
+  latter two, check the signature's history with `bin/ci-failures` before
+  treating the failure as new. A known-issue link means it matched an open
+  tracked issue. `"UNKNOWN ERROR"` means it matched no tracked issue, not
+  that the failure is new: a long streak under that label is a chronic
+  untracked flake worth filing. An issue reference tagged
+  `(POTENTIAL REGRESSION)` means it matched a tracked issue that is now
+  closed: a known signature with stale tracking, not by itself evidence of
+  a new regression; suggest to the user that the closed issue may need
+  reopening. The issue matching is a content heuristic and often wrong, so
+  verify that the matched issue actually describes the problem: search the
+  tracker for the error text, since the right issue may already exist open,
+  or the problem may need a totally new one.
diff --git a/.agents/skills/mz-debug-ci/bk-fallback.md b/.agents/skills/mz-debug-ci/bk-fallback.md
new file mode 100644
index 0000000000000..9756f627c39e9
--- /dev/null
+++ b/.agents/skills/mz-debug-ci/bk-fallback.md
@@ -0,0 +1,67 @@
+# bk fallback for mz-debug-ci
+
+Command recipes for sessions without the Buildkite MCP. The semantics (job
+states, annotation contents, triage rules) live in SKILL.md; only the `bk`
+mechanics live here.
+
+`bk` writes warnings to stderr, including a benign `BUILDKITE_API_TOKEN is
+overriding the credential stored for this organization` on healthy setups.
+Pipe `bk` output with `2>/dev/null`, never `2>&1`, or the noise corrupts the
+JSON.
+
+## Listing a build's failed jobs
+
+```bash
+bk api "/pipelines//builds/" --no-pager 2>/dev/null \
+  | jq -r '.jobs[] | select(.state=="failed" or .state=="timed_out") | select(.soft_failed != true) | [.state, .id, .name] | @tsv'
+```
+
+The same response carries the build's `.commit` and `.branch`. Add
+`?include_retried_jobs=true` to see the earlier attempts of retried jobs
+(exposes `.retried` and `.retry_source`).
+
+## Annotations
+
+```bash
+bk api /pipelines//builds//annotations --no-pager 2>/dev/null \
+  | jq -r '.[] | select(.style=="error") | "=== \(.context)\n\(.body_html)"' \
+  | sed -e 's/<[^>]*>//g' | grep -v '^$'
+```
+
+## Job logs
+
+```bash
+bk job log  -p  -b  --no-timestamps --no-pager 2>/dev/null | tail -100
+```
+
+For large logs, grep for the error instead of tailing. Buildkite logs are
+full of ANSI escapes (colors, plus cursor-movement and OSC sequences from
+docker pulls) and embedded carriage returns that leave doubled blank lines,
+halving grep's `-A`/`-B` reach. Normalize first:
+
+```bash
+bk job log ... 2>/dev/null | tr '\r' '\n' \
+  | sed -e 's/\x1b\[[0-9;]*[a-zA-Z]//g' -e 's/\x1b\][^\x07]*\x07//g' | grep -v '^$' \
+  | grep -B2 -A5 'error\|FAIL\|panicked'
+```
+
+## Artifacts
+
+```bash
+# All artifacts of a build (each entry has id, job_id, path)
+bk artifacts list  -p  --no-pager 2>/dev/null
+# Artifacts of a single job
+bk artifacts list  -p  --job-uuid  --no-pager 2>/dev/null
+# Download one artifact into the current directory
+bk artifacts download  --build  -p 
+```
+
+NOTE: if `bk` rejects one of these flags as unknown, the installed `bk`
+predates the April 2026 artifacts rework. Check
+`bk artifacts  --help` for the local spelling, and recommend to
+the user that they upgrade `bk`.
+
+Do not use `bk api` for artifacts. The plural
+`/pipelines//builds//artifacts` endpoint works but
+silently returns only the first 30 entries, and its `.../download` endpoint
+fails to parse.
diff --git a/.agents/skills/mz-debug-ci/rare-failures.md b/.agents/skills/mz-debug-ci/rare-failures.md
new file mode 100644
index 0000000000000..4c025116aca95
--- /dev/null
+++ b/.agents/skills/mz-debug-ci/rare-failures.md
@@ -0,0 +1,51 @@
+# Rare failure classes for mz-debug-ci
+
+Triage patterns for failure classes that come up rarely. SKILL.md's Step 4
+points here by keyword.
+
+## Feature benchmark regressions
+
+A `New regression against ` annotation shows one comparison table,
+but the harness re-runs every flagged scenario and annotates only the last
+run: grep the console log for `Run [0-9] with scenarios` and `!!YES!!` to
+see whether the scenario survived every re-run. Even surviving all re-runs
+does not prove a code cause, since agent-level speed offsets are stable
+within a job, and these scenarios trip on noise regularly, so check the
+base rate with `bin/ci-failures ''`. The decisive check is
+reproducing locally: check out the exact commit the build ran and run the
+reproducer command from the annotation. mzbuild pulls the images CI already
+published for that commit, so nothing needs to build. Pass `--release` for
+nightly and release-qualification builds (they build with `CI_LTO=1`); PR
+test builds and flag-less local runs both use the optimized profile, so
+no flag there.
+
+## cargo-fuzz crashes
+
+The fuzz target's own panic text is not in the console log; it is in
+`.log` inside the `fuzz-logs.tar.zst` build artifact, and the
+annotation names the file. For known-vs-new use
+`bin/ci-failures ''`; cargo-fuzz runs only in release
+qualification, so that is its entire history.
+
+## Limits test failures
+
+For red `Product limits` / `Instance size limits` jobs, the mz-limits-test
+skill explains the framework, its typical failure modes, and
+single-scenario local reruns.
+
+## Miri failures
+
+`:rust: Miri test (full)` fails as either `timed_out` (a test too slow under
+Miri's interpreter) or `failed` with `unsupported operation` (code Miri
+cannot execute, e.g. a foreign function). Neither is a product bug, and the
+standard reaction is not to make the code Miri-compatible: add
+`#[cfg_attr(miri, ignore)]` with a short reason to the offending test, as
+about a thousand existing tests do.
+
+## `ImagesNotPublicError`
+
+A PR adding a new publishable mzbuild image fails `:rust: Build x86_64`
+until the image's DockerHub/GHCR repos are made public, a process step
+described by the `images-not-public` annotation. The check runs only on the
+x86_64 build, so aarch64 passing while x86_64 fails is expected, not an
+arch bug.
diff --git a/.agents/skills/mz-test/SKILL.md b/.agents/skills/mz-test/SKILL.md
index dbaf40b616f34..126a70d0d9e8a 100644
--- a/.agents/skills/mz-test/SKILL.md
+++ b/.agents/skills/mz-test/SKILL.md
@@ -198,6 +198,9 @@ If a flake cannot be reproduced locally, suggest the user trigger CI runs manual
 Removing the "Target Branch" generates a random identifier, allowing parallel runs.
 10-20 runs to reproduce a flake is fine.
 
+To check whether a failure is already known and chronic in CI, see the
+mz-debug-ci skill's `bin/ci-failures` search.
+
 ## Additional logging for flaky tests
 
 Add logging through `log_filter` in the test's `mzcompose.py`:
diff --git a/Cargo.lock b/Cargo.lock
index 295f6afbb76ac..38ba6908284b4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3498,7 +3498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
 dependencies = [
  "libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -4959,9 +4959,9 @@ dependencies = [
 
 [[package]]
 name = "insta"
-version = "1.47.2"
+version = "1.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e"
+checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82"
 dependencies = [
  "console",
  "once_cell",
@@ -5032,7 +5032,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
 dependencies = [
  "hermit-abi",
  "libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -5119,7 +5119,7 @@ dependencies = [
  "portable-atomic",
  "portable-atomic-util",
  "serde_core",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -11709,7 +11709,7 @@ dependencies = [
  "errno",
  "libc",
  "linux-raw-sys 0.4.15",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -11722,7 +11722,7 @@ dependencies = [
  "errno",
  "libc",
  "linux-raw-sys 0.12.1",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -12859,10 +12859,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
 dependencies = [
  "fastrand 2.3.0",
- "getrandom 0.4.3",
+ "getrandom 0.3.3",
  "once_cell",
  "rustix 1.1.4",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
diff --git a/ci/nightly/pipeline.template.yml b/ci/nightly/pipeline.template.yml
index 622d7bf9b4bc5..f25c31d5ef852 100644
--- a/ci/nightly/pipeline.template.yml
+++ b/ci/nightly/pipeline.template.yml
@@ -168,6 +168,7 @@ steps:
           args: [--miri-full]
     agents:
       queue: hetzner-aarch64-16cpu-32gb
+    skip: "Has been creating more toil than use"
     sanitizer: skip
 
   - group: Benchmarks
diff --git a/ci/release-qualification/pipeline.template.yml b/ci/release-qualification/pipeline.template.yml
index 598044ac10241..de1c956824af9 100644
--- a/ci/release-qualification/pipeline.template.yml
+++ b/ci/release-qualification/pipeline.template.yml
@@ -278,12 +278,9 @@ steps:
           composition: cargo-fuzz
           ci_builder: nightly
           args:
-            - --profile=fruitful
+            - --profile=all
             - --max-seconds=86400
             - --wall-budget=84600
-            # Step hard-times out at 1440min (86400s). --wall-budget ends fuzzing
-            # at 84600s, leaving 1800s; cap minimize at 1200s so the corpus
-            # upload has ~600s of headroom before the kill.
             - --minimize-timeout=1200
             - --corpus-sync
     artifact_paths:
diff --git a/misc/python/materialize/cli/fmt.py b/misc/python/materialize/cli/fmt.py
index 7ea912bf98a08..9a12b7f631123 100644
--- a/misc/python/materialize/cli/fmt.py
+++ b/misc/python/materialize/cli/fmt.py
@@ -18,6 +18,12 @@
 from materialize import MZ_ROOT
 from materialize.parallel_task import TaskSpec, run_parallel
 
+# Rust sources live in more than one workspace, and `cargo metadata` only ever
+# reports the one it is pointed at. The `src/*/fuzz` cargo-fuzz crates attach to
+# the `test/cargo-fuzz` workspace, which the root workspace does not include, so
+# without a second invocation here they go unformatted entirely.
+RUST_MANIFESTS = ["Cargo.toml", "test/cargo-fuzz/Cargo.toml"]
+
 
 def main() -> int:
     parser = argparse.ArgumentParser(prog="fmt")
@@ -45,28 +51,43 @@ def _rustfmt_fn(*, check: bool):
     def run() -> tuple[bool, str]:
         ncpus = os.cpu_count() or 8
 
-        result = subprocess.run(
-            ["cargo", "metadata", "--no-deps", "--format-version=1"],
-            capture_output=True,
-            text=True,
-        )
-        if result.returncode != 0:
-            return False, result.stderr.strip()
-
-        meta = json.loads(result.stdout)
         kinds = {"lib", "bin", "bench", "test", "example", "proc-macro", "custom-build"}
-        paths = [
-            t["src_path"]
-            for pkg in meta["packages"]
-            for t in pkg["targets"]
-            if kinds & set(t["kind"])
-        ]
-        if not paths:
+        # Keyed by edition: `gen` is an identifier in 2021 but a reserved keyword
+        # in 2024, so rustfmt cannot even parse a file at the wrong edition.
+        paths_by_edition: dict[str, list[str]] = {}
+        for manifest in RUST_MANIFESTS:
+            result = subprocess.run(
+                [
+                    "cargo",
+                    "metadata",
+                    "--no-deps",
+                    "--format-version=1",
+                    f"--manifest-path={manifest}",
+                ],
+                capture_output=True,
+                text=True,
+            )
+            if result.returncode != 0:
+                return False, result.stderr.strip()
+
+            meta = json.loads(result.stdout)
+            for pkg in meta["packages"]:
+                for t in pkg["targets"]:
+                    if kinds & set(t["kind"]):
+                        paths_by_edition.setdefault(pkg["edition"], []).append(
+                            t["src_path"]
+                        )
+        if not paths_by_edition:
             return True, ""
 
         # Split into batches and run rustfmt in parallel.
-        batch_size = math.ceil(len(paths) / ncpus)
-        batches = [paths[i : i + batch_size] for i in range(0, len(paths), batch_size)]
+        batches = []
+        for edition, paths in paths_by_edition.items():
+            batch_size = math.ceil(len(paths) / ncpus)
+            batches += [
+                (edition, paths[i : i + batch_size])
+                for i in range(0, len(paths), batch_size)
+            ]
 
         cmd_base = ["rustfmt", "--config", "error_on_line_overflow=true"]
         if check:
@@ -74,11 +95,11 @@ def run() -> tuple[bool, str]:
 
         procs = [
             subprocess.Popen(
-                cmd_base + batch,
+                cmd_base + [f"--edition={edition}"] + batch,
                 stdout=subprocess.PIPE,
                 stderr=subprocess.STDOUT,
             )
-            for batch in batches
+            for edition, batch in batches
         ]
 
         all_output = []
diff --git a/misc/python/materialize/version_ancestor_overrides.py b/misc/python/materialize/version_ancestor_overrides.py
index e3da6a3d387ee..a13becfa03843 100644
--- a/misc/python/materialize/version_ancestor_overrides.py
+++ b/misc/python/materialize/version_ancestor_overrides.py
@@ -28,6 +28,12 @@ def get_ancestor_overrides_for_performance_regressions(
 
     min_ancestor_mz_version_per_commit = dict()
 
+    if scenario_class_name == "MySqlInitialLoadMultiWorkerSampled":
+        # PR#38094 (storage: Fast approximate snapshot partitioning) increased wallclock by ~30%
+        min_ancestor_mz_version_per_commit[
+            "de8eac0d1683ffd788dfba6edaa88aef7f0f1740"
+        ] = MzVersion.parse_mz("v26.39.0")
+
     if scenario_class_name == "FastPathFilterNoIndex":
         # PR#38151 (Sql-150: Temporary objects to the catalog) increased wallclock by ~10%
         min_ancestor_mz_version_per_commit[
diff --git a/rustfmt.toml b/rustfmt.toml
index f8dc60ca8e5d2..b6ab042396f4a 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,3 +1,8 @@
 edition = "2024"
+# Formatting style is repo-wide, independent of the edition a crate parses as.
+# The `src/*/fuzz` crates are still edition 2021, and `bin/fmt` overrides
+# `edition` per package so they parse at all. Without this pin they would also
+# fall back to the 2021 *style*, which sorts imports differently.
+style_edition = "2024"
 tab_spaces = 4
 merge_derives = false
diff --git a/src/avro/fuzz/Cargo.toml b/src/avro/fuzz/Cargo.toml
index 96118872559ff..6072a0f3298ec 100644
--- a/src/avro/fuzz/Cargo.toml
+++ b/src/avro/fuzz/Cargo.toml
@@ -20,6 +20,10 @@ cargo-fuzz = true
 [dependencies]
 libfuzzer-sys = "0.4"
 mz-avro = { path = ".." }
+# Version must match the root `Cargo.toml`'s. `avro_schema_parse` needs a
+# `serde_json::Value` to reach `Schema::parse`, and that value has to be the same
+# type `mz-avro` was compiled against.
+serde_json = "1.0.150"
 
 [[bin]]
 name = "reader_decode"
diff --git a/src/avro/fuzz/corpus.dict b/src/avro/fuzz/corpus.dict
index 4740480132fd9..dcf3f2b00d664 100644
--- a/src/avro/fuzz/corpus.dict
+++ b/src/avro/fuzz/corpus.dict
@@ -1,10 +1,16 @@
-# libFuzzer dictionary for the Avro reader_decode target.
+# libFuzzer dictionary for the Avro fuzz targets, resolved crate-wide by
+# `dict_for` in test/cargo-fuzz/mzcompose.py.
 #
 # An Avro object-container file starts with the 4-byte magic "Obj\x01" followed
 # by a map of metadata (notably "avro.schema" and "avro.codec") and a 16-byte
-# sync marker. Without the magic the decoder bails immediately, so the most
-# valuable token by far is the magic itself; the rest let the mutator build
-# plausible headers and schemas.
+# sync marker. Without the magic the decoder bails immediately, so the magic and
+# the schema JSON tokens let the mutator build plausible headers and schemas.
+#
+# These tokens pay off for the targets that consume their input as *bytes*:
+# `avro_schema_parse`, and `reader_decode`'s raw-bytes branch. `reader_decode`'s
+# container-building branch and `schema_resolve` instead read their input as an
+# `arbitrary::Unstructured` recipe of generator choices, where a token is just
+# more choice bytes and carries no structural signal.
 
 # Object-container magic.
 "Obj\x01"
diff --git a/src/avro/fuzz/fuzz_targets/avro_schema_parse.rs b/src/avro/fuzz/fuzz_targets/avro_schema_parse.rs
index 52cbe61c7a44c..e7970d02ebc04 100644
--- a/src/avro/fuzz/fuzz_targets/avro_schema_parse.rs
+++ b/src/avro/fuzz/fuzz_targets/avro_schema_parse.rs
@@ -16,20 +16,39 @@
 //!
 //! `schema_resolve` only parses the narrow set of schemas it generates (no
 //! logical types, no named back-references, shallow), and the decode targets
-//! cap nesting low. This one stresses the parser itself: it generates schema
-//! JSON that can nest *past* the depth limit (so the guard must fire cleanly
-//! rather than overflow the stack) and that re-references already-defined names
-//! (recursive definitions, a distinct resolution path).
+//! cap nesting low. This one stresses the parser itself: schemas that
+//! re-reference already-defined names (recursive definitions, a distinct
+//! resolution path), and nesting that straddles `MAX_SCHEMA_DEPTH`.
+//!
+//! Reaching that guard takes the *other* entry point. `Schema::from_str` runs
+//! `serde_json::from_str` first, and serde_json's own recursion limit is also
+//! 128 containers, so it rejects the schema one level before the avro guard can
+//! fire: the deepest JSON text that survives is 127 wrappers, which is avro
+//! depth 128, exactly at the limit and not over it. So the deep case goes
+//! through `Schema::parse`, which takes an already-parsed `serde_json::Value`,
+//! and the text case is bounded to nesting serde_json will accept rather than
+//! generating JSON no avro code will ever see.
 //!
 //! It also drives the parser's *naming* and *validation* paths:
 //!   * `namespace` fields and dotted/`a.b.C` names. The `FullName::from_parts`
 //!     split logic, including the documented edge case where a name has dots
 //!     *and* a `namespace` is also given (they may disagree).
-//!   * `aliases` arrays on named types, sometimes deliberately colliding with
-//!     another defined name or with the type's own name.
-//!   * structurally-valid-but-semantically-invalid schemas the parser is meant
-//!     to *reject* (not panic on): duplicate enum symbols, duplicate record
-//!     field names, decimal `scale > precision`, and `fixed` `size: 0`.
+//!   * a name that collides with an already-defined type's fullname, which
+//!     `alloc_name` must reject.
+//!   * `aliases` arrays on named types, sometimes colliding with another defined
+//!     name or with the type's own name. NOTE: parsing only *collects* aliases
+//!     into the `Name`, so a collision is indistinguishable from any other alias
+//!     here. Aliases only acquire meaning in `resolve_schemas`, which this target
+//!     does not call. They are generated to keep the attribute shape varied, not
+//!     as a validation path.
+//!   * semantically-invalid schemas, which must produce an error rather than a
+//!     panic: duplicate enum symbols, an enum `default` outside its symbols, and
+//!     `fixed` `size: 0` are all rejected. NOTE: two further shapes generated
+//!     here are *accepted* today, and are generated because the parser has to
+//!     survive them, not because it rejects them. `parse_record` has no
+//!     duplicate-field-name check, so a duplicate silently collapses in `lookup`
+//!     while both entries stay in `fields`; and `parse_bytes` catches a decimal
+//!     `scale > precision` error, logs it, and falls back to plain `bytes`.
 //! Parsing must never panic. It returns `Ok`/`Err`.
 
 #![no_main]
@@ -100,11 +119,22 @@ fn gen_named_attrs(
 /// error rather than panic.
 fn gen_invalid(u: &mut Unstructured, counter: &mut u32) -> arbitrary::Result {
     *counter += 1;
-    Ok(match u.int_in_range(0u8..=4)? {
-        // Enum with duplicate symbols.
-        0 => format!(
-            "{{\"type\":\"enum\",\"name\":\"E{counter}\",\"symbols\":[\"A\",\"B\",\"A\"]}}"
+    Ok(match u.int_in_range(0u8..=5)? {
+        // Two named types sharing a fullname. The names generated elsewhere carry
+        // a strictly increasing counter and so can never collide, which leaves
+        // `alloc_name`'s duplicate-fullname rejection unreachable; this is the one
+        // shape that reaches it. Both twins are declared bare, with no
+        // `namespace`, so the collision does not depend on how the enclosing type
+        // was named.
+        5 => format!(
+            "{{\"type\":\"record\",\"name\":\"Outer{counter}\",\"fields\":[\
+             {{\"name\":\"a\",\"type\":{{\"type\":\"record\",\"name\":\"Twin{counter}\",\"fields\":[]}}}},\
+             {{\"name\":\"b\",\"type\":{{\"type\":\"record\",\"name\":\"Twin{counter}\",\"fields\":[]}}}}]}}"
         ),
+        // Enum with duplicate symbols.
+        0 => {
+            format!("{{\"type\":\"enum\",\"name\":\"E{counter}\",\"symbols\":[\"A\",\"B\",\"A\"]}}")
+        }
         // Record with duplicate field names.
         1 => format!(
             "{{\"type\":\"record\",\"name\":\"R{counter}\",\"fields\":[\
@@ -215,14 +245,60 @@ fn gen_type(
     })
 }
 
+/// Enough array layers to straddle `MAX_SCHEMA_DEPTH` (128) from both sides.
+const MAX_WRAPPERS: u32 = 300;
+
 fn run(mut u: Unstructured) -> arbitrary::Result<()> {
     let mut counter = 0u32;
     let mut defined = Vec::new();
-    // Start deeper than MAX_SCHEMA_DEPTH (128) so the fuzzer can drive nesting
-    // past the limit and exercise the depth guard, not just shallow schemas.
-    let depth = u.int_in_range(1u32..=200)?;
+    // Bounded so the generated text always survives serde_json, which refuses
+    // the 128th nested container. `depth` counts generator levels, not
+    // containers, and a level spends up to three of them (a record is
+    // `{record}` → `[fields]` → `{field}` before recursing), with `gen_invalid`
+    // spending five at the leaf. So a schema nests at most `3 * depth + 2`
+    // containers, and 40 keeps the worst case at 122. Deep nesting is the
+    // wrapped path's job below, which is not subject to this limit at all.
+    let depth = u.int_in_range(1u32..=40)?;
     let schema = gen_type(&mut u, &mut counter, &mut defined, depth)?;
-    let _ = Schema::from_str(&schema);
+
+    // Half the inputs go through the text entry point unchanged. The other half
+    // get wrapped to straddle `MAX_SCHEMA_DEPTH`, which is only reachable behind
+    // `Schema::parse`. Splitting rather than doing both keeps the cost at one JSON
+    // parse plus one schema parse per input, the same as parsing the text alone.
+    let wrappers = if u.int_in_range(0u8..=1)? == 0 {
+        0
+    } else {
+        u.int_in_range(1u32..=MAX_WRAPPERS)?
+    };
+    if wrappers == 0 {
+        let _ = Schema::from_str(&schema);
+        return Ok(());
+    }
+
+    // Every generated schema is well-formed JSON by construction (all
+    // interpolated names are `R`/`E`/`F`/`AnAlias`/`Twin`), so a
+    // parse failure here is a generator bug, and swallowing it would silently
+    // skip the depth-guard half of this target.
+    let mut value: serde_json::Value = serde_json::from_str(&schema)
+        .unwrap_or_else(|e| panic!("generated schema {schema} must be valid JSON: {e}"));
+
+    // `Schema::parse` takes an already-parsed value and so is not subject to
+    // serde_json's limit, which is what makes the guard reachable at all. The
+    // wrapper count falls on either side of it, exercising the accepted depths,
+    // the rejection, and the `self.depth` decrement on the guard's error path.
+    // Built by moving `value` into each new layer. `serde_json::json!` would
+    // deep-clone it instead (its leaf case goes through `to_value`), making the
+    // wrapping quadratic in the layer count.
+    for _ in 0..wrappers {
+        let mut layer = serde_json::Map::new();
+        layer.insert(
+            "type".to_string(),
+            serde_json::Value::String("array".to_string()),
+        );
+        layer.insert("items".to_string(), value);
+        value = serde_json::Value::Object(layer);
+    }
+    let _ = Schema::parse(&value);
     Ok(())
 }
 
diff --git a/src/avro/fuzz/fuzz_targets/reader_decode.rs b/src/avro/fuzz/fuzz_targets/reader_decode.rs
index f64f3b34101de..5c308517db63c 100644
--- a/src/avro/fuzz/fuzz_targets/reader_decode.rs
+++ b/src/avro/fuzz/fuzz_targets/reader_decode.rs
@@ -32,12 +32,22 @@
 //!   * an unrecognized codec string: the `UnrecognizedCodec` header-parse
 //!     error. (`snappy` is gated behind a Cargo feature that this fuzz crate
 //!     does not enable, so it is intentionally not generated.)
-//! We also corrupt the declared block framing: occasionally the object-count or
-//! byte-size prefix is replaced with a huge or wildly inconsistent value, which
-//! must be caught by the `safe_len` allocation guard rather than spinning or
-//! over-allocating. And we still hit the structural error paths, occasionally a
-//! corrupted trailing sync marker (the mismatch path) or a whole-file truncation
-//! mid-block (the short-read path). Decoding must never panic any of these ways.
+//! We also corrupt the declared block framing. `read_block_next` runs both the
+//! object-count and the byte-size prefix through the `safe_len` allocation guard
+//! before it touches the payload, so a magnitude past `MAX_ALLOCATION_BYTES`
+//! never reaches anything downstream of that guard. The lies therefore straddle
+//! it deliberately:
+//!   * a count or size past the budget, which `safe_len` must reject rather than
+//!     attempt a giant allocation.
+//!   * a count over the objects the block actually encodes but inside the
+//!     budget, which `safe_len` accepts. `bound_block_object_count` must then
+//!     reject any count the payload could not hold, and one it accepts must
+//!     terminate against the bytes present instead of spinning.
+//!   * a size over the bytes the block actually carries but inside the budget,
+//!     so `fill_buf` runs and takes the short-read path.
+//! And we still hit the structural error paths, occasionally a corrupted trailing
+//! sync marker (the mismatch path) or a whole-file truncation mid-block (another
+//! short read). Decoding must never panic any of these ways.
 
 #![no_main]
 
@@ -45,6 +55,12 @@ use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
 use mz_avro::{Codec, Reader};
 
+/// The ceiling `util::safe_len` applies to every length read off the wire, from
+/// the private `mz_avro::util::MAX_ALLOCATION_BYTES`. Only used to pick the
+/// magnitude of a corrupted block prefix, so drift merely shifts which side of
+/// the guard a generated prefix lands on.
+const MAX_ALLOCATION_BYTES: i64 = 512 * 1024 * 1024;
+
 /// A generated Avro type. Structured (not straight-to-JSON) so the object
 /// encoder can walk the same type. An array is one item type in the schema but
 /// N item values in a block.
@@ -82,7 +98,11 @@ fn gen_ty(u: &mut Unstructured, counter: &mut u32, depth: u32) -> arbitrary::Res
         7 => Ty::Bytes,
         8 => {
             *counter += 1;
-            let size = u.int_in_range(0u32..=24)?;
+            // Size 0 is rejected outright ("Fixed values require a positive size
+            // attribute"), which would fail the header parse and discard the
+            // input before a single block decodes. `avro_schema_parse` covers
+            // that rejection deliberately.
+            let size = u.int_in_range(1u32..=24)?;
             Ty::Fixed(*counter, size)
         }
         9 => {
@@ -101,7 +121,22 @@ fn gen_ty(u: &mut Unstructured, counter: &mut u32, depth: u32) -> arbitrary::Res
         }
         11 => Ty::Array(Box::new(gen_ty(u, counter, depth - 1)?)),
         12 => Ty::Map(Box::new(gen_ty(u, counter, depth - 1)?)),
-        _ => Ty::Nullable(Box::new(gen_ty(u, counter, depth - 1)?)),
+        _ => {
+            // `Ty::Nullable` is the only union this target emits, and the parser
+            // rejects both `["null","null"]` and a union nested directly inside a
+            // union. Flatten a nested nullable and swap a bare `null` for a type
+            // that keeps the union valid, so the header still parses. Those two
+            // rejections belong to `avro_schema_parse`. Here they would only
+            // discard the input before any block decodes.
+            let mut inner = gen_ty(u, counter, depth - 1)?;
+            while let Ty::Nullable(nested) = inner {
+                inner = *nested;
+            }
+            if matches!(inner, Ty::Null) {
+                inner = Ty::Long;
+            }
+            Ty::Nullable(Box::new(inner))
+        }
     })
 }
 
@@ -115,14 +150,16 @@ fn ty_to_json(ty: &Ty, out: &mut String) {
         Ty::Double => out.push_str("\"double\""),
         Ty::String => out.push_str("\"string\""),
         Ty::Bytes => out.push_str("\"bytes\""),
-        Ty::Fixed(n, size) => {
-            out.push_str(&format!("{{\"type\":\"fixed\",\"name\":\"F{n}\",\"size\":{size}}}"))
-        }
+        Ty::Fixed(n, size) => out.push_str(&format!(
+            "{{\"type\":\"fixed\",\"name\":\"F{n}\",\"size\":{size}}}"
+        )),
         Ty::Enum(n) => out.push_str(&format!(
             "{{\"type\":\"enum\",\"name\":\"E{n}\",\"symbols\":[\"A\",\"B\",\"C\"]}}"
         )),
         Ty::Record(n, fields) => {
-            out.push_str(&format!("{{\"type\":\"record\",\"name\":\"R{n}\",\"fields\":["));
+            out.push_str(&format!(
+                "{{\"type\":\"record\",\"name\":\"R{n}\",\"fields\":["
+            ));
             for (i, f) in fields.iter().enumerate() {
                 if i > 0 {
                     out.push(',');
@@ -332,20 +369,36 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
         }
 
         // The block framing prefixes. Usually honest, but occasionally we lie
-        // about the object count or the byte size, including absurdly large or
-        // negative (→ huge `usize`) values, to exercise the `safe_len` guard
-        // and the short-read handling, neither of which may panic.
+        // about the object count or the byte size. `read_block_next` runs both
+        // prefixes through `safe_len` before touching the payload, so a
+        // magnitude past `MAX_ALLOCATION_BYTES` only ever reaches that guard.
+        // The lies below therefore straddle it deliberately: over the budget to
+        // exercise the guard, and over the *encoded data* but inside the budget
+        // to exercise what the reader does with a framing it accepted.
         let (count_prefix, size_prefix) = match u.int_in_range(0u8..=9)? {
             // Honest framing (the common case).
             0..=6 => (nobj, objs.len() as i64),
-            // Huge object count with the real byte size: the reader must cap
-            // the count via `safe_len` instead of trying to decode billions.
-            7 => (u.int_in_range(1i64 << 40..=i64::MAX)?, objs.len() as i64),
+            7 => match u.int_in_range(0u8..=3)? {
+                // More objects than the block encodes, drawn across the whole
+                // range `safe_len` accepts. Two further guards live in here and
+                // both must hold without spinning: `bound_block_object_count`
+                // rejects a count the payload could not hold, and a count it
+                // accepts has to terminate against the bytes actually present.
+                0..=2 => (
+                    u.int_in_range(nobj + 1..=MAX_ALLOCATION_BYTES)?,
+                    objs.len() as i64,
+                ),
+                // Past the budget: `safe_len` must reject before the block bound
+                // is ever consulted.
+                _ => (u.int_in_range(1i64 << 40..=i64::MAX)?, objs.len() as i64),
+            },
             // Negative byte size (wraps to an enormous `usize`): `safe_len`
             // must reject it rather than attempt a giant allocation.
             8 => (nobj, -u.int_in_range(1i64..=i64::MAX)?),
-            // Byte size far larger than the bytes actually present → short read.
-            _ => (nobj, objs.len() as i64 + (1i64 << 30)),
+            // More bytes than the block carries, but a size `safe_len` accepts,
+            // so `fill_buf` runs and takes the short-read path. A magnitude over
+            // the budget would stop at the guard and never get there.
+            _ => (nobj, objs.len() as i64 + u.int_in_range(1i64..=1i64 << 20)?),
         };
         encode_long(count_prefix, &mut out);
         encode_long(size_prefix, &mut out);
@@ -360,7 +413,7 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
     }
 
     // Occasionally truncate the whole file mid-block (short-read path).
-    if !out.is_empty() && u.int_in_range(0u8..=7)? == 0 {
+    if u.int_in_range(0u8..=7)? == 0 {
         let keep = u.int_in_range(0usize..=out.len())?;
         out.truncate(keep);
     }
diff --git a/src/avro/fuzz/fuzz_targets/schema_resolve.rs b/src/avro/fuzz/fuzz_targets/schema_resolve.rs
index e0d555ba5d537..73c61937eb0ef 100644
--- a/src/avro/fuzz/fuzz_targets/schema_resolve.rs
+++ b/src/avro/fuzz/fuzz_targets/schema_resolve.rs
@@ -30,8 +30,16 @@
 //!     trailing field (absent from the writer) with a JSON `default`, driving
 //!     the "reader field not in writer, use default" branch in `resolve_named`.
 //!   * union matching. Multi-variant unions whose variants the resolver must
-//!     match up by type/name across writer and reader.
-//!   * enums with a `default` symbol.
+//!     match up across writer and reader, by type for the primitive variants
+//!     and by *name* for the record/enum/fixed ones. The reader rendering also
+//!     sometimes collapses a union down to the one variant the encoder
+//!     expresses (`ResolveUnionConcrete`) and sometimes wraps a concrete node
+//!     in a union (`ResolveConcreteUnion`), so all three union arms of
+//!     `SchemaResolver::resolve` are reachable, not just union-against-union.
+//!   * enums with a `default` symbol, whose reader rendering also drops
+//!     trailing symbols. A dropped symbol is what makes the `default`
+//!     load-bearing: it becomes an `Err` entry in the resolved enum that decode
+//!     substitutes the default for.
 //! We resolve writer-against-itself (identity), and both cross-directions.
 //!
 //! A panic is not the only failure mode, though. `resolve_schemas` can return
@@ -44,16 +52,25 @@
 //! sees nothing wrong. So beyond requiring no panic, we add a *decode* oracle:
 //! the reader rendering only ever widens the writer, so every node, every
 //! union branch included, has a valid reader target, and decoding a
-//! writer-encoded value through the writer→reader resolved schema MUST succeed.
-//! A deferred mismatch turns that decode into an error, which this target
-//! treats as a finding.
+//! writer-encoded value through the writer→reader resolved schema MUST succeed
+//! and MUST consume every byte the writer wrote. A deferred mismatch turns that
+//! decode into an error, which this target treats as a finding.
+//!
+//! Because the reader rendering only widens, both renderings are valid schema
+//! JSON, identity resolution succeeds, and writer→reader resolution succeeds,
+//! all by construction. Those are `expect`s rather than `if let Ok(..)` guards
+//! on purpose. A promotion that regresses at a *non-union* position (a record
+//! field, an array item, a map value) surfaces as a top-level `Err` from
+//! `resolve_schemas`, not as a deferred per-variant `Err`, so a guard there
+//! would switch the decode oracle off for exactly the inputs that found a bug.
+//! Only the narrowing reader→writer direction may legitimately fail.
 
 #![no_main]
 
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
 use mz_avro::schema::resolve_schemas;
-use mz_avro::{from_avro_datum, Schema};
+use mz_avro::{Schema, from_avro_datum};
 
 /// One of the primitive Avro types, ordered by promotability so the reader
 /// rendering can pick a "wider" target. `int` ⊑ `long` ⊑ `float` ⊑ `double`.
@@ -69,7 +86,16 @@ enum Shape {
     /// A primitive that has no promotion (rendered identically on both sides).
     OtherPrim(&'static str),
     /// `[..]` union with N>=1 variants.
-    Union(Vec),
+    Union {
+        variants: Vec,
+        /// The variant the encoder expresses on the wire. Drawn from the fuzz
+        /// input: decoding a `ResolveUnionUnion` only ever consults the
+        /// `permutation` entry for the encoded index, so a branch fixed by the
+        /// generator would leave every other entry's deferred `Err` unobserved.
+        branch: usize,
+        /// Whether the reader rendering drops the union down to `branch` alone.
+        reader_collapse: bool,
+    },
     Array(Box),
     Map(Box),
     Record {
@@ -82,6 +108,10 @@ enum Shape {
         name: u32,
         /// Whether the reader rendering gives the enum a `default` symbol.
         reader_default: bool,
+        /// How many trailing symbols the reader rendering omits. Only ever
+        /// nonzero when `reader_default` is set: dropping a symbol the writer
+        /// can express is a widening exactly because the default catches it.
+        reader_dropped: u8,
     },
     Fixed {
         name: u32,
@@ -119,17 +149,42 @@ fn gen_shape(u: &mut Unstructured, counter: &mut u32, depth: u32) -> arbitrary::
             if variants.is_empty() {
                 variants.push(Shape::Promotable(0));
             }
-            Shape::Union(variants)
+            // Named variants get distinct names, and a union's duplicate check
+            // keys named variants by name, so any number of them is valid even
+            // when several are the same kind of named type. They are the only
+            // way to reach the name-keyed side of the resolver's variant
+            // matching.
+            for _ in 0..u.int_in_range(0u8..=2)? {
+                variants.push(gen_named(u, counter, depth - 1)?);
+            }
+            let branch = u.int_in_range(0..=variants.len() - 1)?;
+            Shape::Union {
+                variants,
+                branch,
+                // A starved `Unstructured` answers every `int_in_range` with the
+                // low end, so testing against the high end keeps collapsing
+                // rare rather than universal once the input runs out.
+                reader_collapse: u.int_in_range(0u8..=3)? == 3,
+            }
         }
         3 => Shape::Array(Box::new(gen_shape(u, counter, depth - 1)?)),
         4 => Shape::Map(Box::new(gen_shape(u, counter, depth - 1)?)),
-        5 | 6 => {
-            *counter += 1;
-            let name = *counter;
+        _ => gen_named(u, counter, depth - 1)?,
+    })
+}
+
+/// Generate one *named* shape. Named types are what the resolver matches up by
+/// name rather than by type, both at ordinary positions and as union variants.
+fn gen_named(u: &mut Unstructured, counter: &mut u32, depth: u32) -> arbitrary::Result {
+    let kind = u.int_in_range(0u8..=3)?;
+    *counter += 1;
+    let name = *counter;
+    Ok(match kind {
+        0 | 1 => {
             let n = u.int_in_range(0u8..=3)?;
             let mut fields = Vec::with_capacity(n.into());
             for _ in 0..n {
-                fields.push(gen_shape(u, counter, depth - 1)?);
+                fields.push(gen_shape(u, counter, depth)?);
             }
             Shape::Record {
                 name,
@@ -137,20 +192,22 @@ fn gen_shape(u: &mut Unstructured, counter: &mut u32, depth: u32) -> arbitrary::
                 reader_extra_default: u.int_in_range(0u8..=1)? == 0,
             }
         }
-        7 => {
-            *counter += 1;
+        2 => {
+            let reader_default = u.int_in_range(0u8..=1)? == 0;
             Shape::Enum {
-                name: *counter,
-                reader_default: u.int_in_range(0u8..=1)? == 0,
-            }
-        }
-        _ => {
-            *counter += 1;
-            Shape::Fixed {
-                name: *counter,
-                size: u.int_in_range(1u8..=16)?,
+                name,
+                reader_default,
+                reader_dropped: if reader_default {
+                    u.int_in_range(0u8..=2)?
+                } else {
+                    0
+                },
             }
         }
+        _ => Shape::Fixed {
+            name,
+            size: u.int_in_range(1u8..=16)?,
+        },
     })
 }
 
@@ -169,7 +226,7 @@ fn render_writer(shape: &Shape, out: &mut String) {
             out.push_str(p);
             out.push('"');
         }
-        Shape::Union(variants) => {
+        Shape::Union { variants, .. } => {
             out.push('[');
             for (i, v) in variants.iter().enumerate() {
                 if i > 0 {
@@ -218,13 +275,33 @@ fn render_writer(shape: &Shape, out: &mut String) {
 
 /// Render the *reader* version of `shape`: widens each promotable primitive to
 /// a (fuzz-chosen) wider type on the promotion chain, appends a defaulted
-/// `extra` record field, and gives enums a `default` symbol. These are all the
+/// `extra` record field, drops trailing enum symbols in favour of a `default`,
+/// and rewrites unions into and out of concrete types. These are all the
 /// schema-evolution shapes `resolve_schemas` handles.
+///
+/// `in_union` says whether `shape` sits directly inside a union, which forbids
+/// wrapping it in another one.
 fn render_reader_promoted(
     u: &mut Unstructured,
     shape: &Shape,
+    in_union: bool,
     out: &mut String,
 ) -> arbitrary::Result<()> {
+    // Reading a concrete writer node as a union that contains a match for it is
+    // a widening, and the only way to reach `ResolveConcreteUnion`. Avro rejects
+    // a union directly inside a union, and `["null","null"]` is a duplicate
+    // variant, hence the two exclusions. Testing against the high end of the
+    // range keeps wrapping rare once the input is exhausted rather than
+    // wrapping every single node.
+    if !in_union
+        && !matches!(shape, Shape::Union { .. } | Shape::OtherPrim("null"))
+        && u.int_in_range(0u8..=3)? == 3
+    {
+        out.push_str("[\"null\",");
+        render_reader_promoted(u, shape, true, out)?;
+        out.push(']');
+        return Ok(());
+    }
     match shape {
         Shape::Promotable(idx) => {
             // Choose a target at or after `idx` on the chain, a valid
@@ -239,18 +316,35 @@ fn render_reader_promoted(
             out.push_str(p);
             out.push('"');
         }
-        Shape::Union(variants) => {
+        Shape::Union {
+            variants,
+            branch,
+            reader_collapse,
+        } if *reader_collapse => {
+            // Drop the union down to the single variant the encoder expresses,
+            // so a union writer resolves against a concrete reader
+            // (`ResolveUnionConcrete`). That variant is rendered in its *writer*
+            // form on purpose. Resolution records the one writer variant that
+            // matches the reader's concrete type, and decoding then rejects any
+            // other encoded index. Since a union's variants are unique per type
+            // and per name, an exact rendering is what guarantees the match lands
+            // on `branch`. A widened one can select a different variant, turning
+            // a correct decode into a wrong-index error: `["int","long"]` with
+            // the `long` branch encoded, read as `"float"`, matches `int`, which
+            // comes first and promotes to `float` just as well.
+            render_writer(&variants[*branch], out);
+        }
+        Shape::Union { variants, .. } => {
             // Widen the promotable variants while keeping their types distinct.
             // Widening each independently could collapse two variants onto the
             // same type (`[int, long]` becoming `[long, long]`), which Avro
-            // rejects as a duplicate union type. That makes the whole reader
-            // schema unparseable, so the writer->reader decode oracle below is
-            // silently skipped for this input. We emit the non-promotable
-            // variants (only `null`, per `gen_shape`) first, then the
-            // promotables in ascending source order with strictly increasing
-            // targets. Reserving one chain slot for every later variant keeps a
-            // valid assignment reachable, since PROMO_CHAIN has one slot per
-            // possible source index.
+            // rejects as a duplicate union type, and an unparseable reader
+            // rendering fails the parse assertion in `run`. We emit the
+            // non-promotable variants (`null` and the named ones) first, then
+            // the promotables in ascending source order with strictly
+            // increasing targets. Reserving one chain slot for every later
+            // variant keeps a valid assignment reachable, since PROMO_CHAIN has
+            // one slot per possible source index.
             out.push('[');
             let mut written = 0;
             for v in variants
@@ -260,7 +354,7 @@ fn render_reader_promoted(
                 if written > 0 {
                     out.push(',');
                 }
-                render_reader_promoted(u, v, out)?;
+                render_reader_promoted(u, v, true, out)?;
                 written += 1;
             }
             let mut promo: Vec = variants
@@ -290,12 +384,12 @@ fn render_reader_promoted(
         }
         Shape::Array(item) => {
             out.push_str("{\"type\":\"array\",\"items\":");
-            render_reader_promoted(u, item, out)?;
+            render_reader_promoted(u, item, false, out)?;
             out.push('}');
         }
         Shape::Map(values) => {
             out.push_str("{\"type\":\"map\",\"values\":");
-            render_reader_promoted(u, values, out)?;
+            render_reader_promoted(u, values, false, out)?;
             out.push('}');
         }
         Shape::Record {
@@ -311,7 +405,7 @@ fn render_reader_promoted(
                     out.push(',');
                 }
                 out.push_str(&format!("{{\"name\":\"f{i}\",\"type\":"));
-                render_reader_promoted(u, f, out)?;
+                render_reader_promoted(u, f, false, out)?;
                 out.push('}');
             }
             if *reader_extra_default {
@@ -325,9 +419,18 @@ fn render_reader_promoted(
         Shape::Enum {
             name,
             reader_default,
+            reader_dropped,
         } => {
+            // Symbols are dropped from the tail, which keeps `A` around as the
+            // default. A writer symbol the reader lacks becomes an `Err` entry
+            // in the resolved enum, and decoding it substitutes the default.
+            let symbols = match *reader_dropped {
+                0 => "\"A\",\"B\",\"C\"",
+                1 => "\"A\",\"B\"",
+                _ => "\"A\"",
+            };
             out.push_str(&format!(
-                "{{\"type\":\"enum\",\"name\":\"N{name}\",\"symbols\":[\"A\",\"B\",\"C\"]"
+                "{{\"type\":\"enum\",\"name\":\"N{name}\",\"symbols\":[{symbols}]"
             ));
             if *reader_default {
                 out.push_str(",\"default\":\"A\"");
@@ -365,11 +468,11 @@ fn encode_blob(bytes: &[u8], out: &mut Vec) {
 
 /// Avro-binary-encode one value matching the *writer* rendering of `shape`
 /// (i.e. `render_writer`'s wire format), so it can be decoded back through a
-/// resolved schema. For unions we deliberately pick a *promotable* branch:
-/// that is the branch whose resolved decode walks the numeric-promotion path,
-/// the exact spot where #37087 deferred a union match failure into the resolved
-/// schema and re-raised it here at decode time. Every generated union has at
-/// least one promotable variant, so the search never falls back.
+/// resolved schema. A union is encoded at its `Shape::Union::branch`, which the
+/// generator drew from the fuzz input: decoding consults only the resolved
+/// permutation entry for the encoded index, so a deferred `Err` parked in any
+/// other entry, the failure mode of #37087, is invisible unless the branch
+/// varies.
 fn encode_writer_value(
     u: &mut Unstructured,
     shape: &Shape,
@@ -396,13 +499,11 @@ fn encode_writer_value(
                 encode_blob(&b, out);
             }
         },
-        Shape::Union(variants) => {
-            let branch = variants
-                .iter()
-                .position(|v| matches!(v, Shape::Promotable(_)))
-                .unwrap_or(0);
-            encode_long(branch as i64, out);
-            encode_writer_value(u, &variants[branch], out)?;
+        Shape::Union {
+            variants, branch, ..
+        } => {
+            encode_long(*branch as i64, out);
+            encode_writer_value(u, &variants[*branch], out)?;
         }
         Shape::Array(item) => {
             let n = u.int_in_range(0i64..=3)?;
@@ -446,6 +547,24 @@ fn encode_writer_value(
     Ok(())
 }
 
+/// Decode `value` through `resolved`, requiring both that the decode succeeds
+/// and that it consumes every byte the writer wrote. Leftover bytes mean the
+/// resolved schema drove the decoder over less than the writer's value, for
+/// instance a promotion of the wrong width or a field the resolver dropped
+/// without skipping its bytes, which the tail of a record would otherwise hide.
+fn decode_fully(resolved: &Schema, value: &[u8], what: &str) {
+    let mut cursor = value;
+    match from_avro_datum(resolved, &mut cursor) {
+        Ok(_) => assert!(
+            cursor.is_empty(),
+            "decoding the writer's bytes through the {what} left {} of {} bytes unconsumed",
+            cursor.len(),
+            value.len(),
+        ),
+        Err(e) => panic!("decoding the writer's bytes through the {what} failed: {e}"),
+    }
+}
+
 fn run(mut u: Unstructured) -> arbitrary::Result<()> {
     let mut counter = 0u32;
     // The top level of an OCF/registry schema is virtually always a record.
@@ -465,11 +584,17 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
     let mut writer_json = String::new();
     render_writer(&shape, &mut writer_json);
     let mut reader_json = String::new();
-    render_reader_promoted(&mut u, &shape, &mut reader_json)?;
+    render_reader_promoted(&mut u, &shape, false, &mut reader_json)?;
 
-    let Ok(writer) = writer_json.parse::() else {
-        return Ok(());
-    };
+    // Both renderings are valid schema JSON by construction, so a parse failure
+    // is a generator bug. Reporting it beats skipping the input, which would
+    // turn the decode oracle below off without a trace.
+    let writer = writer_json
+        .parse::()
+        .unwrap_or_else(|e| panic!("writer rendering {writer_json} must parse: {e}"));
+    let reader = reader_json
+        .parse::()
+        .unwrap_or_else(|e| panic!("reader rendering {reader_json} must parse: {e}"));
 
     // Encode one value against the writer rendering. We decode it back through
     // the resolved schemas below. The writer only ever widens into the reader,
@@ -481,28 +606,38 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
     let mut writer_value = Vec::new();
     encode_writer_value(&mut u, &shape, &mut writer_value)?;
 
-    // Identity resolution must succeed and decode the writer's own bytes.
-    if let Ok(resolved) = resolve_schemas(&writer, &writer) {
-        from_avro_datum(&resolved, &mut &writer_value[..])
-            .expect("decode through identity-resolved schema must succeed");
-    }
-    if let Ok(reader) = reader_json.parse::() {
-        // Writer→reader is the *widening* direction: it hits the promotion /
-        // default / union-match branches, must resolve, and the resolved schema
-        // must decode the writer's bytes. A deferred promotion mismatch turns
-        // this `expect` into the fuzzer's signal.
-        if let Ok(resolved) = resolve_schemas(&writer, &reader) {
-            from_avro_datum(&resolved, &mut &writer_value[..]).expect(
-                "decode through writer→reader resolved schema must succeed; a failure means \
-                 resolution deferred a fixable mismatch into the resolved schema (see #37087)",
-            );
-        }
-        // The reverse (reader→writer) *narrows*, so its resolution may
-        // legitimately fail or defer a genuine mismatch. We only require that
-        // neither direction panics.
-        let _ = resolve_schemas(&reader, &writer);
-        let _ = resolve_schemas(&reader, &reader);
-    }
+    // Identity resolution is the control: a schema always resolves against
+    // itself, and the resolved form always decodes what the writer wrote.
+    let resolved = resolve_schemas(&writer, &writer)
+        .unwrap_or_else(|e| panic!("identity resolution of {writer_json} must succeed: {e}"));
+    decode_fully(
+        &resolved,
+        &writer_value,
+        &format!("identity resolution of {writer_json}"),
+    );
+
+    // Writer→reader is the *widening* direction: it hits the promotion /
+    // default / union-match branches, must resolve, and the resolved schema must
+    // decode the writer's bytes. A promotion the resolver rejects outright fails
+    // the `resolve_schemas` assertion, and one it accepts but defers into the
+    // resolved schema fails the decode.
+    let resolved = resolve_schemas(&writer, &reader).unwrap_or_else(|e| {
+        panic!("the reader rendering only widens, so resolving {writer_json} against {reader_json} must succeed: {e}")
+    });
+    decode_fully(
+        &resolved,
+        &writer_value,
+        &format!("resolution of {writer_json} against {reader_json}"),
+    );
+
+    // The reverse (reader→writer) *narrows*, so its resolution may legitimately
+    // fail or defer a genuine mismatch. We only require that it does not panic.
+    // It is also the direction that reaches the no-match error paths of the
+    // asymmetric union arms: a node the reader wrapped and widened, say an `int`
+    // read as `["null","double"]`, has no match in reverse, because `double`
+    // does not narrow back to `int`.
+    let _ = resolve_schemas(&reader, &writer);
+    let _ = resolve_schemas(&reader, &reader);
     Ok(())
 }
 
diff --git a/src/avro/fuzz/prepare-corpus.sh b/src/avro/fuzz/prepare-corpus.sh
index cfa2c6df73569..e2edcac2bef16 100755
--- a/src/avro/fuzz/prepare-corpus.sh
+++ b/src/avro/fuzz/prepare-corpus.sh
@@ -13,6 +13,14 @@
 # container files so the fuzzer doesn't waste cycles bouncing off the
 # magic-header check. libFuzzer mutates these into deeper structural
 # variants while still hitting real decoder code paths.
+#
+# `reader_decode` does not read its input as a container file. It reads it as an
+# `arbitrary::Unstructured` recipe, and only the branch selected by the first
+# byte feeds the remaining bytes to `Reader` verbatim. A container file dropped
+# in as-is is therefore consumed as generator choices and never reaches the
+# magic-header check at all, so each seed is prefixed with the byte that selects
+# that raw branch. `int_in_range(0..=3)` takes one byte from the front and
+# returns it modulo 4, so a leading NUL selects branch 0.
 
 set -euo pipefail
 
@@ -20,7 +28,8 @@ cd "$(dirname "$0")"
 
 mkdir -p corpus/reader_decode
 find corpus/reader_decode -maxdepth 1 -name 'seed_*.avro' -delete
-cp ../benches/quickstop-null.avro corpus/reader_decode/seed_01_quickstop_null.avro
+{ printf '\0'; cat ../benches/quickstop-null.avro; } \
+    > corpus/reader_decode/seed_01_quickstop_null.avro
 
 echo "Seeded:"
 for d in corpus/*/; do
diff --git a/src/avro/src/schema.rs b/src/avro/src/schema.rs
index ff1a7c42cdecb..68c51006a5715 100644
--- a/src/avro/src/schema.rs
+++ b/src/avro/src/schema.rs
@@ -2503,6 +2503,49 @@ mod tests {
 
     use super::*;
 
+    /// Wrap `"int"` in `n` layers of `{"type":"array","items":…}`.
+    fn nest_arrays(n: usize) -> Value {
+        let mut value = Value::String("int".to_string());
+        for _ in 0..n {
+            let mut layer = Map::new();
+            layer.insert("type".to_string(), Value::String("array".to_string()));
+            layer.insert("items".to_string(), value);
+            value = Value::Object(layer);
+        }
+        value
+    }
+
+    #[mz_ore::test]
+    fn parse_rejects_nesting_past_the_depth_limit() {
+        // `MAX_SCHEMA_DEPTH` counts `parse_inner` levels, and the innermost leaf
+        // costs a level without being a JSON container, so `n` wrappers reach
+        // depth `n + 1`.
+        assert_ok!(Schema::parse(&nest_arrays(MAX_SCHEMA_DEPTH - 1)));
+        let err = Schema::parse(&nest_arrays(MAX_SCHEMA_DEPTH))
+            .expect_err("nesting past the limit must be rejected");
+        assert!(
+            err.to_string()
+                .contains(&format!("nesting depth exceeds limit {MAX_SCHEMA_DEPTH}")),
+            "unexpected error: {err}"
+        );
+    }
+
+    #[mz_ore::test]
+    fn from_str_cannot_reach_the_depth_limit() {
+        // Why the test above builds a `Value` rather than parsing text: `from_str`
+        // runs serde_json first, whose own recursion limit is also 128
+        // containers, so it rejects the schema one level before the guard could
+        // fire. A depth-guard test written against text would be asserting
+        // serde_json's error, not ours. If this ever fails, the guard became
+        // reachable from text and deserves a test there too.
+        let text = serde_json::to_string(&nest_arrays(MAX_SCHEMA_DEPTH)).unwrap();
+        let err = Schema::from_str(&text).expect_err("serde_json must reject this depth");
+        assert!(
+            err.to_string().contains("recursion limit exceeded"),
+            "unexpected error: {err}"
+        );
+    }
+
     fn check_schema(schema: &str, expected: SchemaPiece) {
         let schema = Schema::from_str(schema).unwrap();
         assert_eq!(&expected, schema.top_node().inner);
diff --git a/src/catalog-protos/fuzz/Cargo.toml b/src/catalog-protos/fuzz/Cargo.toml
index 09c93c65098da..4b8cb63def46d 100644
--- a/src/catalog-protos/fuzz/Cargo.toml
+++ b/src/catalog-protos/fuzz/Cargo.toml
@@ -16,6 +16,10 @@ cargo-fuzz = true
 [dependencies]
 libfuzzer-sys = "0.4"
 mz-catalog-protos = { path = "..", features = ["proptest"] }
+# For `Jsonb`, so the oracle can assert the round trip the durable catalog
+# actually performs (`StateUpdateKindJson` packs the serde value into a `Row`)
+# rather than only the JSON text form, which never touches that stage.
+mz-repr = { path = "../../repr" }
 proptest = "1.11.0"
 serde = "1.0.219"
 serde_json = "1.0.150"
diff --git a/src/catalog-protos/fuzz/catalog_objects_serde_roundtrip.dict b/src/catalog-protos/fuzz/catalog_objects_serde_roundtrip.dict
new file mode 100644
index 0000000000000..f9c640a620148
--- /dev/null
+++ b/src/catalog-protos/fuzz/catalog_objects_serde_roundtrip.dict
@@ -0,0 +1,100 @@
+# libFuzzer dictionary for the catalog_objects_serde_roundtrip target.
+#
+# The raw-bytes arm feeds the input to `serde_json::from_slice::`, which needs
+# a complete JSON object with the exact field names and variant tags before it
+# returns anything at all. The nested types (`ClusterValue`, `ItemValue`,
+# `RoleValue`, `NetworkPolicyValue`, `ClusterReplicaValue`) are out of reach for
+# byte mutation without them, so that arm would spend the whole run bailing on a
+# failed decode. Only the short types (`ConfigValue`, `SettingValue`, `RoleId`)
+# are reachable unaided.
+#
+# Structure.
+"{}"
+"[]"
+":"
+","
+"null"
+"true"
+"false"
+
+# `StateUpdateKind` is `#[serde(tag = "kind")]`, so its discriminant is a field.
+"\"kind\""
+"\"key\""
+"\"value\""
+
+# `StateUpdateKind` variants.
+"\"AuditLog\""
+"\"Cluster\""
+"\"ClusterIntrospectionSourceIndex\""
+"\"ClusterReplica\""
+"\"Comment\""
+"\"Config\""
+"\"Database\""
+"\"DefaultPrivileges\""
+"\"FenceToken\""
+"\"GidMapping\""
+"\"IdAlloc\""
+"\"Item\""
+"\"NetworkPolicy\""
+"\"Role\""
+"\"RoleAuth\""
+"\"Schema\""
+"\"ServerConfiguration\""
+"\"Setting\""
+"\"SourceReferences\""
+"\"StorageCollectionMetadata\""
+"\"SystemPrivileges\""
+"\"TxnWalShard\""
+"\"UnfinalizedShard\""
+
+# Field names reachable from the fuzzed types.
+"\"acl_mode\""
+"\"attributes\""
+"\"auto_scaling_strategy\""
+"\"availability_zones\""
+"\"catalog_id\""
+"\"cluster_id\""
+"\"config\""
+"\"definition\""
+"\"entries\""
+"\"extra_versions\""
+"\"fingerprint\""
+"\"global_id\""
+"\"grantee\""
+"\"grantor\""
+"\"inherit\""
+"\"location\""
+"\"logging\""
+"\"membership\""
+"\"name\""
+"\"oid\""
+"\"owner_id\""
+"\"privileges\""
+"\"replication_factor\""
+"\"rules\""
+"\"schedule\""
+"\"schema_id\""
+"\"size\""
+"\"superuser\""
+"\"variant\""
+"\"vars\""
+"\"version\""
+"\"workload_class\""
+"\"create_sql\""
+
+# Variant tags of the nested enums: `RoleId`/`GlobalId`, `ClusterVariant`,
+# `ReplicaLocation`, `CatalogItem`, `RoleVar`, `ClusterSchedule`.
+"\"System\""
+"\"User\""
+"\"Public\""
+"\"Predefined\""
+"\"Transient\""
+"\"Explain\""
+"\"IntrospectionSourceIndex\""
+"\"Managed\""
+"\"Unmanaged\""
+"\"Manual\""
+"\"Refresh\""
+"\"Flat\""
+"\"SqlSet\""
+"\"V1\""
diff --git a/src/catalog-protos/fuzz/fuzz_targets/catalog_objects_serde_roundtrip.rs b/src/catalog-protos/fuzz/fuzz_targets/catalog_objects_serde_roundtrip.rs
index d2a9171f43fc9..e5559ffde3131 100644
--- a/src/catalog-protos/fuzz/fuzz_targets/catalog_objects_serde_roundtrip.rs
+++ b/src/catalog-protos/fuzz/fuzz_targets/catalog_objects_serde_roundtrip.rs
@@ -7,21 +7,34 @@
 // the Business Source License, use of this software will be governed
 // by the Apache License, Version 2.0.
 
-//! Fuzz target: catalog object serde JSON round-trip is idempotent. The
-//! catalog state is durable on-disk data, so a serde edge case that loses
-//! information through a JSON round trip is a catalog-corruption risk.
+//! Fuzz target: catalog object serde round-trips are lossless. The catalog
+//! state is durable on-disk data, so an encoding edge case that loses
+//! information is a catalog-corruption risk.
+//!
+//! The durable form is not JSON *text*. `StateUpdateKindJson` packs the serde
+//! value into a `Jsonb`, i.e. an `mz_repr::Row`, and reads it back out through
+//! `to_serde_json` + `from_value`. That JSONB leg is the one that can lose
+//! information: every number becomes a `Datum::Numeric` and object keys are
+//! deduplicated and reordered by the `Row` map encoding. So the oracle asserts
+//! that leg, not just `to_vec`/`from_slice`, which never touches it.
 //!
 //! Two complementary input arms (the first byte picks the arm, the next byte
-//! picks which catalog type to exercise):
+//! picks which catalog type to exercise). Both arms dispatch over the same type
+//! list in the same order, so a corpus entry's type byte means the same thing in
+//! either arm:
 //!
 //!  * **Structured arm.** Drives the catalog type's proptest `Arbitrary`
 //!    (behind mz-catalog-protos' `proptest` feature) from the libFuzzer byte
-//!    stream to synthesize a *valid, deeply-populated* value, then asserts the
-//!    full `value -> JSON -> value -> JSON` chain is idempotent. We deliberately
-//!    target the genuinely nested catalog types: `ClusterValue`
-//!    (`RoleId` + `Vec` + the `ClusterConfig`/`ClusterVariant`/
-//!    `ManagedCluster`/`ClusterSchedule` tree), `ItemValue` (the `CatalogItem`
-//!    enum + `GlobalId` enum + `Vec`), `RoleValue` (the
+//!    stream to synthesize a *valid, deeply-populated* value, then asserts both
+//!    round trips. The list leads with `StateUpdateKind`, the durable envelope
+//!    every catalog write goes through and the one member of this family whose
+//!    round trip is not trivially total: it is `#[serde(tag = "kind")]`, so it
+//!    deserializes through serde's content-buffering path rather than straight
+//!    from the input. The rest are the genuinely nested values reached through
+//!    it: `ClusterValue` (`RoleId` + `Vec` + the
+//!    `ClusterConfig`/`ClusterVariant`/`ManagedCluster`/`ClusterSchedule` tree),
+//!    `ItemValue` (the `CatalogItem` enum + `GlobalId` enum +
+//!    `Vec`), `RoleValue` (the
 //!    `RoleAttributes`/`RoleMembership`/`RoleVars`/`RoleVar` tree),
 //!    `NetworkPolicyValue` (`Vec`), and `ClusterReplicaValue`
 //!    (the `ReplicaConfig`/`ReplicaLocation` enum). Random JSON bytes almost
@@ -29,15 +42,18 @@
 //!    serde branches actually get covered.
 //!  * **Raw-bytes arm.** Deserializes arbitrary bytes straight into the type,
 //!    exercising the deserializer against malformed/adversarial JSON input,
-//!    then re-serializes the recovered value.
+//!    then round-trips the recovered value. The nested types need a complete
+//!    object with the right field names and variant tags to get past the decode,
+//!    which is what `catalog_objects_serde_roundtrip.dict` supplies.
 
 #![no_main]
 
 use libfuzzer_sys::fuzz_target;
 use mz_catalog_protos::objects::{
     ClusterConfig, ClusterReplicaValue, ClusterValue, ConfigValue, GidMappingValue, ItemValue,
-    MzAclItem, NetworkPolicyValue, RoleId, RoleValue, SettingValue,
+    MzAclItem, NetworkPolicyValue, RoleId, RoleValue, SettingValue, StateUpdateKind,
 };
+use mz_repr::adt::jsonb::Jsonb;
 use proptest::strategy::{Strategy, ValueTree};
 use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
 
@@ -69,17 +85,28 @@ where
     assert_idempotent(tree.current());
 }
 
-/// `value -> JSON -> value` must be the identity, and re-serializing must
-/// produce byte-identical JSON.
+/// `value -> encoded -> value` must be the identity, through both the JSON text
+/// form and the JSONB form the durable catalog actually stores.
 fn assert_idempotent(orig: T)
 where
     T: serde::de::DeserializeOwned + serde::Serialize + PartialEq + std::fmt::Debug,
 {
+    // JSON text: what a `to_vec`/`from_slice` consumer sees.
     let json = serde_json::to_vec(&orig).expect("serialize of valid value must succeed");
     let round: T = serde_json::from_slice(&json).expect("re-decode must round-trip");
     assert_eq!(orig, round, "serde roundtrip changed value");
-    let json2 = serde_json::to_vec(&round).expect("re-serialize must succeed");
-    assert_eq!(json, json2, "serde re-serialize was not idempotent");
+
+    // The durable path, `StateUpdateKindJson::from_serde` / `try_to_serde`: pack
+    // the serde value into a `Row` and read it back. Numbers survive as
+    // `Datum::Numeric` only because every integer in these types fits in
+    // `Numeric`'s 39 digits (`u64::MAX` is 20) and `to_standard_notation_string`
+    // never emits exponent notation, which is a property worth asserting rather
+    // than assuming.
+    let value = serde_json::to_value(&orig).expect("serialize to a value must succeed");
+    let jsonb = Jsonb::from_serde_json(value).expect("catalog value must pack as jsonb");
+    let via_jsonb: T = serde_json::from_value(jsonb.as_ref().to_serde_json())
+        .expect("jsonb must round-trip back into the catalog type");
+    assert_eq!(orig, via_jsonb, "jsonb roundtrip changed value");
 }
 
 /// Decode adversarial JSON bytes straight into `T`, then assert the recovered
@@ -94,6 +121,28 @@ where
     assert_idempotent(orig);
 }
 
+/// Dispatch `$arm` over the catalog types. One list for both arms, so the type
+/// byte selects the same type either way and a corpus entry stays meaningful
+/// across a one-bit change to the arm byte.
+macro_rules! dispatch {
+    ($which:expr, $arm:ident, $rest:expr) => {
+        match $which % 12 {
+            0 => $arm::($rest),
+            1 => $arm::($rest),
+            2 => $arm::($rest),
+            3 => $arm::($rest),
+            4 => $arm::($rest),
+            5 => $arm::($rest),
+            6 => $arm::($rest),
+            7 => $arm::($rest),
+            8 => $arm::($rest),
+            9 => $arm::($rest),
+            10 => $arm::($rest),
+            _ => $arm::($rest),
+        }
+    };
+}
+
 fuzz_target!(|data: &[u8]| {
     let Some((&mode, rest)) = data.split_first() else {
         return;
@@ -104,31 +153,9 @@ fuzz_target!(|data: &[u8]| {
 
     if mode & 1 == 0 {
         // Structured arm: synthesize a valid, deeply-nested value.
-        match which % 10 {
-            0 => structured_roundtrip::(rest),
-            1 => structured_roundtrip::(rest),
-            2 => structured_roundtrip::(rest),
-            3 => structured_roundtrip::(rest),
-            4 => structured_roundtrip::(rest),
-            5 => structured_roundtrip::(rest),
-            6 => structured_roundtrip::(rest),
-            7 => structured_roundtrip::(rest),
-            8 => structured_roundtrip::(rest),
-            _ => structured_roundtrip::(rest),
-        }
+        dispatch!(which, structured_roundtrip, rest)
     } else {
         // Raw-bytes arm: decode adversarial JSON, then round-trip.
-        match which % 10 {
-            0 => raw_roundtrip::(rest),
-            1 => raw_roundtrip::(rest),
-            2 => raw_roundtrip::(rest),
-            3 => raw_roundtrip::(rest),
-            4 => raw_roundtrip::(rest),
-            5 => raw_roundtrip::(rest),
-            6 => raw_roundtrip::(rest),
-            7 => raw_roundtrip::(rest),
-            8 => raw_roundtrip::(rest),
-            _ => raw_roundtrip::(rest),
-        }
+        dispatch!(which, raw_roundtrip, rest)
     }
 });
diff --git a/src/catalog-protos/fuzz/prepare-corpus.sh b/src/catalog-protos/fuzz/prepare-corpus.sh
new file mode 100755
index 0000000000000..f3c7ffce90b99
--- /dev/null
+++ b/src/catalog-protos/fuzz/prepare-corpus.sh
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+
+# Copyright Materialize, Inc. and contributors. All rights reserved.
+#
+# Use of this software is governed by the Business Source License
+# included in the LICENSE file at the root of this repository.
+#
+# As of the Change Date specified in that file, in accordance with
+# the Business Source License, use of this software will be governed
+# by the Apache License, Version 2.0.
+#
+# prepare-corpus.sh: seed the catalog_objects_serde_roundtrip corpus with one
+# valid document per catalog type for the raw-bytes arm.
+#
+# That arm feeds the input straight to `serde_json::from_slice::`, which
+# returns nothing at all until the input is a complete object with the right
+# field names and variant tags. For the nested types that is far out of reach of
+# byte mutation starting from nothing: a run seeded only by libFuzzer itself
+# keeps a corpus of 2-4 byte entries, because growing an input yields no new
+# coverage until it parses, so there is no gradient to climb. The dictionary
+# supplies the tokens, and these seeds supply the shape to insert them into.
+#
+# Each seed carries the two-byte prefix the target reads first: an odd `mode`
+# byte to select the raw arm, then the type index. The JSON is deliberately
+# minimal rather than a dump of a realistic value, so libFuzzer has short inputs
+# to mutate.
+#
+# A seed that stops parsing (a field added to one of these types, say) costs
+# only the coverage it was buying, never a false pass: the target's oracle runs
+# on whatever it manages to decode. Field sets were taken from the types
+# themselves, all twelve verified to deserialize at the time of writing.
+
+set -euo pipefail
+
+cd "$(dirname "$0")"
+
+corpus=corpus/catalog_objects_serde_roundtrip
+mkdir -p "$corpus"
+find "$corpus" -maxdepth 1 -name 'seed_*' -delete
+
+# `mode` byte 0x01 selects the raw arm; the second byte is the type index in the
+# target's `dispatch!` list.
+seed() {
+    local index="$1" name="$2" json="$3"
+    local file
+    file="$corpus/seed_$(printf '%02d' "$index")_$name"
+    printf '%b' "\\0001\\0$(printf '%03o' "$index")" > "$file"
+    printf '%s' "$json" >> "$file"
+}
+
+cluster_value='{"name":"c","owner_id":"Public","privileges":[],"config":{"workload_class":null,"variant":"Unmanaged"}}'
+
+seed 0 state_update_kind "{\"kind\":\"Cluster\",\"key\":{\"id\":{\"User\":1}},\"value\":$cluster_value}"
+seed 1 cluster_value "$cluster_value"
+seed 2 item_value '{"schema_id":{"User":1},"name":"i","definition":{"V1":{"create_sql":"SELECT 1"}},"owner_id":"Public","privileges":[],"oid":1,"global_id":{"User":1},"extra_versions":[]}'
+seed 3 role_value '{"name":"r","attributes":{"inherit":true,"superuser":null,"login":null,"auto_provision_source":null},"membership":{"map":[]},"vars":{"entries":[]},"oid":1}'
+seed 4 network_policy_value '{"name":"n","rules":[{"name":"a","address":"0.0.0.0/0","action":"Allow","direction":"Ingress"}],"owner_id":"Public","privileges":[],"oid":1}'
+seed 5 cluster_replica_value '{"cluster_id":{"User":1},"name":"r1","config":{"logging":{"log_logging":false,"interval":null},"location":{"Managed":{"size":"1","availability_zones":[],"internal":false,"billed_as":null,"pending":false}},"arrangement_compression":false},"owner_id":"Public"}'
+seed 6 cluster_config '{"workload_class":null,"variant":"Unmanaged"}'
+seed 7 gid_mapping_value '{"catalog_id":1,"global_id":1,"fingerprint":"f"}'
+seed 8 mz_acl_item '{"grantee":"Public","grantor":{"User":1},"acl_mode":{"bitflags":0}}'
+seed 9 role_id '{"User":1}'
+seed 10 config_value '{"value":1}'
+seed 11 setting_value '{"value":"s"}'
+
+echo "Seeded:"
+printf "  %-40s %4d seeds\n" "$corpus/" "$(find "$corpus" -maxdepth 1 -name 'seed_*' | wc -l)"
diff --git a/src/compute/src/render/top_k.rs b/src/compute/src/render/top_k.rs
index de9d27ab502ee..9810115be1bad 100644
--- a/src/compute/src/render/top_k.rs
+++ b/src/compute/src/render/top_k.rs
@@ -722,7 +722,7 @@ where
                 // We decode the datums once, into a common buffer for efficiency.
                 // Each row should contain `arity` columns; we should check that.
                 let temp_storage = mz_repr::RowArena::new();
-                let mut buffer = datum_vec.borrow();
+                let mut buffer = Vec::with_capacity(arity * source.len());
                 for (index, (datums, _)) in source.iter().enumerate() {
                     datums.extend_datums(&temp_storage, &mut buffer, None);
                     assert_eq!(buffer.len(), arity * (index + 1));
diff --git a/src/expr/fuzz/fuzz_targets/agg_decompose.rs b/src/expr/fuzz/fuzz_targets/agg_decompose.rs
index 95507c9dee885..ddb8c539a4581 100644
--- a/src/expr/fuzz/fuzz_targets/agg_decompose.rs
+++ b/src/expr/fuzz/fuzz_targets/agg_decompose.rs
@@ -19,38 +19,86 @@
 //!  * **Hierarchical re-aggregation** (min/max/any/all, idempotent aggregates
 //!    whose output type equals their input type): `agg(whole)` must equal
 //!    `agg([agg(chunk0), agg(chunk1), ...])` over the non-empty chunks of an
-//!    arbitrary partition. This is exactly how the bucketed hierarchical reduce
-//!    computes min/max, and it must hold even with nulls present in the data.
+//!    arbitrary partition, and it must hold even with nulls present in the data.
+//!    For min/max this is exactly how the bucketed hierarchical reduce works.
+//!    `any`/`all` are maintained accumulably in the dataflow instead
+//!    (`mz_compute_types::plan::reduce::reduction_type` groups them with the sums
+//!    and `Count`, and the accumulator keeps bool counts rather than partial
+//!    booleans), but both are idempotent bool monoid homomorphisms, so the same
+//!    law holds and pins down `eval`'s three-valued folding.
 //!  * **Additive decomposition**: `count(whole)` must equal the sum of the
 //!    per-chunk counts. Likewise `sum(whole)` must equal the sum of the
 //!    per-chunk sums (this is exactly how accumulable maintenance combines
 //!    partial sums across batches). We check the sum law for the *integer* sums
 //!    (`SumInt32`/`SumInt64`), where the per-chunk combination is exact.
+//!  * **Expansion equivalence** (every aggregate): a row at multiplicity `k` must
+//!    aggregate exactly like `k` rows at multiplicity one.
 //!
 //! We generate a random multiset of nullable datums in one of several type
 //! groups, a random permutation of it, and a random partition into chunks, then
 //! check the applicable laws for each aggregate over the chosen group.
 //!
+//! Every generated row carries a random *positive* multiplicity, because
+//! `AggregateFunc::eval` consumes `(datum, diff)` pairs and dispatches on the
+//! diff three ways: `count` sums the diffs, the signed integer sums scale each
+//! value by its diff, and multiplicity-insensitive aggregates
+//! (`AggregateFunc::ignores_multiplicity`) drop it. Constant folding is the only
+//! caller that ever passes a diff other than one, because
+//! `FoldConstants::fold_reduce_constant` consolidates the constant's rows before
+//! handing them to `eval` while the dataflow's reduce renderers pass `Diff::ONE`
+//! deliberately. So a divergence in that arithmetic is a silent wrong answer
+//! reachable only through constant folding, and this is the only coverage of it.
+//!
+//! The expansion law is what actually pins that arithmetic down, and the
+//! permutation and decomposition laws cannot substitute for it. Both of those
+//! hold for *any* per-row function summed over the input, so they stay satisfied
+//! if `count` counts rows instead of summing diffs, or if the sum drops its
+//! `diff` factor: row-counting is additive too. Only comparing against the
+//! expanded form distinguishes them. What no law here can detect is a change to
+//! `ignores_multiplicity` membership for min/max, since routing those through
+//! `expand_counts` instead is genuinely equivalent at positive diffs.
+//!
+//! Retractions are deliberately out of scope. The signed sums finalize
+//! `accum == 0 && non_nulls == 0` to `Null`, which stops being additive once
+//! diffs can cancel: `[(0, -1)]` and `[(0, +1)]` each finalize to `0` while
+//! their union finalizes to `Null`. Covering retractions needs a law stated over
+//! accumulators rather than over finalized datums.
+//!
 //! Groups: `int4`/`int8`/`bool` (exact integers/booleans, plain equality
 //! oracle), `text` (lexicographic min/max), plus `float8` and `numeric`. The
 //! float/numeric groups exercise the `OrderedFloat`/`OrderedDecimal` ordering
-//! used by min/max and feed in the tricky special values (`NaN`, `±Inf`,
-//! `-0.0`). We only apply min/max to the float/numeric groups: floating-point
-//! and bounded-decimal *sum* is not
-//! associative under rounding, so an additive/permutation law over it would be
-//! a generator artifact rather than a real product invariant. Datum equality is
-//! `OrderedFloat`-based (so `NaN == NaN`), so it is a sound oracle for the
-//! ordering aggregates even with NaN present.
+//! used by min/max. `float8` feeds in the full set of IEEE-754 corners (`NaN`,
+//! `±Inf`, `-0.0`); `numeric` feeds the specials a numeric datum can actually
+//! hold, which is `NaN` plus equal-value-different-scale pairs (see
+//! `SPECIAL_NUMERIC`). We only apply min/max to the float/numeric groups:
+//! floating-point and bounded-decimal *sum* is not associative under rounding,
+//! so an additive/permutation law over it would be a generator artifact rather
+//! than a real product invariant.
+//!
+//! Datum equality is the oracle throughout, and it is sound for the ordering
+//! aggregates because it agrees with the `Ord` that `max_datum`/`min_datum`
+//! select by: `Datum`'s derived `PartialEq` delegates to `OrderedFloat` (so
+//! `NaN == NaN`) and to `OrderedDecimal`, whose `PartialEq` is defined as
+//! `cmp(..) == Equal`. So min/max returning any one of several mutually equal
+//! candidates can never trip an assertion here.
 
 #![no_main]
 
 use libfuzzer_sys::arbitrary::{self, Arbitrary, Unstructured};
 use libfuzzer_sys::fuzz_target;
 use mz_expr::AggregateFunc;
-use mz_repr::{Datum, Diff, RowArena};
+use mz_repr::{Datum, Diff, RowArena, strconv};
 
 const MAX_ROWS: usize = 24;
 const MAX_CHUNKS: usize = 4;
+/// Upper bound on a row's multiplicity. `MAX_ROWS * MAX_DIFF * 2^63` is far
+/// inside `i128` and inside numeric's 39 digits of precision, so every partial
+/// sum below is exact: `sum_signed_int_counted`'s wrapping arithmetic never
+/// wraps, and `SumInt32`'s narrowing to `i64` never truncates.
+const MAX_DIFF: u8 = 4;
+
+/// A generated input row: a datum paired with its multiplicity.
+type Update = (Datum<'static>, Diff);
 
 #[derive(Clone, Copy)]
 enum Group {
@@ -70,8 +118,8 @@ enum Group {
 /// (`"a"`/`"ab"`) where lexicographic ordering is subtle.
 const POOL_STR: &[&str] = &["", "a", "ab", "abc", "b", "Z", "z", "10", "9"];
 
-/// Which decomposition laws apply to an aggregate (permutation invariance always
-/// applies and is checked separately).
+/// Which decomposition law applies to an aggregate. Permutation invariance and
+/// expansion equivalence apply to every aggregate and are checked separately.
 enum Law {
     /// Idempotent, output type == input type: `agg(whole) == agg(map(agg, parts))`.
     Hierarchical,
@@ -97,6 +145,29 @@ const SPECIAL_F64: &[f64] = &[
     f64::MAX,
 ];
 
+/// Numeric values worth probing. `Datum::from(i128)` only ever yields canonical,
+/// finite, exponent-0 numerics, which makes the integer majority of this group a
+/// relabelling of `Group::Int64` through a different comparison function. These
+/// add the two datum shapes that actually exercise `OrderedDecimal`'s ordering:
+/// `NaN` (whose `Ord` sorts above every finite value), and numerically equal
+/// values at different scales, which `OrderedDecimal` reduces before comparing
+/// and so calls equal despite differing bit patterns. Both min/max and the
+/// equality oracle have to agree on those.
+///
+/// Every entry must be a value a numeric *datum* can actually hold, otherwise a
+/// panic here would be a generator artifact rather than a product bug.
+/// `±Infinity` and `-0` are excluded for that reason: `strconv::parse_numeric`
+/// rejects a non-overflow infinity outright, `numeric::munge_numeric` folds `-0`
+/// to `0` and `-NaN` to `NaN`, and numeric arithmetic returns
+/// `EvalError::FloatOverflow` rather than saturating to an infinity. `NaN` is the
+/// one special that survives, via `'NaN'::numeric`.
+const SPECIAL_NUMERIC: &[&str] = &["NaN", "0", "0.000", "100", "100.00", "1E+2", "-0.5"];
+
+/// A positive multiplicity. See the module doc for why retractions are excluded.
+fn gen_diff(u: &mut Unstructured) -> arbitrary::Result {
+    Ok(Diff::from(i64::from(u.int_in_range(1..=MAX_DIFF)?)))
+}
+
 fn gen_datum(u: &mut Unstructured, group: Group) -> arbitrary::Result> {
     if u.ratio(1u8, 5u8)? {
         return Ok(Datum::Null);
@@ -124,10 +195,17 @@ fn gen_datum(u: &mut Unstructured, group: Group) -> arbitrary::Result {
-            // Integer-valued numerics keep min/max exact and easy to read. The
-            // point of the group is the `OrderedDecimal` comparison path, not
-            // fractional precision.
-            Datum::from(i128::from(i64::arbitrary(u)?))
+            // Bias in the specials at the same ratio as the float group. The
+            // majority stays integer-valued, which keeps min/max exact and easy
+            // to read, but a fully random i64 never lands on a corner case.
+            if u.ratio(1u8, 2u8)? {
+                let i = u.int_in_range(0..=SPECIAL_NUMERIC.len() - 1)?;
+                Datum::from(
+                    strconv::parse_numeric(SPECIAL_NUMERIC[i]).expect("literal numeric parses"),
+                )
+            } else {
+                Datum::from(i128::from(i64::arbitrary(u)?))
+            }
         }
         Group::Str => {
             let i = u.int_in_range(0..=POOL_STR.len() - 1)?;
@@ -181,10 +259,7 @@ fn aggregates(group: Group) -> Vec<(AggregateFunc, Law)> {
 }
 
 /// A Fisher-Yates shuffle driven by the fuzz input.
-fn shuffle(
-    u: &mut Unstructured,
-    input: &[Datum<'static>],
-) -> arbitrary::Result>> {
+fn shuffle(u: &mut Unstructured, input: &[Update]) -> arbitrary::Result> {
     let mut v = input.to_vec();
     for i in (1..v.len()).rev() {
         let j = u.int_in_range(0..=i)?;
@@ -195,15 +270,12 @@ fn shuffle(
 
 /// Randomly assign each input to one of `1..=MAX_CHUNKS` chunks (some may be
 /// empty). The chunks' concatenation is a permutation of the input multiset.
-fn partition(
-    u: &mut Unstructured,
-    input: &[Datum<'static>],
-) -> arbitrary::Result>>> {
+fn partition(u: &mut Unstructured, input: &[Update]) -> arbitrary::Result>> {
     let k = u.int_in_range(1usize..=MAX_CHUNKS)?;
     let mut chunks = vec![Vec::new(); k];
-    for &d in input {
+    for &update in input {
         let b = u.int_in_range(0..=k - 1)?;
-        chunks[b].push(d);
+        chunks[b].push(update);
     }
     Ok(chunks)
 }
@@ -217,9 +289,9 @@ fn as_count(d: Datum) -> i64 {
 
 /// Decode an integer-sum result datum to an exact `i128`. `SumInt32` yields an
 /// `Int64`, `SumInt64` yields an (integer-valued) `Numeric`. An empty/all-null
-/// chunk yields `Null` (returned as `None`). The values are bounded (<= 24
-/// inputs of at most i64 magnitude), so every partial sum fits an `i128`
-/// exactly and the per-chunk combination below is lossless.
+/// chunk yields `Null` (returned as `None`). The values are bounded (see
+/// `MAX_DIFF`), so every partial sum fits an `i128` exactly and the per-chunk
+/// combination below is lossless.
 fn as_sum(d: Datum) -> Option {
     match d {
         Datum::Null => None,
@@ -244,37 +316,56 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
     let n = u.int_in_range(0usize..=MAX_ROWS)?;
     let mut input = Vec::with_capacity(n);
     for _ in 0..n {
-        input.push(gen_datum(u, group)?);
+        input.push((gen_datum(u, group)?, gen_diff(u)?));
     }
     let permuted = shuffle(u, &input)?;
     let chunks = partition(u, &input)?;
+    // NOTE: expansion is only faithful because every diff is >= 1. At diff 0 the
+    // row would vanish here while `ignores_multiplicity` aggregates still see it,
+    // so min/max would diverge for reasons that are not a product bug.
+    let expanded: Vec = input
+        .iter()
+        .flat_map(|&(d, diff)| {
+            let copies = usize::try_from(diff.into_inner()).expect("positive diff");
+            std::iter::repeat((d, Diff::ONE)).take(copies)
+        })
+        .collect();
 
     let arena = RowArena::new();
     for (agg, law) in aggregates(group) {
-        // `AggregateFunc::eval` consumes `(datum, multiplicity)` pairs. Each
-        // generated row has multiplicity one, so pair every datum with
-        // `Diff::ONE`.
-        let whole = agg.eval(input.iter().map(|&d| (d, Diff::ONE)), &arena);
+        let whole = agg.eval(input.iter().copied(), &arena);
 
         // Permutation invariance: order must never matter.
-        let shuffled = agg.eval(permuted.iter().map(|&d| (d, Diff::ONE)), &arena);
+        let shuffled = agg.eval(permuted.iter().copied(), &arena);
         assert_eq!(
             whole, shuffled,
             "{agg:?} is not permutation-invariant\n  input    = {input:?}\n  permuted = {permuted:?}"
         );
 
+        // Expansion equivalence: a row at multiplicity `k` must aggregate
+        // exactly like `k` rows at multiplicity one.
+        let unit_diffs = agg.eval(expanded.iter().copied(), &arena);
+        assert_eq!(
+            whole, unit_diffs,
+            "{agg:?} does not treat a diff of k like k unit rows\n  input    = {input:?}\n  expanded = {expanded:?}"
+        );
+
         match law {
             Law::Hierarchical => {
-                // agg(whole) == agg(map(agg, non-empty chunks)). Empty chunks are
-                // skipped: an empty chunk aggregates to null, and for any/all
-                // (three-valued) a stray null would corrupt an otherwise false/true
-                // result (`false OR null = null`). Min/max absorb null as their
-                // identity, but skipping is correct for all of them.
+                // agg(whole) == agg(map(agg, non-empty chunks)). Skipping the
+                // empty chunks keeps the partials to the results of non-trivial
+                // work. It is a no-op for these aggregates: min/max aggregate an
+                // empty chunk to `Null` and filter nulls back out on the way in,
+                // and any/all fold from their identity (`False`/`True`), so an
+                // empty chunk contributes nothing either way.
                 let partials: Vec = chunks
                     .iter()
                     .filter(|c| !c.is_empty())
-                    .map(|c| agg.eval(c.iter().map(|&d| (d, Diff::ONE)), &arena))
+                    .map(|c| agg.eval(c.iter().copied(), &arena))
                     .collect();
+                // The partials are values, not updates, so each enters the
+                // re-aggregation once. Every aggregate under this law ignores
+                // multiplicity regardless.
                 let reaggregated = agg.eval(partials.iter().map(|&d| (d, Diff::ONE)), &arena);
                 assert_eq!(
                     whole, reaggregated,
@@ -285,7 +376,7 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
                 // count(whole) == sum(map(count, chunks))
                 let total: i64 = chunks
                     .iter()
-                    .map(|c| as_count(agg.eval(c.iter().map(|&d| (d, Diff::ONE)), &arena)))
+                    .map(|c| as_count(agg.eval(c.iter().copied(), &arena)))
                     .sum();
                 assert_eq!(
                     as_count(whole),
@@ -300,7 +391,7 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
                 // whole is also Null, so both sides are "no partials" and match.
                 let partials: Vec = chunks
                     .iter()
-                    .filter_map(|c| as_sum(agg.eval(c.iter().map(|&d| (d, Diff::ONE)), &arena)))
+                    .filter_map(|c| as_sum(agg.eval(c.iter().copied(), &arena)))
                     .collect();
                 let combined: Option = if partials.is_empty() {
                     None
diff --git a/src/expr/fuzz/fuzz_targets/build_regex.rs b/src/expr/fuzz/fuzz_targets/build_regex.rs
index a752cb1d2124e..7b8ec722eef1f 100644
--- a/src/expr/fuzz/fuzz_targets/build_regex.rs
+++ b/src/expr/fuzz/fuzz_targets/build_regex.rs
@@ -10,8 +10,12 @@
 //! Fuzz target: `func::build_regex` compiles an untrusted regular expression
 //! (and flags) for the `regexp_*` SQL functions, and the result matches
 //! untrusted text. A user controls both, so a panic compiling or matching is a
-//! real availability bug. The `regex` crate is size-limited, so an oversized
-//! pattern returns an error rather than OOMing. Single-match ops
+//! real availability bug. Two independent size limits guard that, and both are
+//! under test here: the `regex` crate's `size_limit` bounds the *compiled* NFA,
+//! and `MAX_REGEX_SIZE_BEFORE_COMPILATION` bounds `pattern.len()` before
+//! compilation, because the memory a compile spends translating the pattern is
+//! not something `size_limit` ever sees. Exceeding either must yield an error
+//! rather than an OOM or a hang. Single-match ops
 //! (`is_match`/`find`/`captures`) run in linear time, but the all-matches ops
 //! (`replace_all`/`split`) re-scan from every match via `find_iter` and are
 //! superlinear in the text length for adversarial patterns, so the match text
@@ -32,6 +36,10 @@
 //!  * occasional near-size-limit patterns built from nested counted quantifiers,
 //!    which push the compiler toward its state-count / size limit (where it must
 //!    return an error rather than hang or OOM) and toward deep AST nesting.
+//!  * rare very long patterns, built by repeating one small unit, which straddle
+//!    `MAX_REGEX_SIZE_BEFORE_COMPILATION`. These are the only way to reach the
+//!    compile-time memory cost, which scales with the *source* length and so is
+//!    invisible to every other arm (see `gen_long_pattern`).
 
 #![no_main]
 
@@ -145,9 +153,12 @@ fn gen_regex(
 }
 
 /// Builds a deeply nested chain of counted quantifiers whose multiplied bounds
-/// approach the regex crate's compiled-size limit, e.g. `(?:(?:a{40}){40}){40}`.
-/// `build_regex` must reject this with an error (PatternTooLarge or the regex
-/// crate's CompiledTooBig) rather than hang or OOM.
+/// approach the regex crate's compiled-size limit, e.g. `(?:(?:a){40}){40}`.
+///
+/// These patterns stay tiny in source form, a few dozen bytes, so
+/// `MAX_REGEX_SIZE_BEFORE_COMPILATION` never fires on them. The regex crate's
+/// own `size_limit` is what must reject them, with `CompiledTooBig`, rather than
+/// hanging or OOMing. `gen_long_pattern` covers the other limit.
 fn gen_near_limit(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
     let layers = u.int_in_range(2usize..=5)?;
     for _ in 0..layers {
@@ -160,20 +171,64 @@ fn gen_near_limit(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<(
     Ok(())
 }
 
+/// Builds a pattern that is long *before* compilation by repeating one small
+/// unit.
+///
+/// The regex crate's `size_limit` bounds only the compiled NFA. Class-heavy
+/// patterns spend their memory earlier, in `regex-syntax`'s HIR translation,
+/// which that limit never sees, so cost scales with the pattern's *source*
+/// length. Measured worst case is ~7.4 KB of peak RSS per pattern byte
+/// (`\p{L}` repeated, under the `i` flag, where case-folding the Unicode class
+/// is the multiplier), against ~0.35 KB per byte for a plain literal. That
+/// makes `MAX_REGEX_SIZE_BEFORE_COMPILATION` the only guard on the vector, so
+/// straddle it: draw a length on both sides of 1 MiB, exercising both the
+/// `PatternTooLarge` rejection and the largest pattern that gets through.
+///
+/// The length is synthesized from a handful of input bytes rather than read out
+/// of the input, since libFuzzer's default `-max_len` of 4096 puts a pattern
+/// this long out of reach of the raw arm.
+fn gen_long_pattern(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
+    let unit = *u.choose(&[
+        "\\p{L}",
+        "\\p{Greek}",
+        "\\w",
+        "\\d",
+        "\\s",
+        "[a-c]",
+        "a",
+        "(?:a)",
+    ])?;
+    let bytes = u.int_in_range(4096usize..=1_100_000)?;
+    out.reserve(bytes + unit.len());
+    // Round *up*, so a multi-byte unit can still land above the guard.
+    for _ in 0..bytes.div_ceil(unit.len()) {
+        out.push_str(unit);
+    }
+    Ok(())
+}
+
 /// A replacement string mixing several capture-reference forms so the regex
 /// crate's interpolation runs against whatever captures the match produced:
 /// numbered (`$1`), named-braced (`${g0}`), the whole match (`$0`), a literal
-/// `$$`, and an out-of-range index (`$99`, which interpolates to empty).
-const REPLACEMENT: &str = "x$1-${g0}-$0-$$-$99y";
+/// `$$`, and an out-of-range index (`${99}`, which interpolates to empty).
+///
+/// NOTE: the braces on `${99}` are load-bearing. An unbraced `$name` takes the
+/// longest run of `[0-9A-Za-z_]`, so `$99y` would parse as the capture *name*
+/// `99y` and take the name-lookup path instead of the index one, silently
+/// swallowing the trailing literal.
+const REPLACEMENT: &str = "x$1-${g0}-$0-$$-${99}y";
 
 fn drive(pattern: &str, flags: &str, text: &str) {
-    let pattern = cap(pattern, 4096);
     // The all-matches ops below (`replace_all`, `split`) drive `find_iter`,
     // which re-scans from each match and is superlinear in the text length for
     // adversarial patterns. Coverage instrumentation amplifies that by orders of
     // magnitude, so keep the match text very short to stay within libFuzzer's
-    // per-unit timeout. Pattern length compiles fast and is not the amplifier,
-    // so it stays generous.
+    // per-unit timeout.
+    //
+    // The pattern is deliberately *not* capped. Its length is what drives
+    // compile-time memory, so capping it here would hide the very blowup this
+    // target is meant to reach, and `MAX_REGEX_SIZE_BEFORE_COMPILATION` is the
+    // guard under test. See `gen_long_pattern`.
     let text = cap(text, 16);
     let Ok(regex) = func::build_regex(pattern, flags) else {
         return;
@@ -198,13 +253,17 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
         return Ok(());
     }
     let mut pattern = String::new();
-    // Occasionally emit a near-size-limit nested-quantifier pattern (the
-    // compiler must reject it cleanly). Otherwise the structured generator.
-    if u.int_in_range(0u8..=7)? == 0 {
-        gen_near_limit(&mut u, &mut pattern)?;
-    } else {
-        let mut name_id = 0u32;
-        gen_regex(&mut u, 3, &mut name_id, &mut pattern)?;
+    // Occasionally emit an adversarially sized pattern, aimed at one of the two
+    // size limits, which the compiler must reject cleanly. The long-pattern arm
+    // legitimately costs seconds and gigabytes per unit, so it stays rare.
+    // Otherwise the structured generator.
+    match u.int_in_range(0u8..=15)? {
+        0 | 1 => gen_near_limit(&mut u, &mut pattern)?,
+        2 => gen_long_pattern(&mut u, &mut pattern)?,
+        _ => {
+            let mut name_id = 0u32;
+            gen_regex(&mut u, 3, &mut name_id, &mut pattern)?;
+        }
     }
     let mut text = String::new();
     for _ in 0..u.int_in_range(0usize..=24)? {
diff --git a/src/expr/fuzz/fuzz_targets/cast_string.rs b/src/expr/fuzz/fuzz_targets/cast_string.rs
index c91eb7b8810c8..810a782bb5d4c 100644
--- a/src/expr/fuzz/fuzz_targets/cast_string.rs
+++ b/src/expr/fuzz/fuzz_targets/cast_string.rs
@@ -30,7 +30,7 @@
 
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
-use mz_expr::{func, Eval, MirScalarExpr, UnaryFunc};
+use mz_expr::{Eval, MirScalarExpr, UnaryFunc, func};
 use mz_repr::{Datum, ReprScalarType, RowArena};
 
 const HEX: &[u8] = b"0123456789abcdefABCDEF";
diff --git a/src/expr/fuzz/fuzz_targets/eval_error_proto_roundtrip.rs b/src/expr/fuzz/fuzz_targets/eval_error_proto_roundtrip.rs
index e3ad1d4634199..68bfb7cca11f4 100644
--- a/src/expr/fuzz/fuzz_targets/eval_error_proto_roundtrip.rs
+++ b/src/expr/fuzz/fuzz_targets/eval_error_proto_roundtrip.rs
@@ -8,24 +8,30 @@
 // by the Apache License, Version 2.0.
 
 //! Fuzz target: `EvalError` must survive a proto re-encode + re-decode with the
-//! same value, exercising lossy proto conversions where the wire form decodes
-//! into a value that doesn't round-trip back to itself.
+//! same value, and the decoded value must then be renderable. This exercises
+//! lossy proto conversions where the wire form decodes into a value that doesn't
+//! round-trip back to itself, and decoded states that no Rust constructor
+//! produces and that consumers therefore mishandle.
 //!
 //! Two input modes share the byte stream (the first byte selects the mode):
 //!
 //!   * Mode A (Arbitrary): drive proptest's `Arbitrary` impl for `EvalError`
 //!     from the libFuzzer byte stream to synthesize a *valid, deeply nested*
-//!     `EvalError` (one of 80+ variants, several carrying `char`, `usize`,
-//!     nested `NumericMaxScale`/`InvalidArrayError`/`DomainLimit` invariants).
-//!     We then assert `from_proto(into_proto(v)) == v`. Random bytes decoded as
-//!     a proto almost always yield near-empty/default messages, so this arm is
-//!     what actually reaches the interesting variants and their narrowing /
-//!     validation logic (`char::from_proto`, `usize`/`u32` casts, etc.).
+//!     `EvalError` (one of 80+ variants, several carrying `char`, `usize`, or a
+//!     nested `InvalidArrayError` / `ParseError` / `ParseHexError` /
+//!     `InvalidRangeError` / `DomainLimit`). We then assert
+//!     `from_proto(into_proto(v)) == v`. Random bytes decoded as a proto almost
+//!     always yield near-empty/default messages, so this arm is what reaches the
+//!     deeper variants at all, along with the `usize <-> u64` / `char <-> u32`
+//!     casts on their payloads.
 //!
 //!   * Mode B (raw bytes): decode the remaining bytes directly as
 //!     `ProtoEvalError` and, if it converts to Rust, assert it round-trips.
-//!     Kept for robustness against hand-crafted / malformed wire forms that the
-//!     structured generator would never produce.
+//!     Mode A always starts from an already-valid Rust value, so it structurally
+//!     cannot reach the *rejection* branches of `char::from_proto` /
+//!     `usize::from_proto`, nor any state that no constructor builds. Only this
+//!     arm can, which is why it stays even though most inputs decode to
+//!     something trivial.
 
 #![no_main]
 
@@ -36,16 +42,25 @@ use proptest::strategy::{Strategy, ValueTree};
 use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
 use prost::Message;
 
-/// Assert that a `EvalError` survives encode -> decode -> into_rust unchanged.
+/// Assert that a `EvalError` survives encode -> decode -> into_rust unchanged,
+/// and that the decoded value is renderable.
 fn assert_roundtrip(orig: &EvalError) {
     let proto = >::from_rust(orig);
     let bytes = proto.encode_to_vec();
-    let decoded = ProtoEvalError::decode(bytes.as_slice())
-        .expect("re-encode of valid EvalError must decode");
+    let decoded =
+        ProtoEvalError::decode(bytes.as_slice()).expect("re-encode of valid EvalError must decode");
     let round: EvalError = decoded
         .into_rust()
         .expect("re-encoded EvalError must convert back to Rust");
     assert_eq!(orig, &round, "EvalError changed across proto roundtrip");
+
+    // Round-trip identity alone would certify a decoded value that aborts the
+    // process the moment anything looks at it. `DataflowErrorSer::Display`
+    // decodes untrusted bytes and Displays them on the index peek path, so a
+    // panicking `Display`/`detail`/`hint` arm is a dataflow crash loop.
+    let _ = round.to_string();
+    let _ = round.detail();
+    let _ = round.hint();
 }
 
 /// Decode `data` directly as a `ProtoEvalError`. If it converts to a Rust
@@ -78,11 +93,13 @@ fuzz_target!(|data: &[u8]| {
         }
         let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed);
         let mut runner = TestRunner::new_with_rng(Config::default(), rng);
-        let value = ::arbitrary()
-            .new_tree(&mut runner)
-            .expect("valuetree")
-            .current();
-        assert_roundtrip(&value);
+        let Ok(tree) =
+            ::arbitrary().new_tree(&mut runner)
+        else {
+            // A generator rejection is a harness limit, not a product bug.
+            return;
+        };
+        assert_roundtrip(&tree.current());
     } else {
         // Mode B: raw wire bytes.
         raw_bytes_arm(rest);
diff --git a/src/expr/fuzz/fuzz_targets/jsonb_get.rs b/src/expr/fuzz/fuzz_targets/jsonb_get.rs
index cebff0629b2cb..c354d6b395d68 100644
--- a/src/expr/fuzz/fuzz_targets/jsonb_get.rs
+++ b/src/expr/fuzz/fuzz_targets/jsonb_get.rs
@@ -23,6 +23,13 @@
 //! generate the access key/index from that same set, so the accessors hit real
 //! fields/elements (the success + traversal paths) as well as missing ones, and
 //! the index includes out-of-range and extreme values for the array-bounds path.
+//!
+//! Two caps on the generated document are deliberate rather than oversights.
+//! String values draw from a fragment set that covers escapes and non-ASCII but
+//! omits the NUL escape (SQL-475), and nesting stops at depth 5 (SQL-515). Both
+//! of the excluded shapes hit bugs that are tracked already and that this target
+//! has no oracle for anyway, and a target that reproduces a known crash on every
+//! run buries whatever else it would have found.
 
 #![no_main]
 
@@ -30,13 +37,33 @@ use std::str::FromStr;
 
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
-use mz_expr::{func, Eval, MirScalarExpr};
+use mz_expr::{Eval, MirScalarExpr, func};
 use mz_repr::adt::jsonb::Jsonb;
 use mz_repr::{Datum, ReprScalarType, RowArena};
 
 /// Object keys, kept to a small set so generated access keys hit real fields.
 const KEYS: &[&str] = &["a", "b", "c", "x"];
 
+/// Fragments of a JSON string value, spliced raw between the quotes, so escape
+/// sequences appear here already escaped. Past plain ASCII these cover what
+/// `->>` has to re-escape when `jsonb_stringify` renders the accessed element
+/// back to text: quote, backslash, a control character, and non-ASCII in both
+/// `\u` and raw UTF-8 form, including a surrogate pair.
+const STRING_PARTS: &[&str] = &[
+    "a",
+    "z",
+    "0",
+    " ",
+    "\\\"",
+    "\\\\",
+    "\\n",
+    "\\u0001",
+    "\\u00e9",
+    "é",
+    "\\ud83d\\ude00",
+    "😀",
+];
+
 fn gen_json(u: &mut Unstructured, depth: u32, out: &mut String) -> arbitrary::Result<()> {
     let leaf = depth == 0 || u.is_empty();
     match if leaf {
@@ -54,7 +81,7 @@ fn gen_json(u: &mut Unstructured, depth: u32, out: &mut String) -> arbitrary::Re
         3 => {
             out.push('"');
             for _ in 0..u.int_in_range(0usize..=4)? {
-                out.push(*u.choose(&['a', 'z', '0', ' '])?);
+                out.push_str(u.choose(STRING_PARTS)?);
             }
             out.push('"');
         }
@@ -87,15 +114,31 @@ fn gen_json(u: &mut Unstructured, depth: u32, out: &mut String) -> arbitrary::Re
     Ok(())
 }
 
-fn run(mut u: Unstructured) -> arbitrary::Result<()> {
-    let mut json = String::new();
-    gen_json(&mut u, 5, &mut json)?;
-    let Ok(jsonb) = Jsonb::from_str(&json) else {
-        return Ok(());
-    };
-    let value = jsonb.as_ref().into_datum();
-    let arena = RowArena::new();
+/// Evaluates `expr`, requiring that it neither panic nor error.
+///
+/// The four accessors under test are infallible, they return `Option`, not
+/// `Result`, and both operands are `Literal(Ok(..))`, which cannot error. So the
+/// only `Err` reachable here is the `EvalError::Internal` that the eager binary
+/// dispatch raises when a literal's `Datum`/`ReprScalarType` does not match the
+/// function's declared input type. Nothing checks that match at compile time, so
+/// an `Err` means this harness built an invalid expression and every eval below
+/// short-circuits before reaching the code under test, leaving a target that
+/// exercises nothing yet still reports clean.
+fn eval_ok(expr: MirScalarExpr, arena: &RowArena) {
+    let res = expr.eval(&[], arena);
+    assert!(res.is_ok(), "harness built an invalid expression: {res:?}");
+}
 
+fn run(mut u: Unstructured) -> arbitrary::Result<()> {
+    // The access key and index are drawn before the document, and the order
+    // matters. `gen_json` scales its appetite with the input it is handed and
+    // drains the buffer on anything up to libFuzzer's default `-max_len=4096`,
+    // and `Unstructured` does not error once exhausted, it returns the low end of
+    // every range. Drawing these afterwards therefore pins them to `""` and `0`
+    // for the vast majority of executions, which unfuzzes half the input space.
+    // `gen_json` degrades gracefully on the remainder, it emits `null` when
+    // starved.
+    //
     // Key: usually a real field name (hit), sometimes a miss / arbitrary string.
     let key_buf;
     let key: &str = if u.int_in_range(0u8..=2)? == 0 {
@@ -116,27 +159,39 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
         i64::from(u.int_in_range(-3i32..=8)?)
     };
 
+    let mut json = String::new();
+    gen_json(&mut u, 5, &mut json)?;
+    let Ok(jsonb) = Jsonb::from_str(&json) else {
+        return Ok(());
+    };
+    let value = jsonb.as_ref().into_datum();
+    let arena = RowArena::new();
+
     let key_expr = || MirScalarExpr::literal_ok(Datum::String(key), ReprScalarType::String);
     let index_expr = || MirScalarExpr::literal_ok(Datum::Int64(index), ReprScalarType::Int64);
     let jsonb_expr = || MirScalarExpr::literal_ok(value, ReprScalarType::Jsonb);
 
     // jsonb -> ''  (object field access, returns jsonb)
-    let _ = jsonb_expr()
-        .call_binary(key_expr(), func::JsonbGetString)
-        .eval(&[], &arena);
+    eval_ok(
+        jsonb_expr().call_binary(key_expr(), func::JsonbGetString),
+        &arena,
+    );
     // jsonb ->> ''  (object field access, returns text via jsonb_stringify)
-    let _ = jsonb_expr()
-        .call_binary(key_expr(), func::JsonbGetStringStringify)
-        .eval(&[], &arena);
+    eval_ok(
+        jsonb_expr().call_binary(key_expr(), func::JsonbGetStringStringify),
+        &arena,
+    );
 
     // jsonb ->   (array element access, returns jsonb)
-    let _ = jsonb_expr()
-        .call_binary(index_expr(), func::JsonbGetInt64)
-        .eval(&[], &arena);
+    eval_ok(
+        jsonb_expr().call_binary(index_expr(), func::JsonbGetInt64),
+        &arena,
+    );
     // jsonb ->>   (array element access, returns text via jsonb_stringify)
-    let _ = jsonb_expr()
-        .call_binary(index_expr(), func::JsonbGetInt64Stringify)
-        .eval(&[], &arena);
+    eval_ok(
+        jsonb_expr().call_binary(index_expr(), func::JsonbGetInt64Stringify),
+        &arena,
+    );
     Ok(())
 }
 
diff --git a/src/expr/fuzz/fuzz_targets/jsonb_path.rs b/src/expr/fuzz/fuzz_targets/jsonb_path.rs
index 9750217628710..76c95f6180fbf 100644
--- a/src/expr/fuzz/fuzz_targets/jsonb_path.rs
+++ b/src/expr/fuzz/fuzz_targets/jsonb_path.rs
@@ -38,7 +38,7 @@ use std::str::FromStr;
 
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
-use mz_expr::{func, Eval, MirScalarExpr};
+use mz_expr::{Eval, MirScalarExpr, func};
 use mz_repr::adt::array::{ArrayDimension, InvalidArrayError};
 use mz_repr::adt::jsonb::Jsonb;
 use mz_repr::{Datum, ReprScalarType, RowArena};
@@ -119,14 +119,15 @@ fn gen_json(u: &mut Unstructured, depth: u32, out: &mut String) -> arbitrary::Re
 }
 
 fn run(mut u: Unstructured) -> arbitrary::Result<()> {
-    let mut json = String::new();
-    gen_json(&mut u, 5, &mut json)?;
-    let Ok(jsonb) = Jsonb::from_str(&json) else {
-        return Ok(());
-    };
-    let value = jsonb.as_ref().into_datum();
-    let arena = RowArena::new();
-
+    // The path is drawn before the document, and the order matters. `gen_json`
+    // scales its appetite with the input it is handed and drains the buffer on
+    // anything up to libFuzzer's default `-max_len=4096`, and `Unstructured` does
+    // not error once exhausted, it returns the low end of every range. Drawing
+    // the path afterwards therefore pins `n` to 0 for the vast majority of
+    // executions, and `jsonb #> '{}'` returns the document unchanged, so the walk
+    // this target exists for would never run. `gen_json` degrades gracefully on
+    // the remainder, it emits `null` when starved.
+    //
     // Path components: object keys (hits), array indices ("0".."3"), and an
     // occasional miss, so the walk descends real structure and also dead-ends.
     let n = u.int_in_range(0usize..=5)?;
@@ -139,6 +140,14 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
         });
     }
 
+    let mut json = String::new();
+    gen_json(&mut u, 5, &mut json)?;
+    let Ok(jsonb) = Jsonb::from_str(&json) else {
+        return Ok(());
+    };
+    let value = jsonb.as_ref().into_datum();
+    let arena = RowArena::new();
+
     let dims = if path.is_empty() {
         Vec::new()
     } else {
diff --git a/src/expr/fuzz/fuzz_targets/like_pattern_compile.rs b/src/expr/fuzz/fuzz_targets/like_pattern_compile.rs
index 119c00fe5e15f..7cfe239eb5ce5 100644
--- a/src/expr/fuzz/fuzz_targets/like_pattern_compile.rs
+++ b/src/expr/fuzz/fuzz_targets/like_pattern_compile.rs
@@ -14,18 +14,29 @@
 //!
 //!  1. Compiling an arbitrary pattern and matching arbitrary text never panics.
 //!  2. A pattern built by escaping every wildcard/escape character in `text` is
-//!     a pure literal, and `LIKE` is anchored, so it must match exactly `text`.
+//!     a pure literal, and `LIKE` is anchored, so it must match exactly `text`:
+//!     `text` matches, and `text` with a character appended does not.
 //!
 //! A pattern/text pair of random Unicode rarely contains the wildcard/escape
 //! metacharacters or the literal overlap that drives the interesting code, so it
 //! plateaus on shallow shapes. Instead we mostly draw both the pattern and the
 //! match text from a tiny shared alphabet of literals plus the LIKE
-//! metacharacters `%`, `_`, and `\`. Generating *multiple* `%` is deliberate: at
-//! two-or-more `%` (`many_subpatterns > 1`) `compile` abandons the backtracking
-//! string matcher and routes to the linear-time regex engine, a boundary the
-//! string-only inputs never reach. Drawing the text from the same alphabet means
-//! it actually matches the wildcards, exercising the `%`-suffix-search /
-//! backtracking and the regex path on real (not always-empty) matches.
+//! metacharacters `%`, `_`, and `\`. Generating *several* `%` interleaved with
+//! literals is deliberate, because that is what crosses the routing boundary
+//! between the two matcher implementations: `compile` abandons the backtracking
+//! string matcher for the linear-time regex engine once two or more `%`
+//! subpatterns carry a *non-empty* suffix (so `%a%b`, but not `%%%%`, which
+//! collapses into one subpattern, and not `%a%`, whose trailing `%` has an empty
+//! suffix), or once the subpattern count exceeds `MAX_SUBPATTERNS`, which the
+//! `%`-heavy weighting reaches most often in practice.
+//!
+//! Drawing the text from the same alphabet means it actually matches the
+//! wildcards, exercising the `%`-suffix-search / backtracking and the regex path
+//! on real (not always-empty) matches. The alphabet carries multi-byte
+//! characters so byte offsets and character counts diverge: the `%` backtracking
+//! in `is_match_subpatterns` mixes character counting with byte indexing and
+//! walks back over char boundaries by byte, and with a single-byte alphabet that
+//! walk degenerates to one iteration and can never expose an off-by-one.
 
 #![no_main]
 
@@ -34,11 +45,14 @@ use libfuzzer_sys::fuzz_target;
 use mz_expr::like_pattern;
 
 /// Literals shared between pattern and text, so wildcards match real characters.
-const LITERALS: &[char] = &['a', 'b', 'c'];
+/// `é` (2 bytes) and `漢` (3 bytes) make byte offsets and character counts
+/// diverge, so the char-boundary walk in `is_match_subpatterns` runs for more
+/// than one iteration.
+const LITERALS: &[char] = &['a', 'b', 'c', 'é', '漢'];
 
 /// Builds a LIKE pattern over the literal alphabet plus the `%`/`_`/`\`
-/// metacharacters, weighted toward emitting several `%` so the >1-`%` regex
-/// routing boundary is crossed.
+/// metacharacters, weighted toward emitting several `%` so patterns land on both
+/// sides of the string-matcher/regex routing boundary.
 fn gen_pattern(u: &mut Unstructured) -> arbitrary::Result {
     let mut p = String::new();
     let n = u.int_in_range(0usize..=16)?;
@@ -53,7 +67,7 @@ fn gen_pattern(u: &mut Unstructured) -> arbitrary::Result {
                 // reachable when this is the last iteration.
                 p.push('\\');
                 if u.int_in_range(0u8..=3)? != 0 {
-                    p.push(*u.choose(&['%', '_', '\\', 'a'])?);
+                    p.push(*u.choose(&['%', '_', '\\', 'a', 'é'])?);
                 }
             }
             _ => p.push(*u.choose(LITERALS)?),
@@ -96,11 +110,31 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
         }
         literal.push(c);
     }
-    if let Ok(matcher) = like_pattern::compile(&literal, case_insensitive) {
-        assert!(
-            matcher.is_match(&text),
-            "literal LIKE pattern {literal:?} must match its source text {text:?}"
-        );
+    match like_pattern::compile(&literal, case_insensitive) {
+        Ok(matcher) => {
+            assert!(
+                matcher.is_match(&text),
+                "literal LIKE pattern {literal:?} must match its source text {text:?}"
+            );
+            // And nothing else: the pattern is anchored at both ends and every
+            // one of its characters consumes exactly one text character, under
+            // `ILIKE` too, since simple case folding maps a character to other
+            // single characters. So appending anything must break the match. This
+            // is the direction that catches over-matching regressions, such as a
+            // lost anchor in the regex or an escaped metacharacter that falls
+            // back to wildcard semantics.
+            let longer = format!("{text}a");
+            assert!(
+                !matcher.is_match(&longer),
+                "literal LIKE pattern {literal:?} must not match {longer:?}"
+            );
+        }
+        // Every escape sequence in `literal` is terminated by construction, so
+        // the documented pattern-length limit is the only legitimate rejection.
+        Err(e) => assert!(
+            literal.len() > 8 << 10,
+            "valid literal pattern {literal:?} failed to compile: {e}"
+        ),
     }
     Ok(())
 }
diff --git a/src/expr/fuzz/fuzz_targets/like_pattern_escape.rs b/src/expr/fuzz/fuzz_targets/like_pattern_escape.rs
index 81ffb17964753..00b87491e13db 100644
--- a/src/expr/fuzz/fuzz_targets/like_pattern_escape.rs
+++ b/src/expr/fuzz/fuzz_targets/like_pattern_escape.rs
@@ -10,10 +10,27 @@
 //! Fuzz target: `like_pattern::normalize_pattern` rewrites a `LIKE ... ESCAPE c`
 //! pattern (custom escape character) into the default-escape form before
 //! compilation. The user controls both the pattern and the escape character, so
-//! the custom-escape rewrite, a char-by-char parser with escape state, must
-//! not panic on any input, and its output must still compile and match. This is
-//! the `ESCAPE`-clause path that `like_pattern_compile` (default escape only)
-//! never exercises.
+//! the custom-escape rewrite, a char-by-char parser with escape state, must not
+//! panic on any input. This is the `ESCAPE`-clause path that
+//! `like_pattern_compile` (default escape only) never exercises.
+//!
+//! `normalize_pattern` has no slicing, no byte indexing and no fallible
+//! arithmetic, so a panic-only harness over it can never fail. The bug class it
+//! can actually carry is a miscompilation in the escape state machine, a
+//! dropped, doubled or mis-paired escape. Two oracles pin that down:
+//!
+//!  1. Every output of `normalize_pattern` emits the default escape `\` only as
+//!     part of a completed two-character escape, so it is well-formed
+//!     default-escape form and must compile. The size limit is the only
+//!     legitimate rejection. See the exception for the identity passthrough at
+//!     the check itself.
+//!  2. Escaping every metacharacter *and* the escape character itself with the
+//!     custom escape character yields a pure literal pattern. `LIKE` is
+//!     anchored, so after normalization it must match exactly its own source
+//!     text: `text` matches, and `text` with a character appended does not. A
+//!     lost `\` turns an escaped `%`/`_` back into a wildcard and the appended
+//!     direction fires. A spurious or mis-paired `\` breaks the match and the
+//!     first direction fires.
 //!
 //! With a fully arbitrary escape char and a fully arbitrary pattern, the escape
 //! char almost never coincides with a character actually in the pattern, so the
@@ -29,45 +46,116 @@
 
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
+use mz_expr::EvalError;
 use mz_expr::like_pattern::{self, EscapeBehavior};
 
-/// Shared alphabet for the pattern and the escape char, so the escape char
+/// Shared alphabet for the pattern and the match text, so the escape char
 /// frequently matches characters in the pattern and the consume/unterminated
 /// branches fire. Includes the LIKE metacharacters and a few literals.
 const ALPHA: &[char] = &['%', '_', '\\', 'a', 'b', 'c'];
 
-fn gen_pattern(u: &mut Unstructured) -> arbitrary::Result {
-    let mut p = String::new();
+/// Alphabet for the escape char: `ALPHA` minus the default escape `\`.
+/// `EscapeBehavior::Char('\\')` is an identity passthrough that never enters the
+/// custom-escape state machine, so drawing it here would spend executions on a
+/// two-line branch. `\` stays in `ALPHA` because a `\` in the *pattern* is what
+/// drives the doubling branch of the rewrite.
+const ESCAPE_ALPHA: &[char] = &['%', '_', 'a', 'b', 'c'];
+
+fn gen_over_alpha(u: &mut Unstructured) -> arbitrary::Result {
+    let mut s = String::new();
     for _ in 0..u.int_in_range(0usize..=20)? {
-        p.push(*u.choose(ALPHA)?);
+        s.push(*u.choose(ALPHA)?);
     }
-    Ok(p)
+    Ok(s)
 }
 
 fn run(mut u: Unstructured) -> arbitrary::Result<()> {
     let case_insensitive = u.arbitrary()?;
 
-    let (pattern, escape_char) = if u.int_in_range(0u8..=3)? == 0 {
-        // Some fully-arbitrary inputs keep the raw-Unicode reject coverage.
-        (u.arbitrary::()?, u.arbitrary::()?)
+    let (pattern, escape_char, text) = if u.int_in_range(0u8..=3)? == 0 {
+        // Some fully-arbitrary inputs keep the raw-Unicode reject coverage, and
+        // give `is_match` text long enough to expose the super-linear
+        // backtracking in the string matcher.
+        (
+            u.arbitrary::()?,
+            u.arbitrary::()?,
+            u.arbitrary::()?,
+        )
     } else {
-        (gen_pattern(&mut u)?, *u.choose(ALPHA)?)
+        // Text from the same alphabet so the compiled matcher can match.
+        (
+            gen_over_alpha(&mut u)?,
+            *u.choose(ESCAPE_ALPHA)?,
+            gen_over_alpha(&mut u)?,
+        )
     };
-    // Text from the same literal-ish alphabet so the compiled matcher can match.
-    let mut text = String::new();
-    for _ in 0..u.int_in_range(0usize..=20)? {
-        text.push(*u.choose(ALPHA)?);
-    }
 
     for behavior in [EscapeBehavior::Char(escape_char), EscapeBehavior::Disabled] {
         let Ok(normalized) = like_pattern::normalize_pattern(&pattern, behavior) else {
             continue;
         };
-        // The rewritten pattern is in default-escape form and must compile and
-        // match arbitrary text without panicking.
-        if let Ok(matcher) = like_pattern::compile(&normalized, case_insensitive) {
-            let _ = matcher.is_match(&text);
+        // `normalize_pattern` emits `\` only as part of a completed escape pair,
+        // so its output is well-formed default-escape form and the documented
+        // size limit is the only legitimate rejection. Anything else, notably
+        // the `EvalError::Internal` that `build_regex` raises on a regex it
+        // failed to build, means the rewrite produced a pattern the compiler
+        // cannot read. `EscapeBehavior::Char('\\')` is an identity passthrough,
+        // so there a lone trailing `\` from the input pattern reaches `compile`
+        // unchanged and rejecting it is correct.
+        let passthrough = matches!(behavior, EscapeBehavior::Char('\\'));
+        match like_pattern::compile(&normalized, case_insensitive) {
+            Ok(matcher) => {
+                let _ = matcher.is_match(&text);
+            }
+            Err(EvalError::LikePatternTooLong) => {}
+            Err(EvalError::UnterminatedLikeEscapeSequence) if passthrough => {}
+            Err(e) => panic!(
+                "normalize_pattern({pattern:?}, {behavior:?}) = {normalized:?} \
+                 failed to compile: {e:?}"
+            ),
+        }
+    }
+
+    // Escaping every metacharacter in `text` with `escape_char`, plus
+    // `escape_char` itself, yields a pattern that normalizes to a pure literal.
+    // The construction holds for every `escape_char`, including when it is
+    // itself `%`, `_` or `\`: `escape_char` is only ever emitted as the
+    // immediate prefix of the character it escapes, so `normalize_pattern`'s
+    // left-to-right pairing lines up exactly with it and no unescaped wildcard
+    // survives. It also can never end in a dangling escape, which is why
+    // normalization here must succeed.
+    let mut literal = String::with_capacity(2 * text.len());
+    for c in text.chars() {
+        if matches!(c, '%' | '_' | '\\') || c == escape_char {
+            literal.push(escape_char);
+        }
+        literal.push(c);
+    }
+    let normalized = like_pattern::normalize_pattern(&literal, EscapeBehavior::Char(escape_char))
+        .expect("every escape in `literal` is terminated by construction");
+    match like_pattern::compile(&normalized, case_insensitive) {
+        Ok(matcher) => {
+            assert!(
+                matcher.is_match(&text),
+                "literal pattern {literal:?} (escape {escape_char:?}) normalized to \
+                 {normalized:?} must match its source text {text:?}"
+            );
+            // And nothing else: the pattern is anchored at both ends and every
+            // one of its characters consumes exactly one text character, under
+            // `ILIKE` too, since simple case folding maps a character to other
+            // single characters. So appending anything must break the match.
+            let longer = format!("{text}a");
+            assert!(
+                !matcher.is_match(&longer),
+                "literal pattern {literal:?} (escape {escape_char:?}) normalized to \
+                 {normalized:?} must not match {longer:?}"
+            );
         }
+        Err(EvalError::LikePatternTooLong) => {}
+        Err(e) => panic!(
+            "literal pattern {literal:?} (escape {escape_char:?}) normalized to \
+             {normalized:?} failed to compile: {e:?}"
+        ),
     }
     Ok(())
 }
diff --git a/src/expr/fuzz/fuzz_targets/mfp_optimize.rs b/src/expr/fuzz/fuzz_targets/mfp_optimize.rs
index 1294d8397671e..e0c65a6e37b3f 100644
--- a/src/expr/fuzz/fuzz_targets/mfp_optimize.rs
+++ b/src/expr/fuzz/fuzz_targets/mfp_optimize.rs
@@ -33,13 +33,18 @@
 //!    twice-optimized plan, which can repair a first-pass miscompile that
 //!    production `into_plan`-on-raw callers still execute. `optimize` only
 //!    iterates while the expression size strictly decreases, so a fresh second
-//!    call is not guaranteed to be a no-op. The oracle is one-directional,
-//!    mirroring the contract optimize actually owes: optimize is allowed to
-//!    *drop* an error or a row that the raw MFP would reject, because it removes
-//!    unused map expressions and reorders predicates. But for every input row the
-//!    raw MFP passes through cleanly with output `out`, the optimized plan must
-//!    also pass it through with the byte-identical `out`. (When the raw MFP errors
-//!    or filters a row we assert nothing.)
+//!    call is not guaranteed to be a no-op. The oracle constrains both
+//!    directions of the filter, and neither direction of an error: optimize is
+//!    allowed to *drop* an error, because `remove_undemanded` deletes unused
+//!    (possibly fallible) map expressions, and it is allowed to *introduce* one,
+//!    because sorting predicates by position can surface an error the raw order
+//!    rejected the row before reaching. So when either side errors we assert
+//!    nothing. A row whose lazy evaluation absorbs an `AND`/`OR` operand error
+//!    is skipped outright, because `optimize` may legally surface that error or
+//!    fold on it (see `absorbs_and_or_operand_error`). Otherwise: a row the raw
+//!    MFP passes cleanly with output `out` the optimized plan must pass with
+//!    the byte-identical `out`, and a row the raw MFP cleanly filters the
+//!    optimized plan must not pass.
 //!
 //!  * **Temporal lowering.** Add predicates of the form `mz_now()  e` (and
 //!    conjunctions of them) over a bounded `mz_timestamp` expression `e`, then
@@ -51,18 +56,23 @@
 //!    at `T`" (`lower <= T < upper`, with the non-temporal predicates also
 //!    passing) agrees with a substitution reference: the same MFP with every
 //!    `mz_now()` replaced by the literal `mz_timestamp` `T`, evaluated
-//!    non-temporally. The compared timestamps are kept well below `u64::MAX` so
-//!    `StepMzTimestamp` (the `+1` the lowering inserts for `<=`/`>`/`=`) never
-//!    overflows, which keeps the substitution equivalence exact. As above the
-//!    oracle is one-directional: if the reference errors at `T` we assert
-//!    nothing.
+//!    non-temporally. The probed times are a random sample plus the interval's
+//!    own endpoints, because an off-by-one in the operator translation is
+//!    observable at an endpoint and nowhere else. The compared timestamps are
+//!    kept well below `u64::MAX` so `StepMzTimestamp` (the `+1` the lowering
+//!    inserts for `<=`/`>`/`=`) never overflows, which keeps the substitution
+//!    equivalence exact. As above the oracle is one-directional: if the reference
+//!    errors at `T` we assert nothing.
 
 #![no_main]
 
+use std::collections::{BTreeMap, BTreeSet};
+
 use libfuzzer_sys::arbitrary::{self, Arbitrary, Unstructured};
 use libfuzzer_sys::fuzz_target;
 use mz_expr::{
-    func, Eval, EvalError, MapFilterProject, MirScalarExpr, SafeMfpPlan, UnmaterializableFunc,
+    Eval, EvalError, MapFilterProject, MirScalarExpr, SafeMfpPlan, UnmaterializableFunc,
+    VariadicFunc, func,
 };
 use mz_repr::{Datum, Diff, ReprScalarType, Row, RowArena, Timestamp};
 
@@ -275,9 +285,13 @@ fn gen_ts_expr(u: &mut Unstructured, cols: &[Ty]) -> arbitrary::Result MirScalarExpr {
     MirScalarExpr::literal_ok(
@@ -335,13 +349,14 @@ fn reference_present(plan: &mz_expr::SafeMfpPlan, row: &Row) -> Option {
     }
 }
 
+type Interval = Option<(Timestamp, Option)>;
+
 /// Read the validity interval a temporal plan assigns to `row`, evaluating once
 /// with `time = 0`, `diff = +1`, and an always-valid frontier. Returns:
 ///  * `Err(())` if evaluation errored,
 ///  * `Ok(None)` if the (non-temporal part of the) plan rejected the row,
 ///  * `Ok(Some((lower, upper)))` for the half-open interval `[lower, upper)`
 ///    (`upper == None` means unbounded above).
-type Interval = Option<(Timestamp, Option)>;
 fn temporal_interval(plan: &mz_expr::MfpPlan, row: &Row) -> Result {
     let arena = RowArena::new();
     let mut datums: Vec = row.iter().collect();
@@ -419,6 +434,96 @@ fn eval_raw(mfp: &MapFilterProject, row: &Row) -> Option> {
     Some(Some(out))
 }
 
+/// Does evaluating `expr` on this row absorb an `And`/`Or` operand error?
+///
+/// This is `mir_scalar_reduce`'s `absorbs_and_or_operand_error`, the per-row
+/// refinement of `MirScalarExpr::could_hit_nonstrict_error_fold`, minus the
+/// `ErrorIfNull` arm for a function this target's vocabulary lacks. Rows it
+/// reports are skipped: reduce's error propagation may fold the absorbed error
+/// into the plan (CLU-137), and memoization may hoist a fallible subexpression
+/// shared under the `And`/`Or` into a mapped column that is evaluated eagerly,
+/// surfacing the error. Both outcomes are legal for `optimize`, because
+/// `AND`/`OR` evaluation order is undefined (STG-54), so such a row constrains
+/// nothing. See `run` for the two mechanisms side by side.
+///
+/// NOTE: this deliberately evaluates *every* operand of an `And`/`Or`, even
+/// though `And::eval` short-circuits on the first `false` and so may never
+/// reach a later erroring operand. Error propagation and memoization are both
+/// position-insensitive, so a short-circuit-aware version would report "no
+/// absorption" for rows the optimized plan nonetheless errors on.
+fn absorbs_and_or_operand_error<'a>(
+    expr: &'a MirScalarExpr,
+    datums: &[Datum<'a>],
+    arena: &'a RowArena,
+) -> bool {
+    match expr {
+        MirScalarExpr::Column(..)
+        | MirScalarExpr::Literal(..)
+        | MirScalarExpr::CallUnmaterializable(_) => false,
+        MirScalarExpr::CallUnary { expr, .. } => absorbs_and_or_operand_error(expr, datums, arena),
+        MirScalarExpr::CallBinary { expr1, expr2, .. } => {
+            absorbs_and_or_operand_error(expr1, datums, arena)
+                || absorbs_and_or_operand_error(expr2, datums, arena)
+        }
+        MirScalarExpr::CallVariadic { func, exprs } => {
+            let nested = exprs
+                .iter()
+                .any(|expr| absorbs_and_or_operand_error(expr, datums, arena));
+
+            if !matches!(func, VariadicFunc::And(_) | VariadicFunc::Or(_)) {
+                return nested;
+            }
+
+            let is_and = matches!(func, VariadicFunc::And(_));
+            let mut has_absorbing_value = false;
+            let mut has_error = false;
+            for expr in exprs {
+                match expr.eval(datums, arena) {
+                    Ok(Datum::False) if is_and => has_absorbing_value = true,
+                    Ok(Datum::True) if !is_and => has_absorbing_value = true,
+                    Err(_) => has_error = true,
+                    _ => {}
+                }
+            }
+
+            nested || (has_absorbing_value && has_error)
+        }
+        MirScalarExpr::If { cond, then, els } => {
+            if absorbs_and_or_operand_error(cond, datums, arena) {
+                return true;
+            }
+            match cond.eval(datums, arena) {
+                Ok(Datum::True) => absorbs_and_or_operand_error(then, datums, arena),
+                Ok(Datum::False | Datum::Null) => absorbs_and_or_operand_error(els, datums, arena),
+                _ => false,
+            }
+        }
+    }
+}
+
+/// `absorbs_and_or_operand_error` over every map and predicate of `mfp`, in the
+/// column context `eval_raw` evaluates them in.
+///
+/// A map expression that fails to re-evaluate counts as absorbing. That arm is
+/// reachable only for a row `eval_raw` filtered before reaching the map, and
+/// "assert nothing" is the safe answer for a row this function cannot model.
+fn mfp_absorbs_and_or_operand_error(mfp: &MapFilterProject, row: &Row) -> bool {
+    let arena = RowArena::new();
+    let mut datums: Vec = row.iter().collect();
+    for expr in &mfp.expressions {
+        if absorbs_and_or_operand_error(expr, &datums, &arena) {
+            return true;
+        }
+        match expr.eval(&datums, &arena) {
+            Ok(datum) => datums.push(datum),
+            Err(_) => return true,
+        }
+    }
+    mfp.predicates
+        .iter()
+        .any(|(_, predicate)| absorbs_and_or_operand_error(predicate, &datums, &arena))
+}
+
 /// Non-temporal preservation: raw unoptimized MFP semantics vs. the `optimize`d
 /// plan's output.
 fn run_nontemporal(
@@ -440,24 +545,52 @@ fn run_nontemporal(
     for _ in 0..ROWS_PER_MFP {
         let row = gen_input_row(u, types)?;
 
-        // Only a row the raw MFP passes through cleanly constrains the optimized
-        // plan. An error or a filtered row lets optimize legitimately differ.
-        let Some(Some(out_ref)) = eval_raw(&mfp, &row) else {
+        // A raw error constrains nothing: optimize may legitimately drop it.
+        let Some(raw) = eval_raw(&mfp, &row) else {
             continue;
         };
 
+        // Neither does a row that absorbs an `And`/`Or` operand error: optimize
+        // may legally turn its outcome into an error, a different value, or a
+        // different pass/fail. See `run`.
+        if mfp_absorbs_and_or_operand_error(&mfp, &row) {
+            continue;
+        }
+
         let arena = RowArena::new();
         let mut datums_p: Vec = row.iter().collect();
         let mut buf_p = Row::default();
-        match safe_opt.evaluate_into(&mut datums_p, &arena, &mut buf_p) {
-            Ok(Some(out)) => assert_eq!(
+        // Every panic prints both MFPs: which rewrite `optimize` got wrong is not
+        // recoverable from the row alone, and the crash artifact only replays
+        // under a `cargo fuzz` build.
+        let opt_mfp: &MapFilterProject = &safe_opt;
+        let opt = safe_opt.evaluate_into(&mut datums_p, &arena, &mut buf_p);
+        match (raw, opt) {
+            (Some(out_ref), Ok(Some(out))) => assert_eq!(
                 &out_ref, out,
-                "optimize changed the projected output\n  row = {row:?}\n  out_ref = {out_ref:?}\n  out_opt = {out:?}"
+                "optimize changed the projected output\n  row = {row:?}\n  out_ref = {out_ref:?}\n  out_opt = {out:?}\n  mfp = {mfp:?}\n  optimized = {opt_mfp:?}"
+            ),
+            (Some(_), Ok(None)) => panic!(
+                "optimize filtered out a row the raw MFP passed\n  row = {row:?}\n  mfp = {mfp:?}\n  optimized = {opt_mfp:?}"
             ),
-            Ok(None) => panic!("optimize filtered out a row the raw MFP passed\n  row = {row:?}"),
-            Err(e) => panic!(
-                "optimize errored on a row the raw MFP passed cleanly\n  row = {row:?}\n  err = {e:?}"
+            (Some(_), Err(e)) => panic!(
+                "optimize errored on a row the raw MFP passed cleanly\n  row = {row:?}\n  err = {e:?}\n  mfp = {mfp:?}\n  optimized = {opt_mfp:?}"
             ),
+            // The raw MFP cleanly filtered the row, so the optimized plan must
+            // not pass it. `optimize` never drops a predicate beyond
+            // `predicates.dedup()` on exact copies, and no rewrite can turn a
+            // predicate's `false`/`null` into `true`. Reordering only changes
+            // which predicate is reached first. The one rewrite that does change
+            // a value, replacing a mapped expression `e` with `#c` when a
+            // predicate `#c = e` is present, is value-preserving on any row where
+            // that predicate holds, and on a row where it does not the predicate
+            // itself is still there to reject the row.
+            (None, Ok(Some(out))) => panic!(
+                "optimize passed a row the raw MFP cleanly filtered\n  row = {row:?}\n  out_opt = {out:?}\n  mfp = {mfp:?}\n  optimized = {opt_mfp:?}"
+            ),
+            // Filtered on both sides, or the optimized plan errored where the raw
+            // order rejected the row first. Both are allowed.
+            (None, Ok(None)) | (None, Err(_)) => {}
         }
     }
     Ok(())
@@ -476,31 +609,53 @@ fn run_temporal(
         return Ok(());
     };
 
-    // Sample the logical times to probe, and build each substitution reference
-    // plan once (it depends only on `t`, not on the row). A reference plan that
-    // fails to lower is dropped: we simply assert nothing at that time.
-    let mut times: Vec<(u64, mz_expr::SafeMfpPlan)> = Vec::with_capacity(TIMES_PER_MFP);
+    // Logical times probed against every row.
+    let mut sampled = Vec::with_capacity(TIMES_PER_MFP);
     for _ in 0..TIMES_PER_MFP {
-        let t = u.int_in_range(0u64..=TS_BOUND + 2)?;
-        if let Some(ref_plan) = reference_plan(&mfp, t) {
-            times.push((t, ref_plan));
-        }
+        sampled.push(u.int_in_range(0u64..=TS_BOUND + 2)?);
     }
 
+    // A substitution reference depends only on `t`, not on the row, so it is
+    // built once per time and shared across rows. `None` records that lowering
+    // rejected it, meaning we assert nothing at that time. The map is filled
+    // lazily because the interval endpoints probed below are row-dependent.
+    let mut refs: BTreeMap> = BTreeMap::new();
+
     for _ in 0..ROWS_PER_MFP {
         let row = gen_input_row(u, types)?;
-        let interval = temporal_interval(&plan, &row);
         // A plan that errors on this row lets the lowered plan legitimately
         // differ from the substitution reference, so assert nothing.
-        let interval = match interval {
-            Err(()) => continue,
-            Ok(iv) => iv,
+        let Ok(interval) = temporal_interval(&plan, &row) else {
+            continue;
         };
 
-        for (t, ref_plan) in × {
-            // One-directional: only constrain the plan when the reference is a
-            // clean pass/fail. A reference error lets the lowered plan differ.
-            let Some(present_ref) = reference_present(ref_plan, &row) else {
+        let mut times: BTreeSet = sampled.iter().copied().collect();
+        // Probe the interval's own endpoints, not just sampled times. An
+        // off-by-one in the lowering (a missing or spurious `StepMzTimestamp`,
+        // or a wrong width for `mz_now() = e`) moves an endpoint by one, so it
+        // is observable at that endpoint and at no other time. Sampled times
+        // reach an endpoint only incidentally, and mostly the endpoint 0, which
+        // is what every draw decodes to once a short input leaves `Unstructured`
+        // with no data. A nonzero endpoint needs the sample to hit it exactly.
+        if let Some((lower, upper)) = &interval {
+            for t in [Some(u64::from(*lower)), upper.map(u64::from)] {
+                let Some(t) = t else { continue };
+                times.insert(t);
+                if let Some(before) = t.checked_sub(1) {
+                    times.insert(before);
+                }
+            }
+        }
+
+        for t in times {
+            let ref_plan = refs.entry(t).or_insert_with(|| reference_plan(&mfp, t));
+            // One-directional: only constrain the plan when the reference lowers
+            // and is a clean pass/fail. A reference error lets the lowered plan
+            // differ.
+            let Some(present_ref) = ref_plan
+                .as_ref()
+                .and_then(|ref_plan| reference_present(ref_plan, &row))
+            else {
                 continue;
             };
             let present_plan = match &interval {
@@ -508,7 +663,7 @@ fn run_temporal(
                 // failed): it is absent at every `t`.
                 None => false,
                 Some((lower, upper)) => {
-                    let tt = Timestamp::new(*t);
+                    let tt = Timestamp::new(t);
                     *lower <= tt && upper.map(|up| tt < up).unwrap_or(true)
                 }
             };
@@ -563,6 +718,36 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
         .filter(filters)
         .project(projection);
 
+    // Two mechanisms let `optimize` legally change what a row yields once lazy
+    // `And`/`Or` evaluation absorbs an operand error (`Or::eval` returns `true`
+    // the moment it sees one, dropping any error it collected, and `And::eval`
+    // does the same for `false`). Reduce's generic error propagation, run by
+    // `optimize` on every expression, replaces the whole call with any
+    // operand's literal error even though `eval` would have absorbed it (the
+    // open bug CLU-137), and the folded literal's non-nullable type can license
+    // a rewrite above it into a different projected value or a dropped row. And
+    // memoization hoists a fallible subexpression shared under an `And`/`Or`
+    // into a mapped column that `evaluate_inner` runs eagerly, surfacing the
+    // absorbed error. The latter is not a bug: `AND`/`OR` evaluation order is
+    // undefined, in Materialize as in Postgres (STG-54), so the surfaced error
+    // is a legal outcome of the same plan.
+    //
+    // Non-temporal mode handles both row-precisely, skipping exactly the rows
+    // that absorb an operand error (see `absorbs_and_or_operand_error`).
+    // Temporal mode skips the whole MFP on the coarse, expression-level
+    // predicate: its reference side substitutes a literal for `mz_now()` and
+    // re-runs `optimize` per probed time, so "absorbs on this row" has no
+    // single answer there.
+    if temporal
+        && mfp
+            .expressions
+            .iter()
+            .chain(mfp.predicates.iter().map(|(_, p)| p))
+            .any(|e| e.could_hit_nonstrict_error_fold())
+    {
+        return Ok(());
+    }
+
     if temporal {
         run_temporal(u, mfp, &types)
     } else {
diff --git a/src/expr/fuzz/fuzz_targets/mir_scalar_reduce.rs b/src/expr/fuzz/fuzz_targets/mir_scalar_reduce.rs
index cc002a60a841d..975bd23ab40e6 100644
--- a/src/expr/fuzz/fuzz_targets/mir_scalar_reduce.rs
+++ b/src/expr/fuzz/fuzz_targets/mir_scalar_reduce.rs
@@ -37,17 +37,36 @@
 //! batch of random rows. The check is mostly one-directional: reduce is allowed
 //! to *eliminate* a runtime error (e.g. `If(c, x, x)` becomes `x`, dropping `c`,
 //! and `x AND false` becomes `false`), so we only require that a successful
-//! `Ok(v)` result is preserved exactly. The exception is non-strict AND/OR
-//! error absorption, where the oracle allows a reduced error if the original
-//! row succeeded by absorbing an operand error. Every value type compared is
-//! exact (no float/numeric normalization), so equality is the right oracle.
+//! `Ok(v)` result is preserved exactly. Every value type compared is exact (no
+//! float/numeric normalization), so equality is the right oracle.
+//!
+//! The exception is a row that absorbs a non-strict AND/OR operand error, the
+//! shape of the open bug CLU-137: `Or::eval` returns `true` the moment it sees a
+//! true operand and drops any error it collected, while reduce's error
+//! propagation replaces the whole call with the operand's literal error. Such a
+//! row is skipped outright.
+//!
+//! NOTE: this carve-out used to be gated on the reduced side also being an error,
+//! on the reasoning that "an absorbed error cannot resurface as a value". That is
+//! false, and the fuzzer falsified it: once the absorbed error becomes a
+//! `Literal(Err)`, every rewrite downstream is reading a value the original never
+//! produced, and some of them yield an `Ok` rather than propagating. The observed
+//! case reduced `CastBoolToString(IsNull(If(..)))` from `Ok("true")` to a literal
+//! `Ok("false")`, with `Or([Literal(Err), Column(3)])` folding to `Literal(Err)`
+//! where it evaluates to `Ok(True)`. The wrong value is downstream of CLU-137,
+//! not a second defect, so the whole row goes.
+//!
+//! Leaf and atom distributions are tuned against the folds rather than left
+//! uniform, because a literal-heavy expression is annihilated by constant
+//! folding, null propagation, or error propagation long before it reaches the
+//! rewrites above. See `gen_leaf` and `gen_bool_atoms`.
 
 #![no_main]
 
 use libfuzzer_sys::arbitrary::{self, Arbitrary, Unstructured};
 use libfuzzer_sys::fuzz_target;
 use mz_expr::func::variadic::{And, Or};
-use mz_expr::{func, Eval, EvalError, MirScalarExpr, VariadicFunc};
+use mz_expr::{Eval, EvalError, MirScalarExpr, VariadicFunc, func};
 use mz_repr::{Datum, ReprColumnType, ReprScalarType, Row, RowArena};
 
 // Column layout: a contiguous block per type. Columns are nullable.
@@ -137,10 +156,20 @@ fn nonstr_datum(u: &mut Unstructured, ty: Ty) -> arbitrary::Result arbitrary::Result {
     let st = scalar_ty(ty);
-    Ok(match u.int_in_range(0u8..=3)? {
-        0 => {
+    Ok(match u.int_in_range(0u8..=5)? {
+        0..=2 => {
             let col = match ty {
                 Ty::Int => COL_INT0 + u.int_in_range(0..=N_INT - 1)?,
                 Ty::Long => COL_LONG0 + u.int_in_range(0..=N_LONG - 1)?,
@@ -149,14 +178,14 @@ fn gen_leaf(u: &mut Unstructured, ty: Ty) -> arbitrary::Result {
             };
             MirScalarExpr::column(col)
         }
-        1 => match ty {
+        3 => match ty {
             Ty::Str => {
                 let s = gen_string(u)?;
                 MirScalarExpr::literal_ok(Datum::String(&s), st)
             }
             _ => MirScalarExpr::literal_ok(nonstr_datum(u, ty)?, st),
         },
-        2 => MirScalarExpr::literal_null(st),
+        4 => MirScalarExpr::literal_null(st),
         // An error literal exercises reduce's error propagation/ordering.
         _ => MirScalarExpr::literal(Err(EvalError::DivisionByZero), st),
     })
@@ -173,11 +202,27 @@ fn gen_bool_atoms(u: &mut Unstructured, depth: u32) -> arbitrary::Result arbitrary::Result {
-            match u.int_in_range(0u8..=4)? {
+            match u.int_in_range(0u8..=7)? {
                 0 => {
                     let cond = gen_expr(u, Ty::Bool, d)?;
                     let then = gen_expr(u, Ty::Long, d)?;
                     let els = gen_expr(u, Ty::Long, d)?;
                     Ok(cond.if_then_else(then, els))
                 }
+                // int8 arithmetic, mirroring the int4 vocabulary: `%` divides by
+                // zero and negate/abs overflow at `i64::MIN`.
                 1 => Ok(gen_expr(u, Ty::Long, d)?
                     .call_binary(gen_expr(u, Ty::Long, d)?, func::AddInt64)),
                 2 => Ok(gen_expr(u, Ty::Long, d)?
                     .call_binary(gen_expr(u, Ty::Long, d)?, func::SubInt64)),
                 3 => Ok(gen_expr(u, Ty::Long, d)?
                     .call_binary(gen_expr(u, Ty::Long, d)?, func::MulInt64)),
+                4 => Ok(gen_expr(u, Ty::Long, d)?
+                    .call_binary(gen_expr(u, Ty::Long, d)?, func::ModInt64)),
+                5 => Ok(gen_expr(u, Ty::Long, d)?.call_unary(func::NegInt64)),
+                6 => Ok(gen_expr(u, Ty::Long, d)?.call_unary(func::AbsInt64)),
                 // Casts that produce an int8.
                 _ => match u.int_in_range(0u8..=1)? {
                     0 => Ok(gen_expr(u, Ty::Int, d)?.call_unary(func::CastInt32ToInt64)),
@@ -347,6 +398,25 @@ fn gen_row(u: &mut Unstructured) -> arbitrary::Result {
     Ok(row)
 }
 
+/// Does evaluating `expr` on this row absorb an `And`/`Or` operand error?
+///
+/// This is the per-row refinement of `MirScalarExpr::could_hit_nonstrict_error_fold`,
+/// which answers the same question for a whole expression. The coarse predicate is
+/// what the plan-level oracles use; here a row-precise answer is worth the extra
+/// code, because the CLU-137 shape is common in this generator and skipping every
+/// expression carrying it would cost most of the comparisons.
+///
+/// It models `And` and `Or`, the only non-strict variadics this target generates.
+/// `ErrorIfNull`, the third one the shared predicate knows about, is absent from
+/// the vocabulary above; adding it there needs a matching arm here.
+///
+/// NOTE: this deliberately evaluates *every* operand of an `And`/`Or`, even
+/// though `And::eval` short-circuits on the first `false` and so may never reach
+/// a later erroring operand. Reduce's error propagation is equally
+/// position-insensitive (it fires on any literal-error operand, wherever it
+/// sits), so a short-circuit-aware version here would report "no absorption" for
+/// rows whose reduced form is nonetheless an error, and the oracle would flag
+/// them.
 fn absorbs_and_or_operand_error<'a>(
     expr: &'a MirScalarExpr,
     datums: &[Datum<'a>],
@@ -413,12 +483,15 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
         let folded = reduced.eval(&datums, &arena);
         // The invariant is one-directional. `reduce` is permitted to eliminate
         // a runtime error, for example when `reduce_if` collapses `If(c, x, x)`
-        // to `x` and drops `c`. So an `Err` original may become anything. The
-        // known exception in the other direction: non-strict AND/OR error
-        // absorption, where a successful `false AND ` or
-        // `true OR ` may reduce through the absorbed error. Outer
-        // expressions can then fold that error into another value, for example
-        // `IS NULL` reducing to `false`, so skip the whole known shape.
+        // to `x` and drops `c`. So an `Err` original may become anything.
+        //
+        // A row that absorbed a non-strict AND/OR operand error is skipped
+        // entirely, not merely allowed to reduce to an error. Reduce's error
+        // propagation is not absorption-aware (CLU-137), so from the moment it
+        // installs that `Literal(Err)` the reduced expression is evaluating
+        // something the original never computed, and the divergence surfaces as a
+        // wrong *value* as readily as an error. See the module doc for the case
+        // that showed it.
         if original.is_ok() {
             if absorbs_and_or_operand_error(&expr, &datums, &arena) {
                 continue;
diff --git a/src/expr/fuzz/fuzz_targets/timezone_convert.rs b/src/expr/fuzz/fuzz_targets/timezone_convert.rs
index 80f7651d63f7e..7dc0d154d8afb 100644
--- a/src/expr/fuzz/fuzz_targets/timezone_convert.rs
+++ b/src/expr/fuzz/fuzz_targets/timezone_convert.rs
@@ -19,6 +19,11 @@
 //! barely run. So most of the time we pick a real IANA zone *with DST* (and a
 //! few fixed offsets) so the transition math actually executes. A minority arm
 //! still feeds an arbitrary string to keep the parser's reject paths covered.
+//!
+//! The result is packed into a `Row` and read back rather than dropped: the
+//! leap-second bugs this math is prone to produce an out-of-contract
+//! `NaiveTime` that `chrono` hands back without complaint, and only panic once
+//! something decodes a `Row` holding it. Evaluating alone would miss them.
 
 #![no_main]
 
@@ -28,7 +33,7 @@ use libfuzzer_sys::fuzz_target;
 use mz_expr::{Eval, MirScalarExpr, UnaryFunc, func};
 use mz_pgtz::timezone::{Timezone, TimezoneSpec};
 use mz_repr::adt::timestamp::CheckedTimestamp;
-use mz_repr::{Datum, ReprScalarType, RowArena};
+use mz_repr::{Datum, ReprScalarType, Row, RowArena};
 
 /// Real zones whose offsets shift (DST / sub-hour / historical), plus a couple
 /// of fixed offsets, the inputs that actually exercise the conversion math.
@@ -46,6 +51,11 @@ const ZONES: &[&str] = &[
     "UTC",
     "+05:30",
     "-08",
+    // Sub-minute offset. The whole-minute offsets above can only ever move a
+    // leap second to another `:59`, so they never reach the folding branch in
+    // `checked_{add,sub}_with_leapsecond`. The named zones do reach it, but
+    // only at their pre-standardization LMT offsets.
+    "+00:00:01",
 ];
 
 fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
@@ -67,8 +77,32 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
         };
         tz
     };
-    let secs = u.int_in_range(-8_000_000_000_000i64..=8_000_000_000_000)?;
-    let nanos = u.int_in_range(0u32..=999_999_999)?;
+    // 3-in-4 draws land in 1900-2100, where the DST transitions of the zones
+    // above actually live. A uniform draw over the whole representable range
+    // (±253,000 years) would hit an ambiguous or nonexistent local time with
+    // probability ~5e-8. The wide arm bottoms out at `LOW_DATE` (-4713-12-31,
+    // -210_863_606_400): `CheckedTimestamp` rejects everything below that, so
+    // drawing down to chrono's own limit instead would leave roughly half of
+    // all iterations evaluating nothing at all.
+    let mut secs = if u.int_in_range(0u8..=3)? != 0 {
+        u.int_in_range(-2_208_988_800i64..=4_102_444_800)?
+    } else {
+        u.int_in_range(-210_863_606_400i64..=8_000_000_000_000)?
+    };
+    // chrono encodes a leap second as `nanos >= 1_000_000_000`, and only accepts
+    // that encoding when the second-of-minute is 59. That encoding is exactly
+    // what the folding in `checked_{add,sub}_with_leapsecond` exists for, so
+    // construct it deliberately rather than hoping a uniform `nanos` draw lands
+    // there (it never can) or that `secs` happens to be aligned.
+    let nanos = if bool::arbitrary(u)? {
+        // `DateTime::from_timestamp` derives the second-of-day as
+        // `secs.rem_euclid(86_400)`, and `86_400 % 60 == 0`, so aligning `secs`
+        // on `rem_euclid(60) == 59` satisfies chrono's precondition.
+        secs = secs - secs.rem_euclid(60) + 59;
+        u.int_in_range(1_000_000_000u32..=1_999_999_999)?
+    } else {
+        u.int_in_range(0u32..=999_999_999)?
+    };
     let Some(dt) = DateTime::from_timestamp(secs, nanos) else {
         return Ok(());
     };
@@ -78,14 +112,20 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
     if let Ok(ts) = CheckedTimestamp::from_timestamplike(dt.naive_utc()) {
         let expr = MirScalarExpr::literal_ok(Datum::Timestamp(ts), ReprScalarType::Timestamp)
             .call_unary(UnaryFunc::TimezoneTimestamp(func::TimezoneTimestamp(tz)));
-        let _ = expr.eval(&[], &arena);
+        if let Ok(d) = expr.eval(&[], &arena) {
+            let _ = Row::pack_slice(&[d]).unpack_first();
+        }
     }
 
     // TIMESTAMPTZ `AT TIME ZONE tz` -> TIMESTAMP.
     if let Ok(tstz) = CheckedTimestamp::from_timestamplike(dt) {
         let expr = MirScalarExpr::literal_ok(Datum::TimestampTz(tstz), ReprScalarType::TimestampTz)
-            .call_unary(UnaryFunc::TimezoneTimestampTz(func::TimezoneTimestampTz(tz)));
-        let _ = expr.eval(&[], &arena);
+            .call_unary(UnaryFunc::TimezoneTimestampTz(func::TimezoneTimestampTz(
+                tz,
+            )));
+        if let Ok(d) = expr.eval(&[], &arena) {
+            let _ = Row::pack_slice(&[d]).unpack_first();
+        }
     }
     Ok(())
 }
diff --git a/src/expr/fuzz/like_pattern_compile.dict b/src/expr/fuzz/like_pattern_compile.dict
new file mode 100644
index 0000000000000..68ef0f068b31d
--- /dev/null
+++ b/src/expr/fuzz/like_pattern_compile.dict
@@ -0,0 +1,20 @@
+# libFuzzer dictionary for the like_pattern_compile target, resolved by
+# `dict_for` in test/cargo-fuzz/mzcompose.py.
+#
+# These tokens pay off only on the target's fully-arbitrary arm, where the input
+# bytes become the pattern and the text directly. Random Unicode there almost
+# never contains a LIKE metacharacter, so that arm stays on trivial literal
+# patterns without help. On the structured arm the input is a recipe of generator
+# choices, and a token is just more choice bytes carrying no structural signal.
+
+# The metacharacters, and an escape sequence.
+"%"
+"_"
+"\\"
+"\\%"
+# Two `%` with non-empty suffixes: the shape that routes to the regex engine.
+"%a%b"
+# Multi-byte literals, so byte offsets and character counts diverge in the
+# `%` backtracking: é (U+00E9, 2 bytes) and 漢 (U+6F22, 3 bytes).
+"\xc3\xa9"
+"\xe6\xbc\xa2"
diff --git a/src/expr/src/scalar.rs b/src/expr/src/scalar.rs
index 37d325c422922..1be02a7ee0144 100644
--- a/src/expr/src/scalar.rs
+++ b/src/expr/src/scalar.rs
@@ -1407,6 +1407,44 @@ impl VisitChildren for MirScalarExpr {
 }
 
 impl MirScalarExpr {
+    /// Reports whether this expression contains a non-strict variadic call with an
+    /// operand that could error, the shape of the open bug CLU-137.
+    ///
+    /// `And`, `Or` and `ErrorIfNull` do not evaluate every operand: `Or::eval`
+    /// returns `true` the moment it sees a true operand and drops any error it
+    /// collected, `And::eval` does the same for `false`, and `ErrorIfNull`
+    /// evaluates its message operand only when the first operand is NULL. Yet
+    /// `reduce_call_variadic`'s generic error propagation replaces the whole call
+    /// with any operand's literal error, wherever it sits. So `reduce` can turn a
+    /// row the expression should evaluate into an error, and, because that literal
+    /// is typed non-nullable, it can go on to license a nullability-dependent
+    /// rewrite and yield a different *value* rather than an error.
+    ///
+    /// Fuzz oracles that compare evaluation across `reduce` use this to skip the
+    /// shape rather than rediscover CLU-137 on every run. It lives here, next to
+    /// the fold it describes, so the several oracles that need it cannot drift
+    /// apart on which functions count as non-strict.
+    ///
+    /// Deliberately conservative: it asks whether an operand *could* error rather
+    /// than whether it already holds a literal error, because `reduce` folds a
+    /// column-free fallible operand (`1 / 0`) to a literal error first and absorbs
+    /// it after.
+    pub fn could_hit_nonstrict_error_fold(&self) -> bool {
+        let mut hit = false;
+        self.visit_pre(|e| {
+            if let MirScalarExpr::CallVariadic { func, exprs } = e {
+                let non_strict = matches!(
+                    func,
+                    VariadicFunc::And(_) | VariadicFunc::Or(_) | VariadicFunc::ErrorIfNull(_)
+                );
+                if non_strict && exprs.iter().any(|operand| operand.could_error()) {
+                    hit = true;
+                }
+            }
+        });
+        hit
+    }
+
     /// Iterates through references to child expressions.
     pub fn children(&self) -> impl DoubleEndedIterator {
         let mut first = None;
diff --git a/src/interchange/fuzz/fuzz_targets/avro_decode_fuzzed_schema.rs b/src/interchange/fuzz/fuzz_targets/avro_decode_fuzzed_schema.rs
index 73aa7da8de494..12f646698c7dd 100644
--- a/src/interchange/fuzz/fuzz_targets/avro_decode_fuzzed_schema.rs
+++ b/src/interchange/fuzz/fuzz_targets/avro_decode_fuzzed_schema.rs
@@ -25,9 +25,20 @@
 //! and *Avro-binary-encode a random value against that same type*, so the body
 //! is valid by construction and the decoder walks all the way through. Coverage
 //! guidance then learns which byte streams produce which shapes. We don't lose
-//! the error-path coverage a random body gave, though: a quarter of the inputs
-//! feed the raw remaining bytes, and others truncate or single-byte-corrupt the
-//! valid encoding. Either way, an accepted schema must never panic.
+//! the error-path coverage a random body gave, though: up to a quarter of the
+//! inputs feed the raw remaining bytes, and others truncate or
+//! single-byte-corrupt the valid encoding. Either way, an accepted schema must
+//! never panic.
+//!
+//! NOTE: every "don't build a valid body" branch must be gated on there being
+//! input left. `Unstructured::int_in_range` does not fail on an exhausted
+//! `Unstructured`, it returns the *low end* of the range forever, and schema
+//! generation consumes input greedily, so an ungated `int_in_range(0..=3)? == 0`
+//! branch is taken by every exhausted input. That funnelled the majority of
+//! executions into a zero-length body (`take_rest` on nothing, or
+//! truncate-to-zero), which fails at the first `read_exact` and walks none of
+//! the deep decode logic this target exists to reach. Exhaustion must fall
+//! through to the clean-encoding path, never select a degenerate one.
 //!
 //! Beyond "never panic", we add one error oracle. Most bodies *should* be
 //! rejected (random bytes, truncations, an out-of-range `enum` index, an
@@ -37,8 +48,21 @@
 //! (plain scalars, `fixed`, and structural composites over them, see
 //! `decode_infallible`), the decoder is round-tripping bytes it *must* accept,
 //! so there a decode error is a real bug and we assert success. A panic-only
-//! oracle never notices a "valid input wrongly rejected" regression (cf.
-//! #37087's deferred union-promotion error).
+//! oracle never notices a "valid input wrongly rejected" regression.
+//!
+//! NOTE: the scope of that oracle is the *identity-resolution* decode path only.
+//! We build the decoder with `WriterSchemaProvider::None`, for which
+//! `AvroSchemaResolver::resolve` hands back the reader schema verbatim, so no
+//! `SchemaPiece::Resolve*` piece is ever constructed. The deferred
+//! union-promotion class of regression (#37087), where a failed writer/reader
+//! variant pairing is stored as an `Err` inside the resolved union and re-raised
+//! at decode time, lives entirely in that resolution machinery and cannot be
+//! reached from here. It is covered by
+//! `src/avro/fuzz/fuzz_targets/schema_resolve.rs`, which fuzzes `resolve_schemas`
+//! against `from_avro_datum`. Reaching it through the interchange-side
+//! Row-packing decoder would need a separate target that builds the resolved
+//! schema with `resolve_schemas` and hands it to `AvroFlatDecoder` directly,
+//! since `Decoder` offers no way to inject a writer schema without a registry.
 //!
 //! A fraction of inputs instead drive a round-trip *correctness* oracle
 //! (`run_roundtrip`): a decode that succeeds but yields the *wrong* datum slips
@@ -49,10 +73,12 @@
 //!
 //! Rather than freeze the schema-dependent knobs at one value each, we vary the
 //! ones that drive distinct decode arithmetic: `decimal` precision (1..=39, the
-//! `NUMERIC_DATUM_MAX_PRECISION` boundary), scale (0..=precision, where
-//! `parse_decimal` rejects scale > precision and `twos_complement_be_to_numeric`
-//! interprets it), and the backing `fixed` size, so the two's-complement byte
-//! run and the precision/scale interaction are not pinned. The decimal
+//! `NUMERIC_DATUM_MAX_PRECISION` boundary, further capped by the backing `fixed`
+//! size so `parse_fixed` does not silently demote the decimal to a plain
+//! `fixed`), scale (0..=precision, where `parse_decimal` rejects scale >
+//! precision and `twos_complement_be_to_numeric` interprets it), and the backing
+//! `fixed` size, so the two's-complement byte run and the precision/scale
+//! interaction are not pinned. The decimal
 //! *value* bytes are likewise biased (see `push_twos_complement` /
 //! `gen_decimal_len`) toward the patterns that stress that arithmetic rather
 //! than only uniform-random runs: the empty run (== 0), all-`0x00`/`0xFF` sign
@@ -61,10 +87,23 @@
 //! `json` logical field (a `string` tagged `connect.name:io.debezium.data.Json`)
 //! whose body is real JSON text, reaching the `AvroFlatDecoder::json` ->
 //! `JsonbPacker` path that a plain string never touches. And multi-variant
-//! *essential* unions like `["int","string"]`, accepted only as a record field
-//! (each non-null variant expands to its own nullable column) and rejected
-//! elsewhere, exercising `get_union_columns`' field-invention/expansion logic
-//! that the `["null", T]` nullability pattern alone never reaches.
+//! *essential* unions like `["int","string"]`, which occupy one column per
+//! non-null variant, exercising the multi-column `AvroFlatDecoder::union_branch`
+//! arithmetic (one `Datum` pushed per non-null variant, `Datum::Null` for every
+//! unselected one) that the two-branch `["null", T]` pattern never reaches.
+//!
+//! NOTE: this target stops at `parse_schema`. `Decoder::new` ->
+//! `AvroSchemaResolver::new` -> `parse_schema` is the whole chain, so none of
+//! `src/interchange/src/avro/schema.rs`'s SQL-side validation runs here:
+//! `validate_schema_1` / `validate_schema_2` / `get_union_columns` are reachable
+//! only from `schema_to_relationdesc`, which nothing here calls. The union
+//! column-name invention (`get_union_columns`' `format!("{}{}", n, i + 1)`, the
+//! `UNKNOWN_COLUMN_NAME` fallback, the resulting `RelationDesc`) is therefore
+//! untested by this target. Where the generator does keep to a validator rule
+//! (essential unions only as record fields, decimal precision <= 39) the reason
+//! is that production runs `schema_to_relationdesc` before ever building a
+//! `Decoder`, so those are the only schemas the decoder sees in practice. It is
+//! not that this target would reject the others.
 
 #![no_main]
 
@@ -125,22 +164,39 @@ enum Ty {
     Nullable(Box),
     /// A multi-variant *essential* union of non-null variants, optionally with a
     /// leading `null`: e.g. `["int","string"]` or `["null","int","string"]`.
-    /// `validate_schema_2` rejects this everywhere except as a record field,
-    /// where `get_union_columns` expands it to one nullable column per non-null
-    /// variant. Stored as (has_null, variants). Generated only as a record field.
+    /// At decode time `AvroFlatDecoder::union_branch` packs one `Datum` per
+    /// non-null variant. Stored as (has_null, variants). Generated only as a
+    /// record field, because that is the only position the SQL validator accepts
+    /// it in and therefore the only one production reaches (this target does not
+    /// run that validator, see the module docs). Avro itself would accept the
+    /// same union anywhere a type is expected.
     EssentialUnion(bool, Vec),
 }
 
-/// Generate a `decimal`'s precision/scale. `parse_decimal` requires
-/// `0 <= scale <= precision` and the SQL validator caps precision at
-/// `NUMERIC_DATUM_MAX_PRECISION` (39). Pick within those bounds so the schema is
-/// accepted and we vary the decode arithmetic across the whole legal range.
-fn gen_decimal_params(u: &mut Unstructured) -> arbitrary::Result<(u32, u32)> {
-    let precision = u.int_in_range(1u32..=39)?;
+/// Materialize's `NUMERIC_DATUM_MAX_PRECISION`. The SQL Avro validator caps
+/// `decimal` precision here, and production only hands the decoder schemas that
+/// passed it, so stay within it even though this target never runs that check.
+const MAX_DECIMAL_PRECISION: u32 = 39;
+
+/// Generate a `decimal`'s precision/scale with precision in
+/// `1..=max_precision`. `parse_decimal` requires `0 <= scale <= precision`, so
+/// pick the scale from the chosen precision and vary the decode arithmetic
+/// across the whole legal range.
+fn gen_decimal_params(u: &mut Unstructured, max_precision: u32) -> arbitrary::Result<(u32, u32)> {
+    let precision = u.int_in_range(1u32..=max_precision)?;
     let scale = u.int_in_range(0u32..=precision)?;
     Ok((precision, scale))
 }
 
+/// The largest `decimal` precision `parse_fixed` will honour over a `size`-byte
+/// `fixed`, mirroring its own bound. Past it the logical type is silently
+/// demoted to a plain `fixed` (a `warn!`, not a parse error), so the schema
+/// stops exercising the decimal path and the harness is left believing a
+/// `DecimalFixed` node is a decimal when the decoder sees a `fixed`.
+fn max_precision_over_fixed(size: u32) -> u32 {
+    ((f64::from(8 * size - 1)) * 2f64.log10()).floor() as u32
+}
+
 /// Generate one syntactically valid Avro type. `counter` keeps named types
 /// (record/enum/fixed) unique within the schema, since duplicate names make Avro
 /// schema parsing fail, wasting the whole input. This never returns an
@@ -164,15 +220,19 @@ fn gen_ty(u: &mut Unstructured, counter: &mut u32, depth: u32) -> arbitrary::Res
             _ => Ty::Bytes,
         },
         1 => {
-            let (p, s) = gen_decimal_params(u)?;
+            let (p, s) = gen_decimal_params(u, MAX_DECIMAL_PRECISION)?;
             Ty::DecimalBytes(p, s)
         }
         2 => {
             *counter += 1;
-            let (p, s) = gen_decimal_params(u)?;
             // `fixed` requires a positive size. Allow runs both shorter and
             // longer than the canonical 16/24 to vary the two's-complement path.
             let size = u.int_in_range(1u32..=40)?;
+            // Bound the precision by the byte run, else `parse_fixed` demotes
+            // the decimal to a plain `fixed` and this node stops testing the
+            // decimal path at all (`Ty::Fixed` already covers plain runs).
+            let max = max_precision_over_fixed(size).min(MAX_DECIMAL_PRECISION);
+            let (p, s) = gen_decimal_params(u, max)?;
             Ty::DecimalFixed(*counter, size, p, s)
         }
         3 => Ty::Date,
@@ -181,7 +241,9 @@ fn gen_ty(u: &mut Unstructured, counter: &mut u32, depth: u32) -> arbitrary::Res
         6 => Ty::Uuid,
         7 => {
             *counter += 1;
-            let size = u.int_in_range(0u32..=24)?;
+            // `parse_fixed` rejects a non-positive size outright, which would
+            // fail `Decoder::new` and discard the whole input, schema and body.
+            let size = u.int_in_range(1u32..=24)?;
             Ty::Fixed(*counter, size)
         }
         8 => {
@@ -201,7 +263,18 @@ fn gen_ty(u: &mut Unstructured, counter: &mut u32, depth: u32) -> arbitrary::Res
         }
         11 => Ty::Array(Box::new(gen_ty(u, counter, depth - 1)?)),
         12 => Ty::Map(Box::new(gen_ty(u, counter, depth - 1)?)),
-        _ => Ty::Nullable(Box::new(gen_ty(u, counter, depth - 1)?)),
+        _ => {
+            // `UnionSchema::new` rejects `["null",["null",T]]` ("Unions may not
+            // directly contain a union"), which would fail `Decoder::new` and
+            // discard the whole input. Collapse rather than re-roll, so the
+            // input the inner type already consumed is not wasted either.
+            let inner = gen_ty(u, counter, depth - 1)?;
+            if matches!(inner, Ty::Nullable(_)) {
+                inner
+            } else {
+                Ty::Nullable(Box::new(inner))
+            }
+        }
     })
 }
 
@@ -377,7 +450,11 @@ fn encode_json(u: &mut Unstructured, out: &mut Vec) -> arbitrary::Result<()>
         }
         5 => "[]".into(),
         6 => "{}".into(),
-        7 => format!("[{},{},null]", u.arbitrary::()?, u.arbitrary::()?),
+        7 => format!(
+            "[{},{},null]",
+            u.arbitrary::()?,
+            u.arbitrary::()?
+        ),
         // Not valid JSON: exercise the decoder's BadJson error path.
         _ => "{".into(),
     };
@@ -392,7 +469,11 @@ fn encode_json(u: &mut Unstructured, out: &mut Vec) -> arbitrary::Result<()>
 /// (`len <= 17`), the `negate_twos_complement_le` path (any negative value), and
 /// the wide-representation precision-overflow handling. Most of the time it
 /// still emits a uniform-random run so coverage guidance keeps exploring.
-fn push_twos_complement(u: &mut Unstructured, len: usize, out: &mut Vec) -> arbitrary::Result<()> {
+fn push_twos_complement(
+    u: &mut Unstructured,
+    len: usize,
+    out: &mut Vec,
+) -> arbitrary::Result<()> {
     let fill = match u.int_in_range(0u8..=9)? {
         0 => 0x00u8, // zero / positive sign-extension
         1 => 0xFF,   // -1 / negative sign-extension
@@ -459,7 +540,11 @@ fn encode_value(u: &mut Unstructured, ty: &Ty, out: &mut Vec) -> arbitrary::
         // The fixed-backed decimal reads exactly `size` bytes (no length prefix),
         // so only the byte pattern varies.
         Ty::DecimalFixed(_, size, _, _) => {
-            push_twos_complement(u, usize::try_from(*size).expect("fixed size fits usize"), out)?;
+            push_twos_complement(
+                u,
+                usize::try_from(*size).expect("fixed size fits usize"),
+                out,
+            )?;
         }
         Ty::Json => encode_json(u, out)?,
         Ty::Uuid => {
@@ -703,10 +788,11 @@ fn run_roundtrip(u: &mut Unstructured) -> arbitrary::Result<()> {
     }
     schema.push_str("]}");
 
-    let Ok(mut decoder) = Decoder::new(&schema, &[], WriterSchemaProvider::None, "fuzz".into()) else {
-        // A record of plain scalars always validates. If not, nothing to check.
-        return Ok(());
-    };
+    // `Decoder::new` only parses the schema, and this one is built from a fixed
+    // template over plain scalars, so a parse failure is a harness bug rather
+    // than an input we should skip.
+    let mut decoder = Decoder::new(&schema, &[], WriterSchemaProvider::None, "fuzz".into())
+        .expect("plain-scalar record schema parses");
 
     let mut body = Vec::new();
     for (val, nullable) in &cols {
@@ -728,7 +814,9 @@ fn run_roundtrip(u: &mut Unstructured) -> arbitrary::Result<()> {
             "Avro decode produced the wrong row for a plain-scalar record\nschema = {schema}",
         ),
         Ok(Err(e)) => panic!("plain-scalar record failed to decode (schema = {schema}): {e}"),
-        Err(e) => panic!("plain-scalar record hit a transient decode error (schema = {schema}): {e}"),
+        Err(e) => {
+            panic!("plain-scalar record hit a transient decode error (schema = {schema}): {e}")
+        }
     }
     Ok(())
 }
@@ -741,8 +829,8 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
         return run_roundtrip(&mut u);
     }
 
-    // Top-level reader schema: a record whose fields span everything
-    // `validate_schema_2` accepts.
+    // Top-level reader schema: a record whose fields span every single-column
+    // shape the decoder handles.
     let mut counter = 0u32;
     let nfields = u.int_in_range(1u8..=8)?;
     let mut fields = Vec::with_capacity(nfields.into());
@@ -757,37 +845,51 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
 
     // No CSR client and confluent_wire_format = false, so decode is
     // self-contained (no network) over the generated reader schema.
-    let Ok(mut decoder) = Decoder::new(&schema, &[], WriterSchemaProvider::None, "fuzz".into()) else {
+    let Ok(mut decoder) = Decoder::new(&schema, &[], WriterSchemaProvider::None, "fuzz".into())
+    else {
         return Ok(());
     };
 
-    // Body: usually a valid encoding (so the decoder runs deep), but a quarter
-    // of the time the raw remaining bytes, and otherwise a valid encoding
-    // occasionally truncated or single-byte corrupted. The non-valid forms keep
-    // the decoder's error paths covered: short read, bad length/union tag,
-    // inconsistent content.
+    // Body: usually a valid encoding (so the decoder runs deep), but up to a
+    // quarter of the time the raw remaining bytes, and otherwise a valid
+    // encoding occasionally truncated or single-byte corrupted. The non-valid
+    // forms keep the decoder's error paths covered: short read, bad length/union
+    // tag, inconsistent content.
+    // Both "don't build a valid body" decisions are gated on input remaining,
+    // because an exhausted `Unstructured` returns the low end of every range and
+    // would otherwise steer the majority of executions into an empty body (see
+    // the module docs).
     // `assert_success` is set only when the body is a *clean* (uncorrupted)
     // encoding of a type tree every node of which is guaranteed to decode for
     // any value the encoder can produce (see `decode_infallible`). In that one
     // case the decoder is round-tripping bytes it must accept, so a decode
     // *error* is a real bug. The panic-only oracle that the random-bytes,
     // truncated, corrupted, and value-validated (`enum`/`decimal`/`json`/…)
-    // arms rely on would silently miss it, the same class of regression as the
-    // deferred union-promotion error in #37087.
+    // arms rely on would silently miss it. This covers the identity-resolution
+    // decode path only, see the module docs for the resolved-schema gap.
     let mut assert_success = false;
-    let body = if u.int_in_range(0u8..=3)? == 0 {
+    // Only take the raw tail when there actually is one to feed.
+    let body = if u.len() >= 8 && u.int_in_range(0u8..=3)? == 0 {
         u.take_rest().to_vec()
     } else {
         let mut b = Vec::new();
         encode_value(&mut u, &row, &mut b)?;
-        match u.int_in_range(0u8..=9)? {
-            0 if !b.is_empty() => {
-                let keep = u.int_in_range(0usize..=b.len())?;
+        let corruption = if u.is_empty() {
+            None
+        } else {
+            Some(u.int_in_range(0u8..=9)?)
+        };
+        match corruption {
+            // `0..=len - 1`, not `0..=len`: a `keep == len` draw would leave the
+            // body untouched, so the arm would not always truncate.
+            Some(0) if !b.is_empty() => {
+                let keep = u.int_in_range(0usize..=b.len() - 1)?;
                 b.truncate(keep);
             }
-            1 if !b.is_empty() => {
+            // A nonzero mask, so the arm always flips at least one bit.
+            Some(1) if !b.is_empty() => {
                 let i = u.int_in_range(0usize..=b.len() - 1)?;
-                b[i] ^= u.arbitrary::()?;
+                b[i] ^= u.int_in_range(1u8..=u8::MAX)?;
             }
             // Uncorrupted body: if its type is guaranteed decodable, the decode
             // below must succeed.
@@ -809,6 +911,15 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
                 "decoder rejected a clean, in-range Avro body whose every field is guaranteed \
                  decodable; such a round-trip must succeed",
             );
+        // `decode` reads through `bytes`, so a clean body must be consumed in
+        // full. A length-accounting bug that under-consumes while still packing
+        // a well-formed `Row` passes every other oracle here.
+        assert!(
+            bytes.is_empty(),
+            "decoder left {} of {} bytes of a clean Avro body unconsumed (schema = {schema})",
+            bytes.len(),
+            body.len(),
+        );
     }
     Ok(())
 }
diff --git a/src/interchange/fuzz/fuzz_targets/json_encode.rs b/src/interchange/fuzz/fuzz_targets/json_encode.rs
index f660328db5424..6bbdf039c839d 100644
--- a/src/interchange/fuzz/fuzz_targets/json_encode.rs
+++ b/src/interchange/fuzz/fuzz_targets/json_encode.rs
@@ -16,6 +16,13 @@
 //! that can't be serialized, corrupts/halts a sink, so encoding then serializing
 //! must never panic for any well-typed row.
 //!
+//! The blast radius is wider than the sinks. `TypedDatum::json` is also the
+//! result encoder for the HTTP `/api/sql` and WebSocket SQL APIs, which carry no
+//! `catch_unwind`, so a panic there aborts `environmentd` under
+//! `install_enhanced_handler` rather than just failing one sink task. Those
+//! callers pass `JsonNumberPolicy::ConvertNumberToString`, while
+//! `encode_datums_as_json` hardcodes `KeepAsNumber`, so we drive both.
+//!
 //! Beyond scalars, we generate *composite* column types (`List`, `Map`,
 //! `Record`, multi-dimensional `Array`, and `Jsonb`) plus the remaining scalar
 //! shapes with their own encode logic (`Uuid`, `Char`/`VarChar` padding,
@@ -33,15 +40,16 @@
 use chrono::{DateTime, NaiveTime, Utc};
 use libfuzzer_sys::arbitrary::{self, Arbitrary, Unstructured};
 use libfuzzer_sys::fuzz_target;
-use mz_interchange::json::encode_datums_as_json;
+use mz_interchange::encode::TypedDatum;
+use mz_interchange::json::{JsonNumberPolicy, ToJson, encode_datums_as_json};
 use mz_repr::adt::char::CharLength;
 use mz_repr::adt::date::Date;
 use mz_repr::adt::interval::Interval;
 use mz_repr::adt::jsonb::JsonbPacker;
-use mz_repr::adt::mz_acl_item::{AclItem, MzAclItem};
-use mz_repr::adt::range::Range;
+use mz_repr::adt::mz_acl_item::{AclItem, AclMode, MzAclItem};
+use mz_repr::adt::range::{Range, RangeLowerBound, RangeUpperBound};
 use mz_repr::adt::system::Oid;
-use mz_repr::adt::timestamp::CheckedTimestamp;
+use mz_repr::adt::timestamp::{CheckedTimestamp, HIGH_DATE, LOW_DATE};
 use mz_repr::adt::varchar::VarCharMaxLength;
 use mz_repr::role_id::RoleId;
 use mz_repr::strconv::parse_numeric;
@@ -67,29 +75,40 @@ enum GType {
     Array(Vec, SqlScalarType),
 }
 
+/// Draw a `DateTime` inside `CheckedTimestamp`'s bounds.
+///
+/// Drawing from the full `i64` (or even `±8e12`) second range spends most of the
+/// budget on values `from_timestamplike` rejects, and never lands on the
+/// boundary days. Drawing from `[LOW_DATE 00:00:00, HIGH_DATE 23:59:59]` instead
+/// makes every draw valid, so the two conversions below cannot fail: `secs` is
+/// well inside chrono's own year range, and `nanos` cannot carry past the end of
+/// `HIGH_DATE`.
+fn gen_ts(u: &mut Unstructured) -> arbitrary::Result> {
+    let low = LOW_DATE
+        .and_hms_opt(0, 0, 0)
+        .expect("midnight is a valid time")
+        .and_utc()
+        .timestamp();
+    let high = HIGH_DATE
+        .and_hms_opt(23, 59, 59)
+        .expect("23:59:59 is a valid time")
+        .and_utc()
+        .timestamp();
+    let secs = u.int_in_range(low..=high)?;
+    let nanos = u.int_in_range(0u32..=999_999_999)?;
+    Ok(DateTime::from_timestamp(secs, nanos).expect("secs drawn within chrono's range"))
+}
+
 fn gen_naive_ts(
     u: &mut Unstructured,
 ) -> arbitrary::Result> {
-    let secs = u.int_in_range(-8_000_000_000_000i64..=8_000_000_000_000)?;
-    let nanos = u.int_in_range(0u32..=999_999_999)?;
-    Ok(DateTime::from_timestamp(secs, nanos)
-        .and_then(|d| CheckedTimestamp::from_timestamplike(d.naive_utc()).ok())
-        .unwrap_or_else(|| {
-            CheckedTimestamp::from_timestamplike(
-                DateTime::from_timestamp(0, 0).unwrap().naive_utc(),
-            )
-            .unwrap()
-        }))
+    Ok(CheckedTimestamp::from_timestamplike(gen_ts(u)?.naive_utc())
+        .expect("drawn within CheckedTimestamp's bounds"))
 }
 
 fn gen_utc_ts(u: &mut Unstructured) -> arbitrary::Result>> {
-    let secs = u.int_in_range(-8_000_000_000_000i64..=8_000_000_000_000)?;
-    let nanos = u.int_in_range(0u32..=999_999_999)?;
-    Ok(DateTime::from_timestamp(secs, nanos)
-        .and_then(|d| CheckedTimestamp::from_timestamplike(d).ok())
-        .unwrap_or_else(|| {
-            CheckedTimestamp::from_timestamplike(DateTime::from_timestamp(0, 0).unwrap()).unwrap()
-        }))
+    Ok(CheckedTimestamp::from_timestamplike(gen_ts(u)?)
+        .expect("drawn within CheckedTimestamp's bounds"))
 }
 
 /// Generate a scalar type (a leaf of the composite tree, and the element type of
@@ -117,11 +136,27 @@ fn gen_scalar_type(u: &mut Unstructured) -> arbitrary::Result {
         18 => SqlScalarType::MzTimestamp,
         19 => SqlScalarType::Uuid,
         20 => {
-            let length = CharLength::try_from(i64::from(u.int_in_range(1u8..=12)?)).ok();
+            // An unqualified `bpchar` (`length: None`) reaches `format_str_pad`'s
+            // no-padding branch, which a length-qualified `Char` never does.
+            let length = if u.ratio(1u8, 4u8)? {
+                None
+            } else {
+                Some(
+                    CharLength::try_from(i64::from(u.int_in_range(1u8..=12)?))
+                        .expect("1..=12 is a valid char length"),
+                )
+            };
             SqlScalarType::Char { length }
         }
         21 => {
-            let max_length = VarCharMaxLength::try_from(i64::from(u.int_in_range(1u8..=12)?)).ok();
+            let max_length = if u.ratio(1u8, 4u8)? {
+                None
+            } else {
+                Some(
+                    VarCharMaxLength::try_from(i64::from(u.int_in_range(1u8..=12)?))
+                        .expect("1..=12 is a valid varchar max length"),
+                )
+            };
             SqlScalarType::VarChar { max_length }
         }
         // Range over Int32. `unwrap_range().to_string()` is the only logic.
@@ -160,7 +195,15 @@ fn gen_type(u: &mut Unstructured, depth: u32) -> arbitrary::Result {
             let ndims = u.int_in_range(1usize..=3)?;
             let mut dims = Vec::with_capacity(ndims);
             for _ in 0..ndims {
-                dims.push(u.int_in_range(0usize..=3)?);
+                // A single zero-length axis collapses the cardinality to 0, so
+                // `encode_array` walks no elements at all. Drawing lengths
+                // uniformly from `0..=3` made that the outcome for ~45% of
+                // arrays, so keep it reachable but rare.
+                dims.push(if u.ratio(1u8, 16u8)? {
+                    0
+                } else {
+                    u.int_in_range(1usize..=3)?
+                });
             }
             GType::Array(dims, gen_scalar_type(u)?)
         }
@@ -201,6 +244,26 @@ fn to_scalar_type(ty: >ype) -> SqlScalarType {
 }
 
 /// Build a small, valid JSON string of bounded depth for the `Jsonb` path.
+///
+/// TODO: Generate serde_json's arbitrary-precision magic key here once SS-157 is
+/// fixed. `JsonbPacker`'s `KeyClassifier` recognises the object key
+/// `$serde_json::private::Number` and runs the payload through
+/// `strconv::parse_numeric`, which accepts positive quiet NaN, bypassing the
+/// `cast_jsonbable_to_jsonb` guard that the SQL cast path relies on. That is the
+/// only route by which a non-finite numeric enters a `Jsonb`, and
+/// `JsonbRef::to_serde_json` then panics on it, so the literal
+/// `{"$serde_json::private::Number":"NaN"}` (and that value nested inside an
+/// array or object, to cover `JsonbDatum::serialize`'s list/map recursion) is
+/// what this generator has to produce to guard the fix. It is left out until the
+/// fix lands, because this target is in the `FRUITFUL` set and would otherwise
+/// report the same known crash on every release-qualification run.
+///
+/// Adding it also requires relaxing the `pack_str` call in `push_typed`: `"NaN"`
+/// is the only payload `parse_numeric` accepts, so `Infinity`, `sNaN`, `1e5000`
+/// and the like make `pack_str` return `Err`, which today's `.expect()` would
+/// turn into a *harness* panic. `pack_str` collects its commands before touching
+/// the packer, so a rejection leaves the packer untouched and a valid
+/// `Datum::JsonNull` can be substituted instead.
 fn gen_json_text(u: &mut Unstructured) -> arbitrary::Result {
     Ok(match u.int_in_range(0u8..=7)? {
         0 => "null".into(),
@@ -244,9 +307,9 @@ fn push_scalar(
         // represent, exactly the encoding edge we want to exercise.
         SqlScalarType::Float32 => packer.push(Datum::Float32(f32::arbitrary(u)?.into())),
         SqlScalarType::Float64 => packer.push(Datum::Float64(f64::arbitrary(u)?.into())),
-        SqlScalarType::String
-        | SqlScalarType::VarChar { .. }
-        | SqlScalarType::Char { .. } => packer.push(Datum::String(<&str>::arbitrary(u)?)),
+        SqlScalarType::String | SqlScalarType::VarChar { .. } | SqlScalarType::Char { .. } => {
+            packer.push(Datum::String(<&str>::arbitrary(u)?))
+        }
         SqlScalarType::Bytes => packer.push(Datum::Bytes(<&[u8]>::arbitrary(u)?)),
         SqlScalarType::Numeric { .. } => {
             let s = format!("{}.{}", i64::arbitrary(u)?, u32::arbitrary(u)?);
@@ -254,8 +317,10 @@ fn push_scalar(
             packer.push(Datum::Numeric(n));
         }
         SqlScalarType::Date => {
-            let d = Date::from_pg_epoch(i32::arbitrary(u)?)
-                .unwrap_or_else(|_| Date::from_pg_epoch(0).unwrap());
+            // Drawing from the full `i32` day range was rejected ~68% of the
+            // time and never reached the boundary days.
+            let days = u.int_in_range(Date::LOW_DAYS..=Date::HIGH_DAYS)?;
+            let d = Date::from_pg_epoch(days).expect("days drawn within Date's bounds");
             packer.push(Datum::Date(d));
         }
         SqlScalarType::Time => {
@@ -275,14 +340,57 @@ fn push_scalar(
             packer.push(Datum::MzTimestamp(Timestamp::from(u64::arbitrary(u)?)))
         }
         SqlScalarType::Uuid => packer.push(Datum::Uuid(Uuid::from_u128(u.arbitrary::()?))),
-        // The empty range is always valid and its `to_string` is exercised.
-        SqlScalarType::Range { .. } => packer.push(Datum::Range(Range { inner: None })),
+        // `Display for Range` short-circuits the empty range to the constant
+        // "empty", so only a finite range reaches `RangeInner`/`RangeBound`'s
+        // Display, where the actual rendering logic lives.
+        SqlScalarType::Range { .. } => {
+            if u.ratio(1u8, 8u8)? {
+                packer.push(Datum::Range(Range { inner: None }));
+            } else {
+                // `RangeBound::new` maps `Datum::Null` to an infinite bound.
+                let (a, b) = (i32::arbitrary(u)?, i32::arbitrary(u)?);
+                let lower = RangeLowerBound::new(
+                    if u.ratio(1u8, 8u8)? {
+                        Datum::Null
+                    } else {
+                        Datum::Int32(a.min(b))
+                    },
+                    bool::arbitrary(u)?,
+                );
+                let upper = RangeUpperBound::new(
+                    if u.ratio(1u8, 8u8)? {
+                        Datum::Null
+                    } else {
+                        Datum::Int32(a.max(b))
+                    },
+                    bool::arbitrary(u)?,
+                );
+                // `push_range` canonicalizes before it touches the packer, so a
+                // rejected range (an exclusive bound whose step oversteps
+                // `i32`) leaves the packer untouched and we can substitute the
+                // empty range.
+                if packer.push_range(Range::new(Some((lower, upper)))).is_err() {
+                    packer.push(Datum::Range(Range { inner: None }));
+                }
+            }
+        }
+        // `AclMode::empty()` renders no privilege bits at all, so fuzz the mode.
+        // `from_bits_truncate` drops undefined bits, which is what the row
+        // encoding round-trips.
         SqlScalarType::MzAclItem => {
-            let item = MzAclItem::empty(RoleId::User(u.arbitrary::()?), RoleId::Public);
+            let item = MzAclItem {
+                grantee: RoleId::User(u.arbitrary::()?),
+                grantor: RoleId::Public,
+                acl_mode: AclMode::from_bits_truncate(u64::arbitrary(u)?),
+            };
             packer.push(Datum::MzAclItem(item));
         }
         SqlScalarType::AclItem => {
-            let item = AclItem::empty(Oid(u.arbitrary::()?), Oid(u.arbitrary::()?));
+            let item = AclItem {
+                grantee: Oid(u.arbitrary::()?),
+                grantor: Oid(u.arbitrary::()?),
+                acl_mode: AclMode::from_bits_truncate(u64::arbitrary(u)?),
+            };
             packer.push(Datum::AclItem(item));
         }
         // `gen_scalar_type` never produces other types.
@@ -306,7 +414,10 @@ fn push_typed(
                 packer.push(Datum::Null);
             } else {
                 let text = gen_json_text(u)?;
-                // The text is always valid JSON by construction.
+                // The text is always valid JSON by construction, and every
+                // number it contains is one `strconv::parse_numeric` accepts.
+                // See `gen_json_text`'s TODO before generating a payload that
+                // `pack_str` can reject.
                 JsonbPacker::new(packer)
                     .pack_str(&text)
                     .expect("generated valid json");
@@ -381,7 +492,9 @@ fn push_typed(
             // The dims and element count match by construction, so this can't
             // return an error. If it somehow does, fall back to an empty array.
             if packer.try_push_array(&array_dims, elems).is_err() {
-                packer.try_push_array(&[], std::iter::empty::()).unwrap();
+                packer
+                    .try_push_array(&[], std::iter::empty::())
+                    .unwrap();
             }
         }
     }
@@ -420,7 +533,20 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
     // Encode the row's datums as JSON (the sink path) and serialize the result.
     // Neither step may panic for any well-typed row.
     let value = encode_datums_as_json(row.iter(), &columns);
-    let _ = serde_json::to_vec(&value);
+    // Serialize the way the sink does: `JsonEncoder::encode_unchecked` calls
+    // `Value::to_string`, and `ToString` panics when the `Display` impl errors,
+    // which `Display for Value` does for any serialization failure. So a
+    // serialization error is a *panic* in production, where `to_vec` would only
+    // have returned an `Err` that an oracle could not see.
+    let _ = value.to_string();
+
+    // `encode_datums_as_json` hardcodes `KeepAsNumber`. The HTTP `/api/sql` and
+    // WebSocket SQL result encoders pass `ConvertNumberToString`, which recurses
+    // through every composite arm, so drive that policy too.
+    for (datum, (_, typ)) in row.iter().zip(&columns) {
+        let value = TypedDatum::new(datum, typ).json(&JsonNumberPolicy::ConvertNumberToString);
+        let _ = value.to_string();
+    }
     Ok(())
 }
 
diff --git a/src/interchange/fuzz/fuzz_targets/protobuf_decode_fuzzed_schema.rs b/src/interchange/fuzz/fuzz_targets/protobuf_decode_fuzzed_schema.rs
index c2cf3c6a0e91b..61668ebaee589 100644
--- a/src/interchange/fuzz/fuzz_targets/protobuf_decode_fuzzed_schema.rs
+++ b/src/interchange/fuzz/fuzz_targets/protobuf_decode_fuzzed_schema.rs
@@ -23,13 +23,15 @@
 //! repeated -> list, nested message -> record). Instead we keep the schema
 //! structured (`MsgDef`/`FieldTy`), build the `FileDescriptorSet` from it, and
 //! protobuf-binary-encode a valid body for the top message against that same
-//! structure, bounding recursion through cyclic message refs by emitting empty
-//! nested messages at the depth limit. Both wire formats are exercised: the
-//! Confluent variant gets the 5-byte schema-registry header prepended so its
-//! body decodes just as deeply. We keep the error-path coverage a random body
-//! gave, though: a quarter of the inputs feed the raw remaining bytes, and
-//! others truncate or single-byte-corrupt the valid encoding. Neither
-//! validation nor decoding may panic.
+//! structure, bounding recursion through cyclic message refs by omitting
+//! message fields entirely once the depth limit is reached. The wire format is
+//! fuzzer-chosen per input: the Confluent variant gets the six-byte
+//! schema-registry header prepended (magic byte, schema id, message index) so
+//! its body decodes just as deeply, and one input in eight instead lets the
+//! fuzzer pick those bytes so the header's rejection arms stay covered. We keep
+//! the error-path coverage a random body gave, though: a quarter of the inputs
+//! feed the raw remaining bytes, and others truncate or single-byte-corrupt the
+//! valid encoding. Neither validation nor decoding may panic.
 //!
 //! Beyond the well-formed-schema happy path, we drive the descriptor-validation
 //! surface harder. Some schemas grow a `map` field (a repeated
@@ -157,7 +159,8 @@ fn gen_schema(u: &mut Unstructured) -> arbitrary::Result<(Vec, Vec)>
     Ok((msgs, enum_nvals))
 }
 
-fn field_proto(f: &FieldDef) -> FieldDescriptorProto {
+/// Build the descriptor entry for the `fi`th field of a message.
+fn field_proto(fi: usize, f: &FieldDef) -> FieldDescriptorProto {
     let (ty, type_name) = match &f.ty {
         FieldTy::Int32 => (Type::Int32, None),
         FieldTy::Int64 => (Type::Int64, None),
@@ -186,7 +189,14 @@ fn field_proto(f: &FieldDef) -> FieldDescriptorProto {
         Label::Optional
     };
     FieldDescriptorProto {
-        name: Some(format!("f{}", f.number - 1)),
+        // NOTE: the name must come from the position, not from `f.number`. A
+        // name derived from the number makes every duplicate number a duplicate
+        // *name* too, and `DescriptorPool::decode` rejects the name clash first
+        // ("camel-case name of field 'f0' conflicts with field 'f0'"). The
+        // duplicate-number descriptor this target means to generate would then
+        // never exist, and the reject rate would swallow a large share of all
+        // inputs before any Materialize code runs.
+        name: Some(format!("f{fi}")),
         number: Some(f.number),
         label: Some(label as i32),
         r#type: Some(ty as i32),
@@ -219,7 +229,12 @@ fn build_fds(msgs: &[MsgDef], enum_nvals: &[u8]) -> Vec {
         .enumerate()
         .map(|(mi, m)| DescriptorProto {
             name: Some(format!("M{mi}")),
-            field: m.fields.iter().map(field_proto).collect(),
+            field: m
+                .fields
+                .iter()
+                .enumerate()
+                .map(|(fi, f)| field_proto(fi, f))
+                .collect(),
             ..Default::default()
         })
         .collect();
@@ -286,6 +301,24 @@ fn encode_len_delimited(number: i32, bytes: &[u8], out: &mut Vec) {
     out.extend_from_slice(bytes);
 }
 
+/// Pick a value for a field of enum `E{j}`, whose declared values are
+/// `0..nvals`.
+///
+/// One draw in eight is `nvals` itself, which is out of range. prost decodes it
+/// fine (proto3 enums are open, the number survives as `Value::EnumNumber`),
+/// but `pack_value` then fails the row with "unknown enum value" and
+/// `pack_message` propagates that, so every field *after* the enum goes
+/// unpacked. Keeping it a minority leaves most bodies packing in full while
+/// still covering the rejection.
+fn enum_value(enum_nvals: &[u8], j: u8, u: &mut Unstructured) -> arbitrary::Result {
+    let nvals = u64::from(enum_nvals[usize::from(j)]);
+    if u.int_in_range(0u8..=7)? == 0 {
+        Ok(nvals)
+    } else {
+        Ok(u.int_in_range(0..=nvals - 1)?)
+    }
+}
+
 /// Encode one occurrence of a field (tag + value).
 fn encode_field(
     msgs: &[MsgDef],
@@ -330,10 +363,7 @@ fn encode_field(
         }
         FieldTy::Enum(j) => {
             encode_tag(n, 0, out);
-            // Valid values are 0..nvals. `nvals` itself is one out-of-range
-            // index (proto3 open enums accept it, the decoder maps it).
-            let nvals = u64::from(enum_nvals[usize::from(*j)]);
-            encode_varint(u.int_in_range(0u64..=nvals)?, out);
+            encode_varint(enum_value(enum_nvals, *j, u)?, out);
         }
         // wire type 1: 64-bit.
         FieldTy::Fixed64 => {
@@ -379,12 +409,12 @@ fn encode_field(
             encode_len_delimited(n, &b, out);
         }
         FieldTy::Message(r) => {
-            // Bound recursion through cyclic refs: at the depth limit emit an
-            // empty (all-defaults) nested message.
+            // Recursion through cyclic message refs terminates because
+            // `encode_message` gives a message field zero occurrences once
+            // `depth` reaches 0, so this arm only ever runs with `depth > 0`
+            // and `depth - 1` cannot underflow.
             let mut nested = Vec::new();
-            if depth > 0 {
-                encode_message(msgs, enum_nvals, usize::from(*r), depth - 1, u, &mut nested)?;
-            }
+            encode_message(msgs, enum_nvals, usize::from(*r), depth - 1, u, &mut nested)?;
             encode_len_delimited(n, &nested, out);
         }
         FieldTy::Map => {
@@ -453,10 +483,7 @@ fn encode_scalar_value(
             encode_varint(((v << 1) ^ (v >> 63)) as u64, out);
         }
         FieldTy::Bool => encode_varint(u.int_in_range(0u64..=1)?, out),
-        FieldTy::Enum(j) => {
-            let nvals = u64::from(enum_nvals[usize::from(*j)]);
-            encode_varint(u.int_in_range(0u64..=nvals)?, out);
-        }
+        FieldTy::Enum(j) => encode_varint(enum_value(enum_nvals, *j, u)?, out),
         FieldTy::Fixed64 => out.extend_from_slice(&u.arbitrary::()?.to_le_bytes()),
         FieldTy::Sfixed64 => out.extend_from_slice(&u.arbitrary::()?.to_le_bytes()),
         FieldTy::Double => out.extend_from_slice(&u.arbitrary::()?.to_le_bytes()),
@@ -484,7 +511,8 @@ fn encode_message(
     for f in &msg.fields {
         // Repeated fields get 0..=3 occurrences, singular fields 0..=1
         // (proto3 fields are optional). Message fields at the depth limit are
-        // omitted entirely to break cycles.
+        // omitted entirely to break cycles. `encode_field` relies on that: it
+        // decrements `depth` unguarded for a message field.
         let max = if matches!(f.ty, FieldTy::Message(_)) && depth == 0 {
             0
         } else if f.repeated {
@@ -522,6 +550,35 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
     let root = usize::from(u.int_in_range(0u8..=u8::try_from(msgs.len() - 1).unwrap())?);
     let root_name = format!("fuzz.M{root}");
 
+    // The wire format is chosen per input rather than both being run over the
+    // same body. Once `extract_protobuf_header` has stripped the prefix the two
+    // paths are byte-identical, so running both would just rebuild the whole
+    // descriptor pool (the most expensive step here) to repeat the same decode.
+    let confluent_wire_format = bool::arbitrary(&mut u)?;
+
+    // The Confluent protobuf prefix is magic byte 0, a 4-byte big-endian schema
+    // id, then a message-index array. `extract_protobuf_header` accepts only the
+    // single-byte `0` form (the file's first message), so a well-formed prefix
+    // is SIX zero bytes, not five. At five the body's own first byte stands in
+    // for the message index, and for a structured body that byte is always a
+    // field tag `(number << 3) | wire_type` with `number >= 1`, hence always
+    // `>= 8`, hence always rejected. The body would never once be decoded.
+    //
+    // One input in eight lets the fuzzer pick the prefix instead, so the header's
+    // rejection arms (bad magic, nonzero message index, buffer too short to hold
+    // magic + schema id, or to hold a message index after them) stay covered
+    // rather than depending on the accident of a body starting with a zero.
+    let confluent_prefix = if u.int_in_range(0u8..=7)? == 0 {
+        let len = u.int_in_range(0usize..=6)?;
+        let mut p = Vec::with_capacity(len);
+        for _ in 0..len {
+            p.push(u.arbitrary::()?);
+        }
+        p
+    } else {
+        vec![0u8; 6]
+    };
+
     // Body for the root message: usually a valid encoding (so prost decodes
     // through to the Row conversion), but a quarter of the time the raw remaining
     // bytes, and otherwise a valid encoding occasionally truncated or single-byte
@@ -546,25 +603,20 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
         b
     };
 
-    for confluent_wire_format in [false, true] {
-        let Ok(descriptors) = DecodedDescriptors::from_bytes(&fds, root_name.clone()) else {
-            return Ok(());
-        };
-        let Ok(mut decoder) = Decoder::new(descriptors, confluent_wire_format) else {
-            return Ok(());
-        };
-        // The Confluent variant strips a 5-byte schema-registry header (magic
-        // byte 0 + 4-byte schema id) before the body. Prepend one so its body
-        // decodes just as deeply as the raw variant.
-        let payload = if confluent_wire_format {
-            let mut p = vec![0u8; 5];
-            p.extend_from_slice(&body);
-            p
-        } else {
-            body.clone()
-        };
-        let _ = decoder.decode(&payload);
-    }
+    let Ok(descriptors) = DecodedDescriptors::from_bytes(&fds, root_name) else {
+        return Ok(());
+    };
+    let Ok(mut decoder) = Decoder::new(descriptors, confluent_wire_format) else {
+        return Ok(());
+    };
+    let payload = if confluent_wire_format {
+        let mut p = confluent_prefix;
+        p.extend_from_slice(&body);
+        p
+    } else {
+        body
+    };
+    let _ = decoder.decode(&payload);
     Ok(())
 }
 
diff --git a/src/mysql-util/fuzz/fuzz_targets/mysql_table_desc_proto_roundtrip.rs b/src/mysql-util/fuzz/fuzz_targets/mysql_table_desc_proto_roundtrip.rs
index c11cae3e28352..ee3f705201200 100644
--- a/src/mysql-util/fuzz/fuzz_targets/mysql_table_desc_proto_roundtrip.rs
+++ b/src/mysql-util/fuzz/fuzz_targets/mysql_table_desc_proto_roundtrip.rs
@@ -14,7 +14,7 @@
 //! Input generation is split across three arms keyed off the first input
 //! byte so a single byte stream exercises all of them over time:
 //!
-//! 1. **Valid-value arm.** A 32-byte seed (drawn from the input) drives
+//! 1. **Valid-value arm.** A 32-byte seed (zero-padded from the input) drives
 //!    proptest's `Arbitrary for MySqlTableDesc` to build a *structurally
 //!    valid, deeply-populated* descriptor. Non-empty columns with real
 //!    `SqlColumnType`s, every `MySqlColumnMeta` variant, and a populated
@@ -24,11 +24,12 @@
 //!    decodes to near-empty messages).
 //!
 //! 2. **Duplicate/unsorted-keys arm.** Crafts a `ProtoMySqlTableDesc`
-//!    whose `keys` field (a repeated proto `Vec`) contains duplicates and
-//!    out-of-order entries to probe the classic `Vec -> BTreeSet -> Vec`
-//!    round-trip trap: the *first* decode normalizes (dedups + sorts), so
-//!    we assert the conversion is a *fixed point* afterwards rather than
-//!    byte-identical to the crafted input.
+//!    whose `keys` field (a repeated proto `Vec`) contains duplicates in
+//!    non-sorted order, then asserts the two properties the
+//!    `Vec -> BTreeSet` decode owes its callers: the decoded descriptor does
+//!    not depend on the wire order, and the duplicates collapse. The
+//!    conversion itself must succeed, because the crafted proto is valid by
+//!    construction.
 //!
 //! 3. **Raw-bytes arm.** Decode arbitrary bytes and, if they happen to form a
 //!    valid descriptor, check the proto round-trip is stable. This guards
@@ -36,13 +37,32 @@
 
 #![no_main]
 
+use std::sync::OnceLock;
+
 use libfuzzer_sys::fuzz_target;
-use mz_mysql_util::{MySqlKeyDesc, MySqlTableDesc, ProtoMySqlKeyDesc, ProtoMySqlTableDesc};
+use mz_mysql_util::{MySqlTableDesc, ProtoMySqlKeyDesc, ProtoMySqlTableDesc};
 use mz_proto::{ProtoType, RustType};
-use proptest::strategy::{Strategy, ValueTree};
+use proptest::strategy::{BoxedStrategy, Strategy, ValueTree};
 use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
 use prost::Message;
 
+// `Arbitrary::arbitrary()` rebuilds the entire boxed strategy graph on every
+// call: `SqlScalarType`'s ~31-variant `Union` plus a second copy of it for
+// `Array`, the `prop_recursive` wrapper, and a `.*` regex compile per
+// `any::()` leaf. `Config::default()` re-reads the process environment.
+// Both are per-process constants, so pay for them once instead of once per
+// execution. libFuzzer runs a single execution at a time per process, so a
+// `thread_local` suffices for the non-`Sync` strategy.
+thread_local! {
+    static DESC_STRATEGY: BoxedStrategy =
+        ::arbitrary().boxed();
+}
+
+fn config() -> Config {
+    static CONFIG: OnceLock = OnceLock::new();
+    CONFIG.get_or_init(Config::default).clone()
+}
+
 /// Assert that a `MySqlTableDesc` survives a full Rust round-trip through
 /// its proto representation unchanged, including a re-encode/decode of the
 /// wire bytes.
@@ -54,13 +74,17 @@ fn assert_rust_roundtrip(orig: &MySqlTableDesc) {
     let round: MySqlTableDesc = proto2
         .into_rust()
         .expect("re-encoded MySqlTableDesc must convert back to Rust");
-    assert_eq!(orig, &round, "MySqlTableDesc changed across proto roundtrip");
+    assert_eq!(
+        orig, &round,
+        "MySqlTableDesc changed across proto roundtrip"
+    );
 }
 
 /// Decode `bytes` as a proto, and if it is a valid descriptor, assert the
-/// proto round-trip is stable. Used by both the crafted and raw-bytes arms,
-/// where the *first* decode may normalize a `Vec`-shaped field into a
-/// `BTreeSet`, so we only require idempotence from that normalized value on.
+/// proto round-trip is stable. Used by the raw-bytes arm, where a decode or
+/// conversion failure is a legitimate outcome, and where the *first* decode may
+/// normalize a `Vec`-shaped field into a `BTreeSet`, so we only require
+/// idempotence from that normalized value on.
 fn check_decoded(bytes: &[u8]) {
     let Ok(proto) = ProtoMySqlTableDesc::decode(bytes) else {
         return;
@@ -101,63 +125,72 @@ fn craft_unsorted_dup_keys(data: &[u8]) -> ProtoMySqlTableDesc {
     }
 }
 
+/// Encode a hand-built proto, decode the wire bytes, and convert to Rust. Every
+/// step must succeed: the input is structurally valid by construction, so a
+/// failure is the bug the crafted arm exists to catch, for example a
+/// `from_proto` hardened to reject duplicate or unsorted wire keys.
+fn decode_crafted(proto: &ProtoMySqlTableDesc) -> MySqlTableDesc {
+    ProtoMySqlTableDesc::decode(proto.encode_to_vec().as_slice())
+        .expect("hand-built proto must decode")
+        .into_rust()
+        .expect("duplicate/unsorted keys must convert")
+}
+
 fuzz_target!(|data: &[u8]| {
-    // Reserve the first byte as a mode selector and the next 32 bytes as the
-    // proptest seed. Everything after that feeds the raw-bytes / crafting
-    // logic so a single input can drive any arm.
+    // The first byte selects the arm, everything after it is that arm's input.
+    // The arms overlap in the byte stream on purpose: only one of them runs per
+    // execution, so reading them all from byte 1 keeps every mutation live for
+    // whichever arm the mode byte picks. The proptest seed is zero-padded rather
+    // than all-or-nothing, otherwise every input shorter than 33 bytes would
+    // reuse the all-zero seed and libFuzzer grows inputs up from empty.
     let mode = data.first().copied().unwrap_or(0);
+    let tail = data.get(1..).unwrap_or(&[]);
     let mut seed = [0u8; 32];
-    let seed_src = data.get(1..33).unwrap_or(&[]);
-    seed[..seed_src.len()].copy_from_slice(seed_src);
-    let rest = data.get(33..).unwrap_or(&[]);
+    let n = tail.len().min(32);
+    seed[..n].copy_from_slice(&tail[..n]);
 
     match mode % 3 {
         0 => {
             // Valid-value arm: drive proptest's Arbitrary from the seed.
-            let mut runner = TestRunner::new_with_rng(
-                Config::default(),
-                TestRng::from_seed(RngAlgorithm::ChaCha, &seed),
-            );
-            let value = match ::arbitrary()
-                .new_tree(&mut runner)
-            {
+            let mut runner =
+                TestRunner::new_with_rng(config(), TestRng::from_seed(RngAlgorithm::ChaCha, &seed));
+            let value = match DESC_STRATEGY.with(|s| s.new_tree(&mut runner)) {
                 Ok(tree) => tree.current(),
                 Err(_) => return,
             };
             assert_rust_roundtrip(&value);
-
-            // The proptest-built value is already normalized (its keys come
-            // from a BTreeSet). Sanity-check that the BTreeSet semantics hold
-            // after a wire decode by encoding and re-decoding, then confirm
-            // re-collecting the keys into a fresh set is idempotent.
-            let decoded: MySqlTableDesc = value
-                .into_proto()
-                .encode_to_vec()
-                .as_slice()
-                .pipe(ProtoMySqlTableDesc::decode)
-                .expect("decode")
-                .into_rust()
-                .expect("into_rust");
-            let recollected: std::collections::BTreeSet =
-                decoded.keys.iter().cloned().collect();
-            assert_eq!(decoded.keys, recollected, "key set not idempotent");
         }
         1 => {
             // Duplicate/unsorted-keys arm.
-            let proto = craft_unsorted_dup_keys(rest);
-            check_decoded(proto.encode_to_vec().as_slice());
+            let proto = craft_unsorted_dup_keys(tail);
+            let decoded = decode_crafted(&proto);
+            // The wire field is a repeated Vec and the Rust field a BTreeSet, so
+            // decoding must be insensitive to the wire order and must collapse
+            // the duplicates rather than keep them or reject the message.
+            //
+            // NOTE: rotate, don't reverse. `craft_unsorted_dup_keys` lays the
+            // keys out as a palindrome, so reversing them would re-encode the
+            // very same wire bytes and assert nothing.
+            let mut permuted = proto;
+            permuted.keys.rotate_left(1);
+            assert_eq!(
+                decoded,
+                decode_crafted(&permuted),
+                "wire key order changed the decoded descriptor"
+            );
+            // `craft_unsorted_dup_keys` builds exactly two distinct keys (their
+            // `columns` differ in length, so they can never compare equal),
+            // each repeated twice.
+            assert_eq!(
+                decoded.keys.len(),
+                2,
+                "duplicate wire keys did not collapse"
+            );
+            assert_rust_roundtrip(&decoded);
         }
         _ => {
             // Raw-bytes arm: decode arbitrary bytes directly.
-            check_decoded(rest);
+            check_decoded(tail);
         }
     }
 });
-
-/// Tiny extension trait so the valid-value arm can read top-to-bottom.
-trait Pipe: Sized {
-    fn pipe(self, f: impl FnOnce(Self) -> R) -> R {
-        f(self)
-    }
-}
-impl Pipe for T {}
diff --git a/src/persist-client/fuzz/fuzz_targets/rollup_proto_roundtrip.rs b/src/persist-client/fuzz/fuzz_targets/rollup_proto_roundtrip.rs
index 2eb1b38d52892..45a8741c3bd81 100644
--- a/src/persist-client/fuzz/fuzz_targets/rollup_proto_roundtrip.rs
+++ b/src/persist-client/fuzz/fuzz_targets/rollup_proto_roundtrip.rs
@@ -10,7 +10,11 @@
 //! Fuzz target: exercises `ProtoRollup` decoding and the `Rollup`
 //! `from_proto` conversion. A rollup is a full state snapshot read from blob on
 //! load, so a decoder panic on a corrupted/crafted blob makes the shard
-//! unrecoverable.
+//! unrecoverable. NOTE: an `Err` is not a *good* outcome on that path either.
+//! `UntypedState::decode` `.expect()`s the conversion, so a rejected rollup
+//! still aborts the process. What this target buys is a decoder that fails
+//! predictably and boundedly (no hang, no OOM, no wild slicing) plus a working
+//! error path for the CLI/inspect callers that do surface it.
 //!
 //! Decoding *random* bytes as a protobuf almost never yields a `ProtoRollup`
 //! that survives `into_rust`: the conversion needs a well-formed shard id
@@ -21,15 +25,20 @@
 //! `ProtoRollup` on the protobuf wire from fuzzer-chosen parameters. The first
 //! byte selects a mode:
 //!
-//! * mode 0: feed the remaining bytes straight to `ProtoRollup::decode`
-//!   (the robustness arm: must never panic, and any value that
-//!   converts must survive a proto re-encode round trip losslessly).
+//! * mode 0: feed the remaining bytes straight to `ProtoRollup::decode` (the
+//!   robustness arm: must never panic, and any value that converts must be
+//!   *stable* under a further round trip). Arbitrary bytes can be non-canonical,
+//!   so this arm can only assert stability, not losslessness. Mode 1 covers that.
 //! * mode 1: synthesize a valid rollup *with* inlined diffs whose bounds
-//!   satisfy the invariants, decode it (the happy path that the invariant
-//!   checks must accept), and round-trip it.
+//!   satisfy the invariants, then require that it converts and that the
+//!   conversion is *lossless*: the hand-built message is canonical, so
+//!   re-encoding the converted value must reproduce it field for field.
 //! * mode 2: synthesize a valid rollup, then *perturb a single invariant
 //!   field* (drop the rollups map, or shift `diffs.lower`/`diffs.upper` off
-//!   the expected seqno). `from_proto` must reject it with `Err`, never panic.
+//!   the expected seqno). `from_proto` must reject it with an `Err` that names
+//!   the perturbed field, never panic. Matching the message matters: a bare
+//!   `is_err()` would keep passing if the rollup started being rejected for
+//!   some unrelated reason, hiding that the mutation stopped being tested.
 
 #![no_main]
 
@@ -219,7 +228,11 @@ fn build_rollup(u: &mut Unstructured, mutate: Option) -> Vec {
             } else {
                 None
             };
-            put_bytes(&mut buf, 16, &rollups_entry(*seqno, &hollow_rollup(&key, sz)));
+            put_bytes(
+                &mut buf,
+                16,
+                &rollups_entry(*seqno, &hollow_rollup(&key, sz)),
+            );
         }
     }
 
@@ -257,9 +270,11 @@ fn build_rollup(u: &mut Unstructured, mutate: Option) -> Vec {
             _ => {}
         }
 
-        // Pick in-range diff seqnos only for the un-mutated/drop-rollups cases.
-        // `from_proto` does not bound-check individual diff seqnos, so this is
-        // about producing realistic content rather than satisfying an invariant.
+        // Diff seqnos start at `lower`, which is the shifted one for
+        // `ShiftLower`, so the mutated cases can place them outside the true
+        // range. That is harmless: `from_proto` checks only the `lower`/`upper`
+        // bounds and never the individual diff seqnos, so this is about
+        // producing realistic content rather than satisfying an invariant.
         let mut diffs: Vec<(u64, Vec)> = Vec::new();
         if state_seqno > latest_rollup_seqno {
             let span = upper.saturating_sub(lower).min(4);
@@ -275,16 +290,29 @@ fn build_rollup(u: &mut Unstructured, mutate: Option) -> Vec {
     buf
 }
 
-fn roundtrip(proto: ProtoRollup) {
+/// Round trip whatever converts, tolerating inputs that don't.
+///
+/// Only for mode 0, where arbitrary bytes legitimately fail to convert. A mode
+/// that builds a rollup it *knows* is valid must require the conversion instead,
+/// or a newly-rejected shape silently turns the arm into a no-op.
+fn roundtrip_lenient(proto: ProtoRollup) {
     let orig: Rollup = match proto.into_rust() {
         Ok(v) => v,
         Err(_) => return,
     };
+    roundtrip_from(orig.into_proto());
+}
 
-    let proto2: ProtoRollup = orig.into_proto();
+/// Asserts a canonical proto (already an `into_proto` output) is stable under a
+/// further encode / decode / convert / re-encode.
+///
+/// Both sides of the comparison are post-`into_proto`, so this only proves
+/// `into_proto ∘ from_proto` is idempotent. Losslessness of the *first* pass is
+/// mode 1's job, since only a hand-built canonical input can assert it.
+fn roundtrip_from(proto2: ProtoRollup) {
     let bytes2 = proto2.encode_to_vec();
-    let proto3 = ProtoRollup::decode(bytes2.as_slice())
-        .expect("re-encode of valid Rollup must decode");
+    let proto3 =
+        ProtoRollup::decode(bytes2.as_slice()).expect("re-encode of valid Rollup must decode");
     let round: Rollup = proto3
         .into_rust()
         .expect("re-encoded Rollup must convert back to Rust");
@@ -306,33 +334,63 @@ fuzz_target!(|data: &[u8]| {
             let Ok(proto) = ProtoRollup::decode(rest) else {
                 return;
             };
-            roundtrip(proto);
+            roundtrip_lenient(proto);
         }
         1 => {
             // Valid-rollup arm: the synthesized message satisfies the diff
-            // invariants and must decode + round-trip.
+            // invariants, so it must convert, and convert losslessly.
             let mut u = Unstructured::new(rest);
             let bytes = build_rollup(&mut u, None);
             let proto =
                 ProtoRollup::decode(bytes.as_slice()).expect("hand-built ProtoRollup must decode");
-            roundtrip(proto);
+            // Every shape `build_rollup` emits is valid, so rejection here is a
+            // find, not a reason to skip the arm.
+            let orig: Rollup = proto
+                .clone()
+                .into_rust()
+                .expect("synthesized rollup must convert");
+            let proto2: ProtoRollup = orig.into_proto();
+            // The strong oracle: the input is already canonical, so a field the
+            // conversion drops or defaults shows up right here. Comparing two
+            // `into_proto` outputs (as `roundtrip_from` does) cannot see that,
+            // and `Rollup::into_proto` reaches through `self.state.state...`
+            // rather than destructuring, so a field added to `State` and wired
+            // into `from_proto` but forgotten in `into_proto` compiles fine and
+            // is then silently lost from every rollup written.
+            //
+            // The one legitimate normalization: an absent `applier_version`
+            // decodes as "infinitely old" and re-encodes as 0.0.0.
+            let mut expected = proto;
+            if expected.applier_version.is_empty() {
+                expected.applier_version = "0.0.0".into();
+            }
+            assert_eq!(
+                expected, proto2,
+                "field lost across ProtoRollup -> Rollup -> ProtoRollup"
+            );
+            roundtrip_from(proto2);
         }
         _ => {
             // Invariant-violation arm: break exactly one invariant and require
-            // `from_proto` to reject (not panic).
+            // `from_proto` to reject it, naming that invariant (not panic, and
+            // not fail for some unrelated reason).
             let mut u = Unstructured::new(rest);
-            let mutation = match u.u8() % 3 {
-                0 => Mutation::DropRollups,
-                1 => Mutation::ShiftLower,
-                _ => Mutation::ShiftUpper,
+            let (mutation, expected_err) = match u.u8() % 3 {
+                0 => (Mutation::DropRollups, "no rollups"),
+                1 => (Mutation::ShiftLower, "diffs lower"),
+                _ => (Mutation::ShiftUpper, "diffs upper"),
             };
             let bytes = build_rollup(&mut u, Some(mutation));
-            let proto = ProtoRollup::decode(bytes.as_slice())
-                .expect("hand-built ProtoRollup must decode");
+            let proto =
+                ProtoRollup::decode(bytes.as_slice()).expect("hand-built ProtoRollup must decode");
             let result: Result, _> = proto.into_rust();
+            let err = result.err().expect(
+                "Rollup with a broken diff-bounds invariant must be rejected by from_proto",
+            );
+            let msg = err.to_string();
             assert!(
-                result.is_err(),
-                "Rollup with a broken diff-bounds invariant must be rejected by from_proto"
+                msg.contains(expected_err),
+                "expected a rejection mentioning {expected_err:?}, got: {msg}"
             );
         }
     }
diff --git a/src/persist-client/fuzz/fuzz_targets/state_diff_proto_roundtrip.rs b/src/persist-client/fuzz/fuzz_targets/state_diff_proto_roundtrip.rs
index c3ca90b360095..d616a98bc5dec 100644
--- a/src/persist-client/fuzz/fuzz_targets/state_diff_proto_roundtrip.rs
+++ b/src/persist-client/fuzz/fuzz_targets/state_diff_proto_roundtrip.rs
@@ -23,15 +23,22 @@
 //! This target therefore hand-builds the columnar encoding on the protobuf wire
 //! from fuzzer-chosen parameters. The first byte selects a mode:
 //!
-//! * mode 0: feed the remaining bytes straight to `ProtoStateDiff::decode`
-//!   (the robustness arm: must never panic, and any value that
-//!   converts must survive a proto re-encode round trip losslessly).
+//! * mode 0: feed the remaining bytes straight to `ProtoStateDiff::decode` (the
+//!   robustness arm: must never panic, and any value that converts must be
+//!   *stable* under a further round trip). Arbitrary bytes can be non-canonical
+//!   (`Antichain` minimization, the `DeprecatedRollups` remap), so this arm can
+//!   only assert stability, not losslessness.
 //! * mode 1: synthesize a *valid* columnar diff over a mix of fields and
 //!   insert/update/delete diff types, with self-consistent slice counts and
-//!   lengths, then decode + round-trip it.
+//!   lengths. The conversion must accept it, and must keep every diff: a
+//!   dropped or defaulted diff is invisible to the stability oracle.
 //! * mode 2: synthesize a diff whose columnar bookkeeping is *inconsistent*
 //!   (slice count or byte length mismatch, or an unknown field/diff-type enum).
 //!   `from_proto`'s `validate()`/iter must reject it with `Err`, never panic.
+//!   A declared byte length is the one class that can get *past* `validate()`:
+//!   lengths that sum past `u64::MAX` wrap (overflow checks are off in
+//!   release/optimized builds) and can match the real `data_bytes` length, so
+//!   the corruptions cover that edge explicitly.
 
 #![no_main]
 
@@ -193,6 +200,10 @@ struct ColumnarDiff {
     diff_type: u64,
     /// key slice followed by 1 (insert/delete) or 2 (update) value slices.
     slices: Vec>,
+    /// Replaces the `data_lens` entry written for the *last* slice, while the
+    /// real slice bytes still go into `data_bytes`. The only way to express a
+    /// declared length that disagrees with the bytes actually present.
+    len_override: Option,
 }
 
 /// Picks a field + diff type + correctly-shaped key/value slices.
@@ -212,12 +223,12 @@ fn gen_diff(u: &mut Unstructured) -> ColumnarDiff {
 
     // (field, key slice, value-slice generator).
     let (field, key): (u64, Vec) = match u.u8() % 6 {
-        0 => (FIELD_HOSTNAME, Vec::new()),    // key ()
-        1 => (FIELD_LAST_GC_REQ, Vec::new()), // key ()
-        2 => (FIELD_SINCE, Vec::new()),       // key ()
+        0 => (FIELD_HOSTNAME, Vec::new()),               // key ()
+        1 => (FIELD_LAST_GC_REQ, Vec::new()),            // key ()
+        2 => (FIELD_SINCE, Vec::new()),                  // key ()
         3 => (FIELD_ROLLUPS, enc_u64(u.u64() % 10_000)), // key u64 (SeqNo)
-        4 => (FIELD_ACTIVE_ROLLUP, Vec::new()), // key ()
-        _ => (FIELD_ACTIVE_GC, Vec::new()),   // key ()
+        4 => (FIELD_ACTIVE_ROLLUP, Vec::new()),          // key ()
+        _ => (FIELD_ACTIVE_GC, Vec::new()),              // key ()
     };
 
     let mut slices = Vec::with_capacity(1 + num_vals);
@@ -251,6 +262,7 @@ fn gen_diff(u: &mut Unstructured) -> ColumnarDiff {
         field,
         diff_type,
         slices,
+        len_override: None,
     }
 }
 
@@ -265,8 +277,13 @@ fn encode_field_diffs(diffs: &[ColumnarDiff]) -> Vec {
     for d in diffs {
         put_uint(&mut fields, 1, d.field);
         put_uint(&mut diff_types, 2, d.diff_type);
-        for slice in &d.slices {
-            put_uint(&mut data_lens, 3, slice.len() as u64);
+        for (i, slice) in d.slices.iter().enumerate() {
+            let is_last = i + 1 == d.slices.len();
+            let len = match d.len_override {
+                Some(len) if is_last => len,
+                _ => slice.len() as u64,
+            };
+            put_uint(&mut data_lens, 3, len);
             data_bytes.extend_from_slice(slice);
         }
     }
@@ -300,13 +317,27 @@ fn encode_state_diff(u: &mut Unstructured, field_diffs: &[u8]) -> Vec {
     buf
 }
 
-fn roundtrip(proto: ProtoStateDiff) {
+/// Round trip whatever converts, tolerating inputs that don't.
+///
+/// Only for mode 0, where arbitrary bytes legitimately fail to convert. Modes
+/// that build a diff they *know* is valid must require the conversion instead,
+/// or a newly-rejected shape silently turns the arm into a no-op.
+fn roundtrip_lenient(proto: ProtoStateDiff) {
     let orig: StateDiff = match proto.into_rust() {
         Ok(v) => v,
         Err(_) => return,
     };
+    roundtrip_from(orig.into_proto());
+}
 
-    let proto2: ProtoStateDiff = orig.into_proto();
+/// Asserts a canonical proto (already an `into_proto` output) is stable under a
+/// further encode / decode / convert / re-encode.
+///
+/// Both sides of the comparison are post-`into_proto`, so input-side
+/// normalization can't make it false-fire. Comparing the protos rather than the
+/// `StateDiff`s is forced: `StateDiff` only derives `PartialEq` under
+/// `cfg(any(test, debug_assertions))`.
+fn roundtrip_from(proto2: ProtoStateDiff) {
     let bytes2 = proto2.encode_to_vec();
     let proto3 = ProtoStateDiff::decode(bytes2.as_slice())
         .expect("re-encode of valid StateDiff must decode");
@@ -331,7 +362,7 @@ fuzz_target!(|data: &[u8]| {
             let Ok(proto) = ProtoStateDiff::decode(rest) else {
                 return;
             };
-            roundtrip(proto);
+            roundtrip_lenient(proto);
         }
         1 => {
             // Valid columnar arm: a self-consistent set of field diffs that must
@@ -343,17 +374,31 @@ fuzz_target!(|data: &[u8]| {
             let bytes = encode_state_diff(&mut u, &field_diffs);
             let proto = ProtoStateDiff::decode(bytes.as_slice())
                 .expect("hand-built ProtoStateDiff must decode");
-            roundtrip(proto);
+            // Every shape `gen_diff` emits is valid, so rejection here is a
+            // find, not a reason to skip the arm.
+            let orig: StateDiff = proto
+                .into_rust()
+                .expect("valid columnar field_diffs must convert");
+            let proto2: ProtoStateDiff = orig.into_proto();
+            // The oracle below compares two `into_proto` outputs, so it cannot
+            // see content dropped between the input and `proto2`. `from_proto`
+            // pushes exactly one diff per input diff and `into_proto` emits
+            // exactly one entry per stored diff, so the counts must match.
+            assert_eq!(
+                proto2.field_diffs.as_ref().map_or(0, |f| f.fields.len()),
+                num_diffs,
+                "from_proto dropped columnar diffs"
+            );
+            roundtrip_from(proto2);
         }
         _ => {
             // Inconsistent-columnar arm: build a valid set, then corrupt the
             // bookkeeping so `validate()`/iter must reject it (not panic).
             let mut u = Unstructured::new(rest);
             let num_diffs = u.range(1, 6);
-            let mut diffs: Vec =
-                (0..num_diffs).map(|_| gen_diff(&mut u)).collect();
+            let mut diffs: Vec = (0..num_diffs).map(|_| gen_diff(&mut u)).collect();
 
-            match u.u8() % 4 {
+            match u.u8() % 5 {
                 0 => {
                     // Drop a value slice: data_lens count no longer matches the
                     // count implied by diff_types.
@@ -373,12 +418,30 @@ fuzz_target!(|data: &[u8]| {
                         last.field = 9999;
                     }
                 }
-                _ => {
+                3 => {
                     // Unknown diff-type enum value.
                     if let Some(last) = diffs.last_mut() {
                         last.diff_type = 9999;
                     }
                 }
+                _ => {
+                    // A declared length that disagrees with the bytes actually
+                    // written: `validate()`'s data_bytes sum check, including the
+                    // `u64::MAX` edge where the sum of the lengths wraps and can
+                    // match the real data_bytes length.
+                    //
+                    // Always strictly greater than the true length, so the
+                    // corruption is guaranteed to be one (a length that happened
+                    // to match would make the `is_err` assertion below false-fire).
+                    if let Some(last) = diffs.last_mut() {
+                        let true_len = last.slices.last().map_or(0, |s| s.len() as u64);
+                        last.len_override = Some(match u.u8() % 3 {
+                            0 => u64::MAX,
+                            1 => u64::MAX - 1,
+                            _ => true_len + u64::from(u.u8()) + 1,
+                        });
+                    }
+                }
             }
 
             let field_diffs = encode_field_diffs(&diffs);
diff --git a/src/pgcopy/fuzz/fuzz_targets/copy_decode.rs b/src/pgcopy/fuzz/fuzz_targets/copy_decode.rs
index fe9e1bf426d98..73458d49c44f4 100644
--- a/src/pgcopy/fuzz/fuzz_targets/copy_decode.rs
+++ b/src/pgcopy/fuzz/fuzz_targets/copy_decode.rs
@@ -32,7 +32,11 @@
 //!     token) is properly quoted and escaped so it survives the csv-core framing
 //!     layer and reaches the value decoder instead of erroring early. When a
 //!     header is configured we additionally emit a leading header row so the
-//!     header-skip path is exercised alongside data rows.
+//!     header-skip path is exercised alongside data rows. NOTE: a delimiter or
+//!     quote of `\r`/`\n` still frames badly, because the record terminator is
+//!     then itself a framing byte and no amount of quoting disambiguates it.
+//!     Those params are still worth feeding to the decoder, so they are
+//!     generated anyway, they just rarely reach a value decoder.
 //!
 //! A quarter of inputs feed the raw bytes through both formats so the
 //! framing/error paths stay covered.
@@ -83,12 +87,23 @@ fn push_value(
         }
         2 => out.push_str(u.choose(&["t", "f", "true", "false"])?),
         3 => out.push_str(u.choose(&[
-            "0", "-1.5", "3.14", "1e10", "-2.5e-3", "Infinity", "-Infinity", "NaN",
+            "0",
+            "-1.5",
+            "3.14",
+            "1e10",
+            "-2.5e-3",
+            "Infinity",
+            "-Infinity",
+            "NaN",
         ])?),
         4 => {
             // bytea hex: `\x`. In COPY text the backslash must be doubled.
+            // Digits come in pairs: `parse_bytes_hex` rejects an odd count, and
+            // a rejected column aborts the whole decode, so the columns after
+            // this one would never run.
             out.push_str(if text_format { "\\\\x" } else { "\\x" });
-            for _ in 0..u.int_in_range(0usize..=6)? {
+            for _ in 0..u.int_in_range(0usize..=3)? {
+                out.push(*u.choose(HEX)?);
                 out.push(*u.choose(HEX)?);
             }
         }
@@ -124,12 +139,10 @@ fn push_text_escapes(u: &mut Unstructured, out: &mut String) -> arbitrary::Resul
     for _ in 0..n {
         out.push_str(u.choose(&[
             // Literal chars (decode to themselves).
-            "a", "b", "Z", "0",
-            // Recognized C-style escapes (decode to control bytes).
+            "a", "b", "Z", "0", // Recognized C-style escapes (decode to control bytes).
             "\\b", "\\f", "\\n", "\\r", "\\t", "\\v",
             // Hex escapes in the printable ASCII range.
-            "\\x41", "\\x7e", "\\x2c",
-            // Octal escapes in the printable ASCII range.
+            "\\x41", "\\x7e", "\\x2c", // Octal escapes in the printable ASCII range.
             "\\101", "\\052", "\\176",
             // A backslash before a non-escape char drops the backslash.
             "\\q", "\\\\",
@@ -144,26 +157,29 @@ fn push_text_escapes(u: &mut Unstructured, out: &mut String) -> arbitrary::Resul
 /// NULL marker), or when it equals the active NULL token (so a data value that
 /// happens to match the NULL token is preserved as data rather than read as
 /// SQL NULL).
-fn push_csv_field(field: &str, params: &CopyCsvFormatParams, out: &mut String) {
-    let q = params.quote as char;
-    let esc = params.escape as char;
-    let delim = params.delimiter as char;
+///
+/// The record is built as bytes, not as a `String`: the delimiter/quote/escape
+/// params are `u8` and csv-core compares them byte-wise, but `u8 as char`
+/// UTF-8-encodes anything >= 0x80 as two bytes, which emits a byte pair the
+/// configured params cannot frame.
+fn push_csv_field(field: &str, params: &CopyCsvFormatParams, out: &mut Vec) {
+    let (q, esc, delim) = (params.quote, params.escape, params.delimiter);
     let needs_quote = field.is_empty()
         || *field == *params.null
         || field
-            .chars()
-            .any(|c| c == delim || c == q || c == esc || c == '\r' || c == '\n');
+            .bytes()
+            .any(|b| b == delim || b == q || b == esc || b == b'\r' || b == b'\n');
     if needs_quote {
         out.push(q);
-        for c in field.chars() {
-            if c == q || c == esc {
+        for b in field.bytes() {
+            if b == q || b == esc {
                 out.push(esc);
             }
-            out.push(c);
+            out.push(b);
         }
         out.push(q);
     } else {
-        out.push_str(field);
+        out.extend_from_slice(field.as_bytes());
     }
 }
 
@@ -201,8 +217,23 @@ fn arbitrary_csv_params(u: &mut Unstructured) -> arbitrary::Result = ["NULL", "\\N", "null", "NA", "-"]
+            .into_iter()
+            .filter(|t| {
+                !t.bytes()
+                    .any(|b| b == delimiter || b == quote || b == escape)
+            })
+            .collect();
+        if candidates.is_empty() {
+            String::new()
+        } else {
+            u.choose(&candidates)?.to_string()
+        }
     } else {
         String::new()
     };
@@ -254,8 +285,8 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
 
     // CSV: fuzz the format params, then emit data the params can actually frame.
     let params = arbitrary_csv_params(&mut u)?;
-    let delim = params.delimiter as char;
-    let mut s = String::new();
+    let delim = params.delimiter;
+    let mut s = Vec::new();
 
     // A configured header means the first record is column names that the
     // decoder skips. Emit a plausible one so the header-skip path runs.
@@ -266,7 +297,7 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
             }
             push_csv_field(&format!("col{col}"), ¶ms, &mut s);
         }
-        s.push('\n');
+        s.push(b'\n');
     }
 
     let rows = u.int_in_range(1usize..=4)?;
@@ -278,17 +309,17 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
             // 1-in-8 NULL: emit the unquoted NULL token (empty field for the
             // default empty marker), which the decoder reads as SQL NULL.
             if u.int_in_range(0u8..=7)? == 0 {
-                s.push_str(¶ms.null);
+                s.extend_from_slice(params.null.as_bytes());
             } else {
                 let mut field = String::new();
                 push_value(&mut u, col, false, &mut field)?;
                 push_csv_field(&field, ¶ms, &mut s);
             }
         }
-        s.push('\n');
+        s.push(b'\n');
     }
 
-    decode_csv(s.as_bytes(), params);
+    decode_csv(&s, params);
     Ok(())
 }
 
diff --git a/src/pgrepr/fuzz/fuzz_targets/value_decode_binary.rs b/src/pgrepr/fuzz/fuzz_targets/value_decode_binary.rs
index 5eb8310f8a8ac..3067eab00be56 100644
--- a/src/pgrepr/fuzz/fuzz_targets/value_decode_binary.rs
+++ b/src/pgrepr/fuzz/fuzz_targets/value_decode_binary.rs
@@ -14,22 +14,26 @@
 //! so any panic is an availability bug. Must never panic.
 //!
 //! A random byte string almost never satisfies these strict decoders: an exact
-//! length check, a version byte, base-10000 digit bounds, a role-id variant tag.
-//! So feeding raw bytes leaves the decoders barely exercised. Instead we pick
-//! a type and *encode a valid binary value for it* (numeric header + digits, the
-//! 16-byte interval triple, the 26-byte mz_aclitem, a jsonb version byte plus
-//! real JSON, in-range date/time/timestamp), so the decoder runs all the way to
-//! the value-construction and range-check logic. We still occasionally truncate
-//! the valid encoding (to hit the length-validation / short-read paths) and, a
-//! quarter of the time, fall back to the "any OID, raw bytes" mode so the
-//! not-implemented branches and crafted-header paths stay covered.
+//! length check, a version byte, a sign word drawn from a five-value set, a
+//! role-id variant tag. So feeding raw bytes leaves the decoders barely
+//! exercised. Instead we pick a type and *encode a valid binary value for it*
+//! (numeric header + digits, the 16-byte interval triple, the 26-byte
+//! mz_aclitem, a jsonb version byte plus real JSON, in-range
+//! date/time/timestamp), so the decoder runs all the way to the
+//! value-construction and range-check logic. We then occasionally corrupt one
+//! byte of that encoding, so the decoders' *content* validation runs and not
+//! just their length checks, or truncate it for the length-validation and
+//! short-read paths. A quarter of the time we instead fall back to the "any OID,
+//! raw bytes" mode so the not-implemented branches and crafted-header paths stay
+//! covered.
 //!
-//! A few arms also reach past the "happy" shape on purpose: the numeric header
-//! sometimes carries an out-of-band weight/dscale or digit words outside
-//! `0..=9999` (negative or `>9999`) to exercise the base-10000 digit-bound and
-//! scale math, and the bytea/text bodies are occasionally multi-KB so the
-//! decoders' allocation/validation paths run on a large value rather than a
-//! handful of bytes.
+//! A few arms also reach past the "happy" shape on purpose. The numeric header
+//! sometimes carries an out-of-band weight/dscale, or digit words outside
+//! `0..=9999`, which Materialize accepts (unlike PostgreSQL's `numeric_recv`,
+//! which rejects them) and folds into the accumulator, so extreme values land on
+//! the trailing precision guard. The bytea/text bodies are occasionally multi-KB
+//! so the decoders' allocation/validation paths run on a large value rather than
+//! a handful of bytes.
 
 #![no_main]
 
@@ -54,6 +58,134 @@ fn push_role_id(u: &mut Unstructured, b: &mut Vec) -> arbitrary::Result<()>
     Ok(())
 }
 
+/// OID of `numeric`. `NumericConstraints` is not exported from `mz_pgrepr`, so
+/// going through the OID is the only way to build a `Type::Numeric` that carries
+/// constraints.
+const NUMERIC_OID: u32 = 1700;
+
+/// A `numeric` type, usually unconstrained but sometimes carrying a
+/// `numeric(precision, scale)` typmod.
+///
+/// Constraints are the only `Type` payload `decode_binary` acts on: they feed
+/// `rescale_numeric`, which rescales the fully client-controlled decoded value.
+/// Production takes that path for every `$1` bound against a `numeric(p, s)`
+/// column, since pgwire derives the parameter type from the planned scalar type.
+fn gen_numeric_type(u: &mut Unstructured) -> arbitrary::Result {
+    if u.int_in_range(0u8..=2)? != 0 {
+        return Ok(Type::Numeric { constraints: None });
+    }
+    let typmod = if u.int_in_range(0u8..=3)? == 0 {
+        u.arbitrary::()?
+    } else {
+        // A well-formed `numeric(precision, scale)` typmod, packed the way
+        // `NumericConstraints::into_typmod` does it.
+        ((u.int_in_range(0i32..=39)? << 16) | u.int_in_range(0i32..=39)?) + 4
+    };
+    // `NumericConstraints::from_typmod` accepts every `i32`, but fall back
+    // instead of unwrapping so the harness itself cannot panic.
+    Ok(Type::from_oid_and_typmod(NUMERIC_OID, typmod)
+        .unwrap_or(Type::Numeric { constraints: None }))
+}
+
+/// Append a JSON scalar literal.
+///
+/// Covers the shapes `JsonbPacker`'s custom visitors exist for: numbers past
+/// `f64` and past numeric's 39-digit precision (`NumberParser` funnels every
+/// number through `strconv::parse_numeric`), `\u` escapes and lone surrogates,
+/// and the `$serde_json::private::Number` magic key with which serde_json spells
+/// an arbitrary-precision number, so that a one-key map of it is parsed as a
+/// *number* rather than as a map.
+fn push_json_scalar(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
+    match u.int_in_range(0u8..=7)? {
+        0 => out.push_str("null"),
+        1 => out.push_str(*u.choose(&["true", "false"])?),
+        2 => out.push_str(&u.arbitrary::()?.to_string()),
+        3 => out.push_str(&format!(
+            "{}.{}",
+            u.arbitrary::()?,
+            u.arbitrary::()?
+        )),
+        4 => out.push_str(*u.choose(&[
+            "1e309",
+            "-1e309",
+            "1e-400",
+            "1e100000",
+            "-0",
+            "0.00000000000000000000000000000000000000001",
+            "111111111111111111111111111111111111111111111",
+        ])?),
+        5 => out.push_str(*u.choose(&[
+            r#""""#,
+            r#""s""#,
+            r#"" ""#,
+            r#""\ud800""#,
+            r#""\udbff\udfff""#,
+            r#""\\""#,
+            r#""\"""#,
+            r#""é""#,
+        ])?),
+        6 => {
+            out.push_str(r#"{"$serde_json::private::Number":"#);
+            // Only a numeric string packs. The others reach `NumberParser`'s
+            // error paths (unparseable number, non-string payload).
+            out.push_str(*u.choose(&[
+                r#""1""#,
+                r#""NaN""#,
+                r#""Infinity""#,
+                r#""1e100000""#,
+                r#""abc""#,
+                r#""""#,
+                "1",
+                "null",
+            ])?);
+            out.push('}');
+        }
+        _ => out.push_str(&u.arbitrary::()?.to_string()),
+    }
+    Ok(())
+}
+
+/// Append a JSON document. `depth` bounds the nesting so we always terminate.
+fn push_json(u: &mut Unstructured, out: &mut String, depth: u8) -> arbitrary::Result<()> {
+    if depth == 0 || u.int_in_range(0u8..=2)? == 0 {
+        return push_json_scalar(u, out);
+    }
+    let n = u.int_in_range(0usize..=4)?;
+    if u.arbitrary::()? {
+        out.push('[');
+        for i in 0..n {
+            if i > 0 {
+                out.push(',');
+            }
+            push_json(u, out, depth - 1)?;
+        }
+        out.push(']');
+    } else {
+        out.push('{');
+        for i in 0..n {
+            if i > 0 {
+                out.push(',');
+            }
+            // Keys come from a small pool so duplicates (which the packer must
+            // dedup) come up often, and so the magic key also appears alongside
+            // other keys, where it is a plain map key rather than a number. An
+            // escaped key cannot be borrowed out of the input, which takes
+            // `KeyClassifier`'s owned branch instead of its borrowed one.
+            out.push_str(*u.choose(&[
+                r#""a""#,
+                r#""b""#,
+                r#""""#,
+                r#""$serde_json::private::Number""#,
+                r#""\u0041""#,
+            ])?);
+            out.push(':');
+            push_json(u, out, depth - 1)?;
+        }
+        out.push('}');
+    }
+    Ok(())
+}
+
 /// Pick a type that has a binary decoder and encode a valid value for it.
 fn gen_typed_value(u: &mut Unstructured) -> arbitrary::Result<(Type, Vec)> {
     let mut b = Vec::new();
@@ -126,17 +258,9 @@ fn gen_typed_value(u: &mut Unstructured) -> arbitrary::Result<(Type, Vec)> {
         // Jsonb: a version byte (1) followed by real JSON text.
         13 => {
             b.push(1);
-            let json: &[u8] = match u.int_in_range(0u8..=7)? {
-                0 => b"null",
-                1 => b"true",
-                2 => b"123",
-                3 => b"-4.5",
-                4 => b"\"s\"",
-                5 => b"[1,2,3]",
-                6 => b"{\"a\":1}",
-                _ => b"[]",
-            };
-            b.extend_from_slice(json);
+            let mut json = String::new();
+            push_json(u, &mut json, 3)?;
+            b.extend_from_slice(json.as_bytes());
             Type::Jsonb
         }
         14 => {
@@ -146,13 +270,15 @@ fn gen_typed_value(u: &mut Unstructured) -> arbitrary::Result<(Type, Vec)> {
         // Numeric: i16 ndigits, i16 weight, u16 sign, u16 dscale, then ndigits
         // base-10000 words (each 0..=9999).
         15 => {
-            let nan = u.int_in_range(0u8..=5)? == 0;
-            let (ndigits, sign): (i16, u16) = if nan {
-                (0, 0xC000)
-            } else if u.int_in_range(0u8..=1)? == 0 {
-                (u.int_in_range(0i16..=4)?, 0x0000)
+            // All five sign words `Numeric::from_sql` accepts. `NaN` and
+            // `±Infinity` return early, skipping the dscale/scale validation and
+            // the trailing `to_width` plus context-status guard that the finite
+            // signs go through, and a real encoding gives them no digit words.
+            let sign = *u.choose(&[0x0000u16, 0x4000, 0xC000, 0xD000, 0xF000])?;
+            let ndigits: i16 = if sign == 0x0000 || sign == 0x4000 {
+                u.int_in_range(0i16..=4)?
             } else {
-                (u.int_in_range(0i16..=4)?, 0x4000)
+                0
             };
             // Mostly a well-formed in-range header so the decoder reaches value
             // construction. Occasionally an out-of-band weight/dscale so the
@@ -172,18 +298,20 @@ fn gen_typed_value(u: &mut Unstructured) -> arbitrary::Result<(Type, Vec)> {
             b.extend_from_slice(&sign.to_be_bytes());
             b.extend_from_slice(&dscale.to_be_bytes());
             // Each base-10000 digit word should be 0..=9999. Occasionally emit
-            // an out-of-band word (>9999, or the full i16 range incl. negative)
-            // to exercise the digit-bound validation path.
+            // one outside that range. `from_sql` reads the words as `u16` and
+            // folds them straight into the accumulator without a bound check, so
+            // this is not a validation path but a way to push the accumulator
+            // toward the trailing precision guard.
             let oob_words = u.int_in_range(0u8..=7)? == 0;
             for _ in 0..ndigits {
                 let word = if oob_words {
-                    u.arbitrary::()?
+                    u.arbitrary::()?
                 } else {
-                    u.int_in_range(0i16..=9999)?
+                    u.int_in_range(0u16..=9999)?
                 };
                 b.extend_from_slice(&word.to_be_bytes());
             }
-            Type::Numeric { constraints: None }
+            gen_numeric_type(u)?
         }
         16 => {
             b.extend_from_slice(&u.arbitrary::()?.to_be_bytes());
@@ -192,7 +320,11 @@ fn gen_typed_value(u: &mut Unstructured) -> arbitrary::Result<(Type, Vec)> {
         17 => {
             // Occasionally a multi-KB UTF-8 body so the text decoder's
             // validation/copy runs on a large value, not just a short one.
-            let max = if u.int_in_range(0u8..=15)? == 0 { 8192 } else { 16 };
+            let max = if u.int_in_range(0u8..=15)? == 0 {
+                8192
+            } else {
+                16
+            };
             push_ascii(u, &mut b, max)?;
             Type::Text
         }
@@ -248,14 +380,32 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
     // length error paths covered.
     if u.int_in_range(0u8..=3)? == 0 {
         let oid = u32::from(u.arbitrary::()?);
+        // A typmod other than -1 also covers `from_oid_and_typmod`'s
+        // `InvalidTypmod` rejection, which no other mode reaches.
+        let typmod = if u.int_in_range(0u8..=3)? == 0 {
+            u.arbitrary::()?
+        } else {
+            -1
+        };
         let rest = u.take_rest();
-        if let Ok(ty) = Type::from_oid(oid) {
+        if let Ok(ty) = Type::from_oid_and_typmod(oid, typmod) {
             let _ = Value::decode_binary(&ty, rest);
         }
         return Ok(());
     }
 
     let (ty, mut body) = gen_typed_value(&mut u)?;
+    // Occasionally corrupt a byte so the decoders' *content* validation runs and
+    // not only their length checks: an unrecognized role-id variant tag, a NUL or
+    // invalid UTF-8 in a `name`, a non-numeric `mz_timestamp`, an unknown numeric
+    // sign, a wrong jsonb version byte. Truncation alone only ever produces
+    // bodies that are short, never bodies that are wrong. For the
+    // Materialize-specific types this is the only source of bad content at all,
+    // since `Type::from_oid` cannot construct them, so raw mode never sees them.
+    if !body.is_empty() && u.int_in_range(0u8..=7)? == 0 {
+        let i = u.int_in_range(0usize..=body.len() - 1)?;
+        body[i] = u.arbitrary::()?;
+    }
     // Occasionally truncate to hit the exact-length / short-read checks.
     if !body.is_empty() && u.int_in_range(0u8..=7)? == 0 {
         let keep = u.int_in_range(0usize..=body.len())?;
diff --git a/src/pgrepr/fuzz/fuzz_targets/value_decode_text.rs b/src/pgrepr/fuzz/fuzz_targets/value_decode_text.rs
index 1ec5c744ff062..ffa3615ba7768 100644
--- a/src/pgrepr/fuzz/fuzz_targets/value_decode_text.rs
+++ b/src/pgrepr/fuzz/fuzz_targets/value_decode_text.rs
@@ -9,8 +9,8 @@
 
 //! Fuzz target: `Value::decode_text` decodes a client-supplied bind-parameter
 //! value in Postgres *text* format. It dispatches on the type and delegates to
-//! the `strconv` parsers (recursively, for array/list/map/record/range), all
-//! over untrusted client bytes. Must never panic.
+//! the `strconv` parsers (recursively, for array/list/map/range), all over
+//! untrusted client bytes. Must never panic.
 //!
 //! A random byte string almost never reaches the interesting recursive
 //! decoders: the array/list/map/range grammars need a leading brace/bracket and
@@ -24,101 +24,246 @@
 //! bodies for `Array`/`List`/`Map`/`Range` (including the `empty` range and the
 //! unsupported-but-parsed `[lo:hi]=` array-dimension prefix). This drives the
 //! recursive element dispatch and the per-element scalar parsers all the way to
-//! value construction and range/normalization checks. We still spend a quarter
-//! of inputs in the "any OID, raw bytes" mode so the not-implemented branches
-//! and the syntax-error paths stay covered.
+//! value construction and range/normalization checks.
+//!
+//! The type is built first and the literal is then built *for* that type at
+//! every nesting level, so a nested container carries an element that is
+//! well-typed for it rather than one re-rolled from an unrelated type. That is
+//! what reaches `parse_list`'s nested-list mode, `parse_map`'s nested-map mode,
+//! and array-of-range element construction. A small share of elements is
+//! deliberately ill-typed so the element-level error paths stay covered too.
+//!
+//! We still spend a quarter of inputs in the "raw bytes" mode so the
+//! not-implemented branches (json, timetz, int2vector) and the scalar
+//! syntax-error paths stay covered. The OID there is drawn from the set
+//! `Type::from_oid` actually resolves: it recognizes only 64 of the 65,536 `u16`
+//! OIDs, so an arbitrary OID would throw away 99.9% of that budget on an
+//! `UnknownOid` early return.
+//!
+//! NOTE: `Type::Record`'s arm stays unreachable. `from_oid` has no `RECORD` arm,
+//! and `decode_text` rejects anonymous composite types outright, so there is
+//! nothing behind it to exercise.
 //!
 //! Excluded from the main workspace because libFuzzer requires nightly Rust.
 
 #![no_main]
 
+use std::sync::LazyLock;
+
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
 use mz_pgrepr::{Type, Value};
 
-/// A well-formed text literal for a scalar leaf type, paired with that type.
-/// The literal is in the *unnested* representation (no extra quoting). The
-/// container builders re-quote/escape it as needed.
-fn gen_leaf(u: &mut Unstructured) -> arbitrary::Result<(Type, String)> {
-    Ok(match u.int_in_range(0u8..=14)? {
-        0 => (
-            Type::Bool,
-            (*u.choose(&["true", "false", "t", "f", "yes", "no", "on", "off", "1", "0"])?)
-                .to_string(),
-        ),
+/// Every OID that `Type::from_oid` resolves. Discovered by scanning the `u16`
+/// space, which is exhaustive: `from_oid` delegates to
+/// `postgres_types::Type::from_oid`, which knows no OID above that and none of
+/// Materialize's custom OIDs.
+static KNOWN_OIDS: LazyLock> = LazyLock::new(|| {
+    (0..=u32::from(u16::MAX))
+        .filter(|oid| Type::from_oid(*oid).is_ok())
+        .collect()
+});
+
+/// A scalar leaf type, i.e. one of the non-container `decode_text` arms that has
+/// a hand-written text parser behind it.
+fn gen_leaf_type(u: &mut Unstructured) -> arbitrary::Result {
+    Ok(match u.int_in_range(0u8..=18)? {
+        0 => Type::Bool,
+        1 => Type::Int2,
+        2 => Type::Int4,
+        3 => Type::Int8,
+        4 => Type::UInt2,
+        5 => Type::UInt4,
+        6 => Type::UInt8,
+        7 => Type::Oid,
+        8 => Type::Float4,
+        9 => Type::Float8,
+        10 => gen_numeric_type(u)?,
+        11 => Type::Interval { constraints: None },
+        12 => Type::Date,
+        13 => Type::Timestamp { precision: None },
+        14 => Type::Uuid,
+        15 => Type::Name,
+        16 => Type::MzTimestamp,
+        17 => Type::MzAclItem,
+        _ => Type::AclItem,
+    })
+}
+
+/// A `numeric`, sometimes carrying precision/scale constraints so that
+/// `rescale_numeric` runs rather than passing the value straight through.
+///
+/// `Type` exposes no constructor for `NumericConstraints`, so the constraints
+/// have to come from a packed typmod.
+fn gen_numeric_type(u: &mut Unstructured) -> arbitrary::Result {
+    let typmod = match u.int_in_range(0u8..=3)? {
+        // A plausible `numeric(p, s)`: the scale converts and `rescale` runs.
+        1 | 2 => {
+            let precision = u.int_in_range(0i32..=39)?;
+            let scale = u.int_in_range(0i32..=39)?;
+            ((precision << 16) | (scale & 0x7ff)) + 4
+        }
+        // Any typmod: a scale outside `0..=39` (the encoding admits negative
+        // scales) instead fails the `NumericMaxScale` conversion.
+        3 => u.arbitrary::()?,
+        // Unconstrained.
+        _ => -1,
+    };
+    let oid = Type::Numeric { constraints: None }.oid();
+    Ok(Type::from_oid_and_typmod(oid, typmod).unwrap_or(Type::Numeric { constraints: None }))
+}
+
+/// A `Type` the generator can write a literal for, nesting at most `depth`
+/// containers deep.
+fn gen_type(u: &mut Unstructured, depth: u8) -> arbitrary::Result {
+    // At max depth, or randomly, stop at a scalar leaf.
+    if depth == 0 || u.int_in_range(0u8..=2)? == 0 {
+        return gen_leaf_type(u);
+    }
+
+    Ok(match u.int_in_range(0u8..=3)? {
+        0 => Type::Array(Box::new(gen_type(u, depth - 1)?)),
+        1 => Type::List(Box::new(gen_type(u, depth - 1)?)),
+        2 => Type::Map {
+            value_type: Box::new(gen_type(u, depth - 1)?),
+        },
+        // A range element must be a totally-ordered scalar, so restrict it to
+        // one of the supported domains.
+        _ => Type::Range {
+            element_type: Box::new(match u.int_in_range(0u8..=4)? {
+                0 => Type::Int4,
+                1 => Type::Int8,
+                2 => gen_numeric_type(u)?,
+                3 => Type::Date,
+                _ => Type::Timestamp { precision: None },
+            }),
+        },
+    })
+}
+
+/// A well-formed text literal *for* `ty`, in the unnested representation (no
+/// extra quoting). The container builders re-quote/escape it as needed.
+fn leaf_literal(u: &mut Unstructured, ty: &Type) -> arbitrary::Result {
+    Ok(match ty {
+        Type::Bool => (*u.choose(&[
+            "true", "false", "t", "f", "yes", "no", "on", "off", "1", "0",
+        ])?)
+        .to_string(),
         // Integers: in- and out-of-range so the parse-int overflow path is hit.
-        1 => (Type::Int2, gen_int_literal(u)?),
-        2 => (Type::Int4, gen_int_literal(u)?),
-        3 => (Type::Int8, gen_int_literal(u)?),
-        4 => (Type::UInt2, gen_int_literal(u)?),
-        5 => (Type::UInt4, gen_int_literal(u)?),
-        6 => (Type::UInt8, gen_int_literal(u)?),
-        7 => (Type::Oid, gen_int_literal(u)?),
-        // Floats, including the special-token branches.
-        8 => (
-            Type::Float8,
-            (*u.choose(&[
-                "0", "-0", "1.5", "-2.25", "3e10", "1.2e-3", "inf", "-inf", "Infinity", "NaN", ".5",
-                "1e400",
-            ])?)
-            .to_string(),
-        ),
-        9 => (Type::Float4, gen_int_literal(u)?),
-        // Numeric: feed digit strings, exponents, and out-of-band magnitudes.
-        10 => (Type::Numeric { constraints: None }, gen_numeric_literal(u)?),
+        Type::Int2
+        | Type::Int4
+        | Type::Int8
+        | Type::UInt2
+        | Type::UInt4
+        | Type::UInt8
+        | Type::Oid => gen_int_literal(u)?,
+        // Floats, including the special tokens and the overflow/underflow
+        // boundaries `parse_float` detects after the fact. The f32-only bounds
+        // matter because `Float4` is the sole caller of `f32::from_str`.
+        Type::Float4 | Type::Float8 => (*u.choose(&[
+            "0", "-0", "1.5", "-2.25", "3e10", "1.2e-3", "inf", "-inf", "Infinity", "NaN", ".5",
+            "1e400", "3.4e39", "1e-46",
+        ])?)
+        .to_string(),
+        // Numeric: digit strings, exponents, and out-of-band magnitudes.
+        Type::Numeric { .. } => gen_numeric_literal(u)?,
         // Interval: a grab bag of the unit/ISO/SQL-standard forms.
-        11 => (
-            Type::Interval { constraints: None },
-            (*u.choose(&[
-                "1 day",
-                "01:02:03",
-                "-1 year 2 mons",
-                "1-2",
-                "P1Y2M3DT4H5M6S",
-                "1 day 2:03:04.567",
-                "@ 5 hours ago",
-                "100000000 years",
-                "1.5 days",
-            ])?)
-            .to_string(),
-        ),
-        // Date / time / timestamp(tz): valid and edge-of-range forms.
-        12 => (
-            Type::Date,
-            (*u.choose(&[
-                "2000-01-01",
-                "0001-01-01 BC",
-                "294276-12-31",
-                "infinity",
-                "-infinity",
-                "1999-02-29",
-                "2024-02-29",
-            ])?)
-            .to_string(),
-        ),
-        13 => (
-            Type::Timestamp { precision: None },
-            (*u.choose(&[
-                "2000-01-01 00:00:00",
-                "1999-12-31 23:59:59.999999",
-                "294277-01-01 00:00:00",
-                "0001-01-01 00:00:00 BC",
-                "infinity",
-                "2024-02-29 12:34:56+05:30",
-            ])?)
-            .to_string(),
-        ),
+        Type::Interval { .. } => (*u.choose(&[
+            "1 day",
+            "01:02:03",
+            "-1 year 2 mons",
+            "1-2",
+            "P1Y2M3DT4H5M6S",
+            "1 day 2:03:04.567",
+            "@ 5 hours ago",
+            "100000000 years",
+            "1.5 days",
+        ])?)
+        .to_string(),
+        // Date / timestamp: valid and edge-of-range forms.
+        Type::Date => (*u.choose(&[
+            "2000-01-01",
+            "0001-01-01 BC",
+            "294276-12-31",
+            "infinity",
+            "-infinity",
+            "1999-02-29",
+            "2024-02-29",
+        ])?)
+        .to_string(),
+        Type::Timestamp { .. } => (*u.choose(&[
+            "2000-01-01 00:00:00",
+            "1999-12-31 23:59:59.999999",
+            "294277-01-01 00:00:00",
+            "0001-01-01 00:00:00 BC",
+            "infinity",
+            "2024-02-29 12:34:56+05:30",
+        ])?)
+        .to_string(),
         // Uuid: canonical, braced, and hyphen-free spellings.
-        _ => (
-            Type::Uuid,
-            (*u.choose(&[
-                "00000000-0000-0000-0000-000000000000",
-                "ffffffffffffffffffffffffffffffff",
-                "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}",
-                "A0EEBC999C0B4EF8BB6D6BB9BD380A11",
-            ])?)
-            .to_string(),
-        ),
+        Type::Uuid => (*u.choose(&[
+            "00000000-0000-0000-0000-000000000000",
+            "ffffffffffffffffffffffffffffffff",
+            "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}",
+            "A0EEBC999C0B4EF8BB6D6BB9BD380A11",
+        ])?)
+        .to_string(),
+        // Name truncates at 64 bytes without splitting a multibyte character,
+        // so feed runs whose characters straddle that boundary.
+        Type::Name => match u.int_in_range(0u8..=5)? {
+            0 => String::new(),
+            1 => "a".to_string(),
+            2 => "a".repeat(64),
+            3 => "a".repeat(65),
+            // 66 bytes of 2-byte chars: byte 64 is mid-character.
+            4 => "é".repeat(33),
+            // 68 bytes of 4-byte chars: byte 64 ends a character, byte 65 does
+            // not.
+            _ => "😀".repeat(17),
+        },
+        // mz_timestamp parses as a u64 of milliseconds and otherwise falls back
+        // to the timestamptz parser, whose result must fit a u64.
+        Type::MzTimestamp => (*u.choose(&[
+            "0",
+            "18446744073709551615",
+            "18446744073709551616",
+            "-1",
+            " 42 ",
+            "2000-01-01 00:00:00",
+            "1969-12-31 23:59:59+00",
+        ])?)
+        .to_string(),
+        // mz_aclitem is `grantee=privileges/grantor` over `RoleId`s.
+        Type::MzAclItem => (*u.choose(&[
+            "u1=UC/u2",
+            "=arwd/s1",
+            "s1=RBNP/g1",
+            "u1=/u2",
+            "u1=X/u2",
+            "=/",
+            "u1",
+            "u=U/u1",
+            "p=U/p",
+            "u18446744073709551616=U/u1",
+            "é1=U/u2",
+            "u1=U/u2/u3",
+        ])?)
+        .to_string(),
+        // aclitem is the same shape over role OIDs.
+        Type::AclItem => (*u.choose(&[
+            "1=UC/2",
+            "=arwd/0",
+            "1=/2",
+            "1=Z/2",
+            "=/",
+            "1",
+            "99999999999=U/2",
+            "-1=U/2",
+        ])?)
+        .to_string(),
+        // Types the generator does not build. Any short token is a valid
+        // literal for the text-like ones and a syntax probe for the rest.
+        _ => (*u.choose(&["", "abc", "0", " x "])?).to_string(),
     })
 }
 
@@ -144,7 +289,11 @@ fn gen_numeric_literal(u: &mut Unstructured) -> arbitrary::Result {
         0 => "0".to_string(),
         1 => u.arbitrary::()?.to_string(),
         2 => format!("{}.{}", u.arbitrary::()?, u.arbitrary::()?),
-        3 => format!("{}e{}", u.int_in_range(1i32..=9)?, u.int_in_range(-40i32..=40)?),
+        3 => format!(
+            "{}e{}",
+            u.int_in_range(1i32..=9)?,
+            u.int_in_range(-40i32..=40)?
+        ),
         4 => "NaN".to_string(),
         5 => "-Infinity".to_string(),
         // Long digit run (more than the 39 significant digits numeric keeps).
@@ -185,175 +334,148 @@ fn escape_for_container(u: &mut Unstructured, body: &str) -> arbitrary::Result arbitrary::Result<(Type, String)> {
-    // At max depth, or randomly, emit a scalar leaf.
-    if depth == 0 || u.int_in_range(0u8..=2)? == 0 {
-        return gen_leaf(u);
+/// A literal for `ty`: a scalar literal for a leaf, and a container body built
+/// for the container's own element type so that nested elements stay well-typed.
+///
+/// Recursion is bounded by `ty`, which the caller built with a depth limit.
+fn elem_literal(u: &mut Unstructured, ty: &Type) -> arbitrary::Result {
+    // Occasionally an element literal for an unrelated type, so the
+    // element-level parse-error paths stay covered. Tested against the high
+    // bound: `int_in_range` yields its low bound once the input is exhausted,
+    // and this branch must stay rare even then. Keep it well below the
+    // per-container element count, or most container bodies end up ill-typed.
+    if u.int_in_range(0u8..=63)? == 63 {
+        let ty = gen_leaf_type(u)?;
+        return leaf_literal(u, &ty);
     }
 
-    Ok(match u.int_in_range(0u8..=3)? {
-        // Array: `{e1,e2,...}`, possibly multi-dimensional, possibly with NULLs,
-        // possibly prefixed with the (unsupported, but parsed) dimension syntax.
-        0 => {
-            let (elem_ty, _) = gen_value(u, depth - 1)?;
-            let n = u.int_in_range(0usize..=4)?;
-            let mut body = String::new();
-            // Occasionally emit the `[lo:hi]=` dimension prefix, which the parser
-            // recognizes and then rejects as unsupported.
-            if u.int_in_range(0u8..=7)? == 0 {
-                body.push_str(&format!("[{}:{}]=", u.int_in_range(-2i32..=2)?, n));
-            }
-            // Optionally wrap in extra braces for a multi-dimensional shape.
-            let extra_dims = u.int_in_range(0u8..=2)?;
-            for _ in 0..extra_dims {
-                body.push('{');
-            }
-            body.push('{');
-            for i in 0..n {
-                if i > 0 {
-                    body.push(',');
-                }
-                if u.int_in_range(0u8..=6)? == 0 {
-                    body.push_str(*u.choose(&["NULL", "null", "NuLl"])?);
-                } else {
-                    let (_, elem) = elem_literal(u, &elem_ty, depth - 1)?;
-                    body.push_str(&escape_for_container(u, &elem)?);
-                }
-            }
-            body.push('}');
-            for _ in 0..extra_dims {
-                body.push('}');
-            }
-            (Type::Array(Box::new(elem_ty)), body)
+    match ty {
+        Type::Array(elem_ty) => array_body(u, elem_ty),
+        Type::List(elem_ty) => list_body(u, elem_ty),
+        Type::Map { value_type } => map_body(u, value_type),
+        Type::Range { element_type } => range_body(u, element_type),
+        _ => leaf_literal(u, ty),
+    }
+}
+
+/// An array body: `{e1,e2,...}`, possibly multi-dimensional, possibly with
+/// NULLs, possibly prefixed with the (unsupported, but parsed) dimension syntax.
+fn array_body(u: &mut Unstructured, elem_ty: &Type) -> arbitrary::Result {
+    let n = u.int_in_range(0usize..=4)?;
+    let mut body = String::new();
+    // Occasionally emit the `[lo:hi]=` dimension prefix, which the parser
+    // recognizes and then rejects as unsupported. Tested against the high bound
+    // so that an exhausted input does not turn every array into a body that
+    // `parse_array` rejects at byte 0.
+    if u.int_in_range(0u8..=7)? == 7 {
+        body.push_str(&format!("[{}:{}]=", u.int_in_range(-2i32..=2)?, n));
+    }
+    // Optionally wrap in extra braces for a multi-dimensional shape.
+    let extra_dims = u.int_in_range(0u8..=2)?;
+    for _ in 0..extra_dims {
+        body.push('{');
+    }
+    body.push('{');
+    for i in 0..n {
+        if i > 0 {
+            body.push(',');
         }
-        // List: `{e1,e2,...}`. Nested lists allowed via embedded braces.
-        1 => {
-            let (elem_ty, _) = gen_value(u, depth - 1)?;
-            let nested_list = matches!(elem_ty, Type::List(_));
-            let n = u.int_in_range(0usize..=4)?;
-            let mut body = String::from("{");
-            for i in 0..n {
-                if i > 0 {
-                    body.push(',');
-                }
-                if u.int_in_range(0u8..=6)? == 0 {
-                    body.push_str("NULL");
-                } else {
-                    let (_, elem) = elem_literal(u, &elem_ty, depth - 1)?;
-                    // A nested list element keeps its braces. Other elements are
-                    // quoted/escaped.
-                    if nested_list {
-                        body.push_str(&elem);
-                    } else {
-                        body.push_str(&escape_for_container(u, &elem)?);
-                    }
-                }
-            }
-            body.push('}');
-            (Type::List(Box::new(elem_ty)), body)
+        if u.int_in_range(0u8..=6)? == 0 {
+            body.push_str(*u.choose(&["NULL", "null", "NuLl"])?);
+        } else {
+            let elem = elem_literal(u, elem_ty)?;
+            body.push_str(&escape_for_container(u, &elem)?);
         }
-        // Map: `{k1=>v1,k2=>v2,...}` with text keys.
-        2 => {
-            let (val_ty, _) = gen_value(u, depth - 1)?;
-            let nested_map = matches!(val_ty, Type::Map { .. });
-            let n = u.int_in_range(0usize..=4)?;
-            let mut body = String::from("{");
-            for i in 0..n {
-                if i > 0 {
-                    body.push(',');
-                }
-                let key = *u.choose(&["a", "b", "key one", "k\"q", "", "=>"])?;
-                body.push_str(&escape_for_container(u, key)?);
-                body.push_str("=>");
-                if u.int_in_range(0u8..=6)? == 0 {
-                    body.push_str("NULL");
-                } else {
-                    let (_, val) = elem_literal(u, &val_ty, depth - 1)?;
-                    if nested_map {
-                        body.push_str(&val);
-                    } else {
-                        body.push_str(&escape_for_container(u, &val)?);
-                    }
-                }
-            }
-            body.push('}');
-            (Type::Map { value_type: Box::new(val_ty) }, body)
+    }
+    body.push('}');
+    for _ in 0..extra_dims {
+        body.push('}');
+    }
+    Ok(body)
+}
+
+/// A list body: `{e1,e2,...}`. A nested list keeps its braces bare, which is
+/// what `parse_list`'s nested-list mode expects.
+fn list_body(u: &mut Unstructured, elem_ty: &Type) -> arbitrary::Result {
+    let nested_list = matches!(elem_ty, Type::List(_));
+    let n = u.int_in_range(0usize..=4)?;
+    let mut body = String::from("{");
+    for i in 0..n {
+        if i > 0 {
+            body.push(',');
         }
-        // Range: `empty`, `[lo,hi)`, `(,hi]`, `[lo,)`, etc. Range elements must
-        // be a totally-ordered scalar, so restrict to one of the supported
-        // domains.
-        _ => {
-            let elem_ty = match u.int_in_range(0u8..=4)? {
-                0 => Type::Int4,
-                1 => Type::Int8,
-                2 => Type::Numeric { constraints: None },
-                3 => Type::Date,
-                _ => Type::Timestamp { precision: None },
-            };
-            if u.int_in_range(0u8..=5)? == 0 {
-                return Ok((Type::Range { element_type: Box::new(elem_ty) }, "empty".to_string()));
-            }
-            let lo_inc = u.arbitrary::()?;
-            let hi_inc = u.arbitrary::()?;
-            let lo = if u.int_in_range(0u8..=2)? == 0 {
-                String::new()
+        if u.int_in_range(0u8..=6)? == 0 {
+            body.push_str("NULL");
+        } else {
+            let elem = elem_literal(u, elem_ty)?;
+            if nested_list {
+                body.push_str(&elem);
             } else {
-                gen_range_bound(u, &elem_ty)?
-            };
-            let hi = if u.int_in_range(0u8..=2)? == 0 {
-                String::new()
-            } else {
-                gen_range_bound(u, &elem_ty)?
-            };
-            let body = format!(
-                "{}{},{}{}",
-                if lo_inc { '[' } else { '(' },
-                lo,
-                hi,
-                if hi_inc { ']' } else { ')' },
-            );
-            (Type::Range { element_type: Box::new(elem_ty) }, body)
+                body.push_str(&escape_for_container(u, &elem)?);
+            }
         }
-    })
+    }
+    body.push('}');
+    Ok(body)
 }
 
-/// Generate a literal for a *specific* element type (so container element types
-/// stay consistent), bottoming out via the generic builder for containers.
-fn elem_literal(u: &mut Unstructured, ty: &Type, depth: u8) -> arbitrary::Result<(Type, String)> {
-    match ty {
-        Type::Bool => Ok((ty.clone(), (*u.choose(&["t", "f", "true", "false"])?).to_string())),
-        Type::Int2 | Type::Int4 | Type::Int8 | Type::UInt2 | Type::UInt4 | Type::UInt8
-        | Type::Oid | Type::Float4 => Ok((ty.clone(), gen_int_literal(u)?)),
-        Type::Float8 => Ok((
-            ty.clone(),
-            (*u.choose(&["1.5", "-2.25", "inf", "NaN", "0"])?).to_string(),
-        )),
-        Type::Numeric { .. } => Ok((ty.clone(), gen_numeric_literal(u)?)),
-        Type::Date => Ok((
-            ty.clone(),
-            (*u.choose(&["2000-01-01", "1999-12-31", "infinity"])?).to_string(),
-        )),
-        Type::Timestamp { .. } => Ok((
-            ty.clone(),
-            (*u.choose(&["2000-01-01 00:00:00", "1999-12-31 23:59:59"])?).to_string(),
-        )),
-        Type::Uuid => Ok((
-            ty.clone(),
-            "00000000-0000-0000-0000-000000000000".to_string(),
-        )),
-        Type::Interval { .. } => {
-            Ok((ty.clone(), (*u.choose(&["1 day", "01:02:03", "1-2"])?).to_string()))
+/// A map body: `{k1=>v1,k2=>v2,...}` with text keys. A nested map keeps its
+/// braces bare, which is what `parse_map`'s nested-map mode expects.
+fn map_body(u: &mut Unstructured, val_ty: &Type) -> arbitrary::Result {
+    let nested_map = matches!(val_ty, Type::Map { .. });
+    let n = u.int_in_range(0usize..=4)?;
+    let mut body = String::from("{");
+    for i in 0..n {
+        if i > 0 {
+            body.push(',');
         }
-        // Containers and everything else: delegate to the recursive builder,
-        // which will pick its own (possibly different) shape but keep it valid.
-        _ => gen_value(u, depth),
+        let key = *u.choose(&["a", "b", "key one", "k\"q", "", "=>"])?;
+        body.push_str(&escape_for_container(u, key)?);
+        body.push_str("=>");
+        if u.int_in_range(0u8..=6)? == 0 {
+            body.push_str("NULL");
+        } else {
+            let val = elem_literal(u, val_ty)?;
+            if nested_map {
+                body.push_str(&val);
+            } else {
+                body.push_str(&escape_for_container(u, &val)?);
+            }
+        }
+    }
+    body.push('}');
+    Ok(body)
+}
+
+/// A range body: `empty`, `[lo,hi)`, `(,hi]`, `[lo,)`, etc.
+fn range_body(u: &mut Unstructured, elem_ty: &Type) -> arbitrary::Result {
+    if u.int_in_range(0u8..=5)? == 0 {
+        return Ok("empty".to_string());
     }
+    let lo_inc = u.arbitrary::()?;
+    let hi_inc = u.arbitrary::()?;
+    let lo = if u.int_in_range(0u8..=2)? == 0 {
+        String::new()
+    } else {
+        gen_range_bound(u, elem_ty)?
+    };
+    let hi = if u.int_in_range(0u8..=2)? == 0 {
+        String::new()
+    } else {
+        gen_range_bound(u, elem_ty)?
+    };
+    Ok(format!(
+        "{}{},{}{}",
+        if lo_inc { '[' } else { '(' },
+        lo,
+        hi,
+        if hi_inc { ']' } else { ')' },
+    ))
 }
 
-/// A scalar range-bound literal matching the range's element type.
+/// A scalar range-bound literal matching the range's element type. Narrower than
+/// [`leaf_literal`] on purpose: the bounds have to be comparable for the
+/// normalization check to do anything.
 fn gen_range_bound(u: &mut Unstructured, ty: &Type) -> arbitrary::Result {
     Ok(match ty {
         Type::Date => (*u.choose(&["2000-01-01", "1999-12-31", "2024-06-06"])?).to_string(),
@@ -367,19 +489,21 @@ fn gen_range_bound(u: &mut Unstructured, ty: &Type) -> arbitrary::Result
 }
 
 fn run(mut u: Unstructured) -> arbitrary::Result<()> {
-    // A quarter of the time, the raw mode: any OID + raw remaining bytes.
-    // This keeps the not-implemented branches (json, record, timetz,
-    // int2vector) and the scalar syntax-error paths covered.
+    // A quarter of the time, the raw mode: a known OID + raw remaining bytes.
+    // This keeps the not-implemented branches and the scalar syntax-error paths
+    // covered, including the types the typed mode never builds.
     if u.int_in_range(0u8..=3)? == 0 {
-        let oid = u32::from(u.arbitrary::()?);
+        let oid = *u.choose(KNOWN_OIDS.as_slice())?;
         let rest = u.take_rest();
+        // `KNOWN_OIDS` holds only OIDs that resolve, so this always matches.
         if let Ok(ty) = Type::from_oid(oid) {
             let _ = Value::decode_text(&ty, rest);
         }
         return Ok(());
     }
 
-    let (ty, body) = gen_value(&mut u, 3)?;
+    let ty = gen_type(&mut u, 3)?;
+    let body = elem_literal(&mut u, &ty)?;
     let _ = Value::decode_text(&ty, body.as_bytes());
     Ok(())
 }
diff --git a/src/pgtz/fuzz/fuzz_targets/timezone_parse.rs b/src/pgtz/fuzz/fuzz_targets/timezone_parse.rs
index 6eaeb83420a34..5cccf7875783c 100644
--- a/src/pgtz/fuzz/fuzz_targets/timezone_parse.rs
+++ b/src/pgtz/fuzz/fuzz_targets/timezone_parse.rs
@@ -12,21 +12,33 @@
 //! tokenizer + offset builder, in both ISO and POSIX modes. Any panic is an
 //! availability bug.
 //!
-//! The interesting surface is the *offset tokenizer*: `tokenize_timezone`
-//! grabs the first alphabetic run as a single `TzName` and returns immediately
-//! (so any POSIX DST-rule tail is silently discarded, making fuzzing that
-//! grammar dead weight), while everything else flows through `parse_num`, which
-//! splits long all-digit runs into `[..hhhh]mm` chunks unless a `:` is present,
-//! plus the punctuation-as-delimiter trimming and the `z`/`Z`-only-at-end rule.
-//! `build_timezone_offset_second` then matches the token stream against twelve
-//! fixed `±H[H][:M[M][:S[S]]]` / `±HHH` / `TzName` / `Zulu` shapes and enforces
-//! the `hour<=15`, `min<60`, `sec<60` bounds. So we generate inputs that stress
-//! exactly that math: long all-digit runs (`+00000100`, `+0000001:000001`),
-//! the hour/min/sec boundaries (`+15:59:59`, `+16`, `+0:60`), the colon-vs-no-
-//! colon `split_nums` toggle, punctuation-delimited junk around a real offset,
-//! bare `z`/`Z` placed mid-string vs at the end, abbreviations drawn from
-//! `TIMEZONE_ABBREVS`, and case-mangled IANA names. A quarter of inputs stay
-//! the raw bytes so the tokenizer reject paths keep their coverage.
+//! The interesting surface is the *offset tokenizer*. On the first ASCII
+//! alphabetic character `tokenize_timezone` pushes the entire remainder of the
+//! string as a single `TzName` and returns, so everything after a letter is
+//! folded into the name rather than tokenized. Two consequences shape this
+//! generator: fuzzing the POSIX DST-rule grammar is dead weight, and only
+//! inputs whose letters come *last* get both halves of the string tokenized.
+//! Everything else flows through `parse_num`, which splits long all-digit runs
+//! into `[..hhhh]mm` chunks unless a `:` is present, plus the
+//! punctuation-as-`Delim` handling and the `z`/`Z`-only-at-end rule.
+//! `build_timezone_offset_second` then matches the token stream against a table
+//! of `±H[H][:M[M][:S[S]]]` / `±HH H` / `TzName` / `Zulu` shapes and enforces
+//! the `hour<=15`, `min<60`, `sec<60` bounds.
+//!
+//! So we generate inputs that stress exactly that math: long all-digit runs
+//! (`+00000100`, `+0000001:000001`), runs long enough to overflow the `u64`
+//! parse, the hour/min/sec boundaries (`+15:59:59`, `+16`, `+0:60`), the
+//! colon-vs-no-colon `split_nums` toggle, *interior* punctuation, a `z`/`Z`
+//! after digits, abbreviations from `TIMEZONE_ABBREVS` placed after an offset
+//! so both halves tokenize, and case-mangled IANA names. A quarter of inputs
+//! stay the raw bytes so the tokenizer reject paths keep their coverage.
+//!
+//! NOTE: two of the twelve entries in that format table, `[±, Num, Num, Num]`,
+//! are unreachable. `parse_num` is the only thing that pushes `Num` and it
+//! pushes at most two per digit run, while every other tokenizer arm pushes a
+//! separator, `Zulu`, or `TzName` in between, so no input yields three
+//! consecutive `Num` tokens. The widest all-digit offset (`+00000100`) matches
+//! the three-token `[Plus, Num, Num]` shape instead.
 
 #![no_main]
 
@@ -36,38 +48,49 @@ use mz_pgtz::timezone::{Timezone, TimezoneSpec};
 
 /// IANA names exercising fractional-hour offsets and DST, in canonical casing.
 /// `gen_named` may re-case them to hit the case-insensitive lookup path.
+/// `posixrules` is not a chrono-tz zone, so it covers the reject path.
 const NAMED: &[&str] = &[
     "UTC",
     "GMT",
     "America/New_York",
     "Europe/London",
-    "Asia/Kolkata",          // :30 offset
-    "Australia/Lord_Howe",   // :30 offset with DST
-    "Pacific/Chatham",       // :45 offset
+    "Asia/Kolkata",        // :30 offset
+    "Australia/Lord_Howe", // :30 offset with DST
+    "Pacific/Chatham",     // :45 offset
     "America/Argentina/Buenos_Aires",
     "Etc/GMT+12",
     "posixrules",
 ];
 
 /// A spread of abbreviations from `TIMEZONE_ABBREVS`: fixed-offset ones, DST
-/// ones, and ones that alias to a `Tz`, so the abbrev lookup + fallback to
-/// `Tz::from_str_insensitive` both run. `EST`/`PST`/... also double as the
-/// leading `std` name of a POSIX-looking string (whose offset tail is what the
-/// tokenizer actually keeps).
+/// ones, and ones that alias to a `Tz`, so the abbrev lookup and its fallback
+/// to `Tz::from_str_insensitive` both run. `WEST` is the one entry absent from
+/// `src/pgtz/tznames/Default`, so it drives the miss-then-fallback-then-reject
+/// path.
 const ABBREVS: &[&str] = &[
-    "EST", "EDT", "PST", "PDT", "CST", "CDT", "MST", "MDT", "CET", "CEST", "EET",
-    "EEST", "BST", "IST", "JST", "ACDT", "ACST", "AEST", "AEDT", "NZST", "NZDT",
-    "CHADT", "CHAST", "HKT", "WET", "WEST", "UCT", "ZULU", "GMT", "UTC",
+    "EST", "EDT", "PST", "PDT", "CST", "CDT", "MST", "MDT", "CET", "CEST", "EET", "EEST", "BST",
+    "IST", "JST", "ACDT", "ACST", "AEST", "AEDT", "NZST", "NZDT", "CHADT", "CHAST", "HKT", "WET",
+    "WEST", "UCT", "ZULU", "GMT", "UTC",
 ];
 
+/// ASCII whitespace and punctuation, which the tokenizer trims at the edges of
+/// the string and turns into a `Delim` in the interior. Deliberately excludes
+/// `+`/`-`, the two characters the trimming closure spares.
+const JUNK: &[char] = &[' ', '!', '?', '.', ',', '*', '/', '#', '~', '\t'];
+
 /// Emit a numeric UTC offset, biased toward the tokenizer/builder boundaries:
 /// `z`/`Z`, `±HH`, `±HH:MM`, `±HH:MM:SS`, long all-digit runs that `parse_num`
-/// must chunk, and the exact `hour<=15` / `min<60` / `sec<60` edges.
+/// must chunk or fail to parse, and the exact `hour<=15` / `min<60` / `sec<60`
+/// edges.
+///
+/// Every shape but the bare `z`/`Z` ends in a digit. Callers that append more
+/// text must check for that, since a trailing letter would make the tokenizer
+/// fold the whole string into one `TzName`.
 fn gen_offset(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
-    match u.int_in_range(0u8..=8)? {
+    match u.int_in_range(0u8..=9)? {
         // Bare Zulu (only valid at end-of-string).
         0 => {
-            out.push(if u.ratio(1, 2)? { 'z' } else { 'Z' });
+            out.push(zulu(u)?);
             return Ok(());
         }
         // Hour at/around the `<= 15` boundary.
@@ -104,11 +127,12 @@ fn gen_offset(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
             }
             out.push_str(&u.int_in_range(0u32..=999)?.to_string());
         }
-        // Colon-delimited long all-digit runs (colon disables `split_nums`),
-        // e.g. `+0000001:000001:000001`.
+        // Colon-delimited long all-digit runs (a colon disables `split_nums`),
+        // e.g. `+0000001:000001:000001`. At least two parts: a single part emits
+        // no colon at all and degenerates into the arm above.
         5 => {
             out.push(sign(u)?);
-            let parts = u.int_in_range(1u8..=3)?;
+            let parts = u.int_in_range(2u8..=3)?;
             for p in 0..parts {
                 if p > 0 {
                     out.push(':');
@@ -120,6 +144,22 @@ fn gen_offset(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
                 out.push_str(&u.int_in_range(0u32..=99)?.to_string());
             }
         }
+        // A digit run long enough to overflow the `u64` parse in `parse_num`.
+        // The run must start nonzero: leading zeros accumulate to zero and never
+        // overflow, however long the padding. `parse_num` parses the whole run
+        // when a colon is present (`split_nums` off, overflowing past 20 digits)
+        // and the run minus its last two digits otherwise, so this range
+        // straddles both thresholds.
+        6 => {
+            out.push(sign(u)?);
+            let digits = u.int_in_range(18u32..=24)?;
+            for _ in 0..digits {
+                out.push(*u.choose(&['1', '8', '9'])?);
+            }
+            if u.ratio(1, 2)? {
+                out.push_str(":00");
+            }
+        }
         // Ordinary `±HH[:MM[:SS]]` across the full valid range.
         _ => {
             out.push(sign(u)?);
@@ -142,6 +182,10 @@ fn sign(u: &mut Unstructured) -> arbitrary::Result {
     Ok(if u.ratio(1, 2)? { '+' } else { '-' })
 }
 
+fn zulu(u: &mut Unstructured) -> arbitrary::Result {
+    Ok(if u.ratio(1, 2)? { 'z' } else { 'Z' })
+}
+
 /// Emit an IANA name, sometimes case-mangled to hit `from_str_insensitive`.
 fn gen_named(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
     let name = *u.choose(NAMED)?;
@@ -163,40 +207,77 @@ fn gen_named(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
     Ok(())
 }
 
-/// Wrap an inner spec in leading/trailing whitespace and ASCII punctuation,
-/// which the tokenizer trims (except `+`/`-`) or treats as `Delim`. This keeps
-/// the `" ! ? ! - 5:15 ? ! ? "`-style paths covered.
-fn gen_punct_wrapped(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
-    const JUNK: &[char] = &[' ', '!', '?', '.', ',', '*', '/', '#', '~', '\t'];
-    let lead = u.int_in_range(0u8..=3)?;
-    for _ in 0..lead {
+/// Emit an offset whose components are separated by *interior* punctuation,
+/// e.g. `+05!30` or `-12.30`.
+///
+/// Interior placement is what makes this arm distinct: the tokenizer trims
+/// leading and trailing whitespace and punctuation, so an offset merely
+/// *bracketed* in junk is byte-identical to the bare offset by the time it is
+/// tokenized, and yields no `Delim` at all. Empty components put two separators
+/// back to back, producing the odd token streams that match no format at all
+/// (`-5::15` gives `[Dash, Num, Colon, Colon, Num]`), which drives the
+/// mismatch/reset arm of `build_timezone_offset_second`.
+fn gen_punct_delimited(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
+    // Leading junk is trimmed away, so keep it rare, just enough to cover the
+    // trimming closure itself.
+    if u.ratio(1, 4)? {
         out.push(*u.choose(JUNK)?);
     }
-    gen_offset(u, out)?;
-    let trail = u.int_in_range(0u8..=3)?;
-    for _ in 0..trail {
-        out.push(*u.choose(JUNK)?);
+    out.push(sign(u)?);
+    let parts = u.int_in_range(2u8..=4)?;
+    for p in 0..parts {
+        if p > 0 {
+            // `:` keeps `split_nums` disabled, JUNK yields a `Delim`.
+            if u.ratio(1, 3)? {
+                out.push(':');
+            } else {
+                out.push(*u.choose(JUNK)?);
+            }
+        }
+        if u.ratio(7, 8)? {
+            out.push_str(&u.int_in_range(0u32..=99)?.to_string());
+        }
     }
     Ok(())
 }
 
 fn gen_tz(u: &mut Unstructured, out: &mut String) -> arbitrary::Result<()> {
-    match u.int_in_range(0u8..=6)? {
+    match u.int_in_range(0u8..=9)? {
         0 => gen_named(u, out)?,
         1 => out.push_str(u.choose(ABBREVS)?),
-        2 | 3 => gen_offset(u, out)?,
-        4 => gen_punct_wrapped(u, out)?,
-        // An abbreviation immediately followed by an offset: the tokenizer keeps
-        // the abbrev as a `TzName` and returns, so the offset tail is ignored,
-        // but this still stresses the "first alpha wins" early return.
-        5 => {
-            out.push_str(u.choose(ABBREVS)?);
+        2 | 3 | 4 => {
             gen_offset(u, out)?;
+            // A `z`/`Z` after digits is the only shape that reaches the
+            // `parse_num` call in the tokenizer's Zulu arm with digits pending,
+            // e.g. `+05:30z` -> `[Plus, Num, Colon, Num, Zulu]`. Appending it to
+            // a bare `z` offset would instead just build a two-letter `TzName`.
+            if out.ends_with(|c: char| c.is_ascii_digit()) && u.ratio(1, 4)? {
+                out.push(zulu(u)?);
+            }
         }
-        // A bare `z`/`Z` placed *before* more text, so it is NOT at end-of-string
-        // and must be tokenized as a `TzName`, not `Zulu`.
+        5 | 6 => gen_punct_delimited(u, out)?,
+        // An offset, a delimiter, then an abbreviation, e.g. `+05:30 EST` ->
+        // `[Plus, Num, Colon, Num, Delim, TzName("EST")]`. The letters have to
+        // come last for both halves to tokenize, and this reaches a `Delim`, a
+        // short `TzName`, and a six-token stream in one input.
+        7 | 8 => {
+            gen_offset(u, out)?;
+            if out.ends_with(|c: char| c.is_ascii_digit()) {
+                out.push(*u.choose(JUNK)?);
+                out.push_str(u.choose(ABBREVS)?);
+            }
+        }
+        // Letters first, so the alphabetic branch takes the whole remaining
+        // string and the numeric tail is never tokenized: the input collapses
+        // into one long `TzName` that misses both the abbrev table and
+        // `Tz::from_str_insensitive`. Low weight, because its only marginal
+        // coverage over a bare abbreviation is a longer lookup miss.
         _ => {
-            out.push(if u.ratio(1, 2)? { 'z' } else { 'Z' });
+            if u.ratio(1, 2)? {
+                out.push_str(u.choose(ABBREVS)?);
+            } else {
+                out.push(zulu(u)?);
+            }
             gen_offset(u, out)?;
         }
     }
diff --git a/src/pgwire/fuzz/corpus.dict b/src/pgwire/fuzz/corpus.dict
index 81586cb1f734a..cd3931c39a94d 100644
--- a/src/pgwire/fuzz/corpus.dict
+++ b/src/pgwire/fuzz/corpus.dict
@@ -4,6 +4,11 @@
 # (startup/SSL/cancel messages omit the tag). Seeding the type tags and the
 # special startup/SSL/cancel magic version codes lets the mutator reach the
 # per-message decoders instead of being rejected as an unknown tag.
+#
+# These are raw wire bytes, so they pay off on the target's raw-bytes branch
+# (the one the seeds in prepare-corpus.sh steer onto). On the grammar branch the
+# input is consumed as generation choices rather than wire bytes, and a token
+# there is just entropy.
 
 # Frontend message type tags.
 "Q"
diff --git a/src/pgwire/fuzz/fuzz_targets/codec_decode.rs b/src/pgwire/fuzz/fuzz_targets/codec_decode.rs
index 7947ff0cecd1f..1bff0ed0fa7ff 100644
--- a/src/pgwire/fuzz/fuzz_targets/codec_decode.rs
+++ b/src/pgwire/fuzz/fuzz_targets/codec_decode.rs
@@ -15,15 +15,14 @@
 //! A frame is `[type:1][len:4 BE][body:len-4]`. Random bytes rarely have a
 //! length field that matches the bytes that follow, so the decoder bails in the
 //! header before reaching the per-message body parsers (Query/Parse/Bind/
-//! Describe/Execute/…), and once one frame errors, the streaming decoder stops,
-//! so later frames never decode either. So we consume the byte stream as grammar
-//! choices and emit correctly-framed messages: a valid type tag, the right
-//! length, and (usually) a valid body for that type, concatenating several so
-//! the decoder walks frame after frame. A quarter of inputs are still the raw
-//! bytes, and a quarter of frames carry an arbitrary body, so the header
-//! validation and per-message error paths stay covered.
+//! Describe/Execute/…). So we consume the byte stream as grammar choices and emit
+//! correctly-framed messages: a valid type tag, the right length, and (usually) a
+//! valid body for that type, concatenating several so the decoder walks frame
+//! after frame. A quarter of inputs are still the raw bytes, and a quarter of
+//! frames carry an arbitrary body, so the header validation and per-message error
+//! paths stay covered.
 //!
-//! Beyond well-formed frames we deliberately stress two thin spots:
+//! Beyond well-formed frames we deliberately stress three thin spots:
 //!
 //! * **Count-driven loops.** The body parsers for Parse and Bind read an `i16`
 //!   element count (param-type / format-code / parameter counts) and then loop
@@ -31,8 +30,8 @@
 //!   own `i32` byte length. We sometimes emit a huge count or length (up to
 //!   `i16::MAX` / a large positive `i32`) backed by a body far too short to
 //!   satisfy it, so the loops read off the end of the cursor and must error out
-//!   gracefully rather than over-read, over-allocate, or panic. Long cstrings
-//!   feed the same idea on the string side.
+//!   gracefully rather than over-read or panic. Long cstrings feed the same idea
+//!   on the string side.
 //!
 //! * **Streaming / partial-frame reassembly.** The codec is a `tokio_util`
 //!   `Decoder`: it advances `Head -> Data -> Head` across calls and returns
@@ -43,15 +42,32 @@
 //!   chunks, so it parks in the `Data` await-more-bytes state and resumes when
 //!   the rest shows up.
 //!
+//! * **The pre-auth SASL/password grammars.** `Codec::decode` does not parse
+//!   auth payloads. Its `b'p'` arm copies the body verbatim into
+//!   `RawAuthentication`, and `protocol` later picks a parser based on where the
+//!   handshake is. Those parsers are hand-rolled, byte-at-a-time, and run before
+//!   the client has authenticated, so `feed_and_drain` runs all three on every
+//!   payload and `gen_auth_body` generates the shapes they accept.
+//!
 //! Errors are expected. What we assert is the absence of panics and
 //! memory-safety violations.
+//!
+//! Note that allocation amplification is *not* in scope. The only speculative
+//! `reserve` is on the declared frame length, which `parse_frame_len` caps at
+//! `MAX_FRAME_SIZE` (64 MiB), far under the runner's `-rss_limit_mb`. The
+//! count-driven loops push one element per successful cursor read, so they are
+//! bounded by the bytes actually present. No oracle here can catch an
+//! over-allocation, so don't read one into the target.
 
 #![no_main]
 
 use bytes::BytesMut;
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
-use mz_pgwire::fuzz_exports::Codec;
+use mz_pgwire::fuzz_exports::{
+    Codec, Cursor, FrontendMessage, decode_password, decode_sasl_initial_response,
+    decode_sasl_response,
+};
 use tokio_util::codec::Decoder;
 
 /// Frontend message type tags the codec dispatches on.
@@ -118,8 +134,8 @@ fn gen_body(u: &mut Unstructured, tag: u8, out: &mut Vec) -> arbitrary::Resu
         b'X' | b'S' | b'H' | b'c' => {}
         // Simple query / copy-fail: a single cstring.
         b'Q' | b'f' => maybe_long_cstr(u, out)?,
-        // Password / generic auth: a cstring is a plausible password message.
-        b'p' => maybe_long_cstr(u, out)?,
+        // Auth: shapes for the sub-parsers `feed_and_drain` runs on the payload.
+        b'p' => gen_auth_body(u, out)?,
         // CopyData: arbitrary payload.
         b'd' => {
             for _ in 0..u.int_in_range(0usize..=16)? {
@@ -128,7 +144,11 @@ fn gen_body(u: &mut Unstructured, tag: u8, out: &mut Vec) -> arbitrary::Resu
         }
         // Describe / Close: a 'S'tatement|'P'ortal byte then a name cstring.
         b'D' | b'C' => {
-            out.push(if u.int_in_range(0u8..=1)? == 0 { b'S' } else { b'P' });
+            out.push(if u.int_in_range(0u8..=1)? == 0 {
+                b'S'
+            } else {
+                b'P'
+            });
             maybe_long_cstr(u, out)?;
         }
         // Execute: portal cstring + max-rows i32.
@@ -188,6 +208,101 @@ fn gen_body(u: &mut Unstructured, tag: u8, out: &mut Vec) -> arbitrary::Resu
     Ok(())
 }
 
+/// Append a comma-free printable run. Every SASL field is comma-delimited and
+/// goes through `String::from_utf8`, so restricting tokens to `0x2d..=0x7e`
+/// (printable ASCII above `,`) is what lets the parser advance past a field
+/// instead of stopping short or erroring on invalid UTF-8.
+fn push_token(u: &mut Unstructured, out: &mut Vec) -> arbitrary::Result<()> {
+    for _ in 0..u.int_in_range(0usize..=8)? {
+        out.push(u.int_in_range(0x2du8..=0x7e)?);
+    }
+    Ok(())
+}
+
+/// Build an auth-message payload targeting the three sub-parsers: a cleartext
+/// password, a SASL initial response, or a SASL client-final message.
+fn gen_auth_body(u: &mut Unstructured, out: &mut Vec) -> arbitrary::Result<()> {
+    match u.int_in_range(0u8..=2)? {
+        // `decode_password` reads a single cstring.
+        0 => maybe_long_cstr(u, out)?,
+        // `decode_sasl_initial_response`: mechanism cstring, a declared response
+        // length it only rejects when negative, then a client-first-message
+        // parsed out of whatever is left.
+        1 => {
+            match u.int_in_range(0u8..=2)? {
+                0 => out.extend_from_slice(b"SCRAM-SHA-256\0"),
+                1 => out.extend_from_slice(b"SCRAM-SHA-256-PLUS\0"),
+                _ => push_cstr(u, out)?,
+            }
+            // The parser rejects a negative declared length and then ignores the
+            // value entirely, parsing whatever follows regardless. Mostly
+            // declare a non-negative one so the client-first grammar is reached,
+            // occasionally go negative to cover the rejection.
+            let declared = if u.int_in_range(0u8..=7)? == 0 {
+                u.int_in_range(i32::MIN..=-1)?
+            } else {
+                u.int_in_range(0i32..=i32::MAX)?
+            };
+            be32(out, declared);
+            gen_sasl_client_first(u, out)?;
+        }
+        _ => gen_sasl_client_final(u, out)?,
+    }
+    Ok(())
+}
+
+/// A SCRAM `client-first-message` (RFC 5802): `gs2-cbind-flag "," [authzid] ","
+/// ["m=" mext ","] "n=" user "," "r=" nonce ["," ext]*`. Kept well-formed
+/// because the parser aborts on the first unexpected byte, so an approximation
+/// of the grammar would never reach the later fields.
+fn gen_sasl_client_first(u: &mut Unstructured, out: &mut Vec) -> arbitrary::Result<()> {
+    match u.int_in_range(0u8..=2)? {
+        0 => out.push(b'n'),
+        1 => out.push(b'y'),
+        // Channel binding required: "p=" carries the channel name.
+        _ => {
+            out.extend_from_slice(b"p=");
+            push_token(u, out)?;
+        }
+    }
+    out.push(b',');
+    if u.int_in_range(0u8..=1)? == 0 {
+        out.extend_from_slice(b"a=");
+        push_token(u, out)?;
+    }
+    out.push(b',');
+    if u.int_in_range(0u8..=3)? == 0 {
+        out.extend_from_slice(b"m=");
+        push_token(u, out)?;
+        out.push(b',');
+    }
+    out.extend_from_slice(b"n=");
+    push_token(u, out)?;
+    out.extend_from_slice(b",r=");
+    push_token(u, out)?;
+    for _ in 0..u.int_in_range(0usize..=2)? {
+        out.push(b',');
+        push_token(u, out)?;
+    }
+    Ok(())
+}
+
+/// A SCRAM `client-final-message` (RFC 5802): `"c=" cbind "," "r=" nonce
+/// ["," ext]* "," "p=" proof`. The proof is mandatory and last.
+fn gen_sasl_client_final(u: &mut Unstructured, out: &mut Vec) -> arbitrary::Result<()> {
+    out.extend_from_slice(b"c=");
+    push_token(u, out)?;
+    out.extend_from_slice(b",r=");
+    push_token(u, out)?;
+    for _ in 0..u.int_in_range(0usize..=2)? {
+        out.push(b',');
+        push_token(u, out)?;
+    }
+    out.extend_from_slice(b",p=");
+    push_token(u, out)?;
+    Ok(())
+}
+
 /// A cstring that is usually short but occasionally long, to stress the scan
 /// and downstream string allocations.
 fn maybe_long_cstr(u: &mut Unstructured, out: &mut Vec) -> arbitrary::Result<()> {
@@ -239,20 +354,57 @@ fn pump(u: &mut Unstructured, data: &[u8], chunked: bool) -> arbitrary::Result<(
 
     let mut feed_and_drain = |buf: &mut BytesMut| {
         // The codec is a streaming decoder, so pump it until it stops returning
-        // complete messages or errors out. Errors are expected. What we care
-        // about is the absence of panics and memory-safety violations.
+        // complete messages or runs out of forward progress. Errors are
+        // expected. What we care about is the absence of panics and
+        // memory-safety violations.
         loop {
+            let before = buf.len();
             match codec.decode(buf) {
-                Ok(Some(_msg)) => continue,
+                Ok(Some(msg)) => {
+                    if std::env::var_os("MZ_FUZZ_TRACE").is_some() {
+                        eprintln!("TRACE decoded {}", msg.name());
+                        if let FrontendMessage::RawAuthentication(d) = &msg {
+                            eprintln!(
+                                "TRACE   password={:?} sasl_init={:?} sasl_resp={:?}",
+                                decode_password(Cursor::new(d)).is_ok(),
+                                decode_sasl_initial_response(Cursor::new(d)).is_ok(),
+                                decode_sasl_response(Cursor::new(d)).is_ok(),
+                            );
+                        }
+                    }
+                    // `decode_auth` copies the payload verbatim without parsing
+                    // it. The real parsers run in `protocol`, which picks one by
+                    // handshake state: the two SASL parsers during SCRAM,
+                    // `decode_password` for cleartext. This target has no
+                    // connection state, so run all three on every payload. That
+                    // is a superset of what a single connection reaches, but each
+                    // is reachable pre-auth, so a panic in any of them is a real
+                    // pre-auth availability bug.
+                    if let FrontendMessage::RawAuthentication(data) = msg {
+                        let _ = decode_password(Cursor::new(&data));
+                        let _ = decode_sasl_initial_response(Cursor::new(&data));
+                        let _ = decode_sasl_response(Cursor::new(&data));
+                    }
+                    continue;
+                }
                 Ok(None) => break,
-                // NOTE: this breaks out of the drain but does not latch the
-                // error, so in chunked mode the outer loop keeps feeding the
-                // same codec afterwards. Production `FramedConn` instead tears
-                // the connection down on the first error, leaving `decode_state`
-                // mid-frame. Continuing is intentional and harmless here: frame
-                // body parsing is stateless per frame, so anything reachable
-                // after an error is also reachable from a fresh stream.
-                Err(_) => break,
+                Err(_) => {
+                    // Production `FramedConn` tears the connection down on the
+                    // first error, so nothing ever resumes mid-frame. Match that
+                    // by starting over rather than leaving `decode_state` stuck
+                    // in `Data(stale_tag, stale_len)`, which would shred every
+                    // later frame at the stale length and re-parse it under the
+                    // stale tag instead of its own.
+                    codec = Codec::new();
+                    // A body-parse error has already split the frame off, so the
+                    // buffer shrank and the next frame is aligned. A header
+                    // rejection (`parse_frame_len`, or the aggregate size cap)
+                    // consumes nothing, so continuing would spin on the same
+                    // bytes forever.
+                    if buf.len() == before {
+                        break;
+                    }
+                }
             }
         }
     };
@@ -276,7 +428,16 @@ fn pump(u: &mut Unstructured, data: &[u8], chunked: bool) -> arbitrary::Result<(
 
 fn run(mut u: Unstructured) -> arbitrary::Result<()> {
     // A quarter of the time, the raw bytes: keeps the header-framing and
-    // unknown-tag error paths covered.
+    // unknown-tag error paths covered, and is the only path on which a
+    // hand-written wire capture reaches the decoder as written.
+    //
+    // NOTE: `int_in_range` consumes from the *front*, one byte per decision, so
+    // this prefix is a wire format the corpus depends on: `data[0] % 4 == 0`
+    // selects this branch, `data[1] % 2 == 0` selects chunked, and `data[2..]` is
+    // what the decoder sees byte for byte. `prepare-corpus.sh` prepends
+    // `\x00\x00` to every seed to land here. Reordering these two decisions, or
+    // adding a third ahead of them, silently repurposes every seed's leading
+    // bytes and strands the corpus.
     if u.int_in_range(0u8..=3)? == 0 {
         let chunked = u.int_in_range(0u8..=1)? == 0;
         let rest = u.take_rest();
diff --git a/src/pgwire/fuzz/prepare-corpus.sh b/src/pgwire/fuzz/prepare-corpus.sh
index 3f7ad984df7ae..642938400b042 100755
--- a/src/pgwire/fuzz/prepare-corpus.sh
+++ b/src/pgwire/fuzz/prepare-corpus.sh
@@ -18,6 +18,14 @@
 # frontend messages. The startup handshake (StartupMessage, SSLRequest,
 # CancelRequest) is parsed by a separate `decode_startup` path that this
 # target does not exercise, so no startup-phase seeds are included.
+#
+# Every seed carries the two-byte control prefix the target reads off the front
+# of the input before anything else (see `run` in codec_decode.rs): the first
+# byte selects the raw-bytes branch, the second selects chunked feeding, and only
+# the bytes after them reach the decoder. Without the prefix a seed's own type
+# tag is eaten as a control byte, the decoder reads the length field's second
+# byte as the message tag, and the frame parks in the await-more-bytes state
+# without ever reaching the body parser the seed is named after.
 
 set -euo pipefail
 
@@ -31,14 +39,26 @@ python3 - "$corpus" <<'PY'
 import os, struct, sys
 corpus = sys.argv[1]
 
+# Steers `run` onto the raw-bytes branch, feeding the rest of the seed to the
+# decoder verbatim: `data[0] % 4 == 0` picks raw, `data[1] % 2 == 0` picks
+# chunked.
+CONTROL_PREFIX = b"\x00\x00"
+
 def frame(tag: bytes, payload: bytes) -> bytes:
     # Standard pgwire frame: 1-byte type tag + 4-byte BE length (incl
     # itself) + payload.
-    return tag + struct.pack(">I", 4 + len(payload)) + payload
+    return CONTROL_PREFIX + tag + struct.pack(">I", 4 + len(payload)) + payload
 
 def cstr(s: str) -> bytes:
     return s.encode() + b"\x00"
 
+def sasl_initial(mechanism: str, client_first: str) -> bytes:
+    # `decode_sasl_initial_response` reads the mechanism cstring, then a declared
+    # response length it only rejects when negative, then parses the rest as a
+    # client-first-message.
+    resp = client_first.encode()
+    return cstr(mechanism) + struct.pack(">I", len(resp)) + resp
+
 seeds = {
     "01_query_select_1": frame(b"Q", cstr("SELECT 1")),
     "02_query_empty": frame(b"Q", cstr("")),
@@ -55,7 +75,15 @@ seeds = {
     "13_copy_done": frame(b"c", b""),
     "14_copy_fail": frame(b"f", cstr("client gave up")),
     "15_password": frame(b"p", cstr("hunter2")),
-    "16_sasl_initial": frame(b"p", cstr("SCRAM-SHA-256") + struct.pack(">I", 5) + b"hello"),
+    # The auth seeds carry payloads that parse, so the sub-parsers the target
+    # runs on `RawAuthentication` get real starting coverage. A SCRAM message
+    # is comma-delimited and the parsers abort on the first unexpected byte, so
+    # an approximate payload would never reach the later fields.
+    "16_sasl_initial": frame(b"p", sasl_initial("SCRAM-SHA-256", "n,,n=user,r=rOprNGfwEbeRWgbNEkqO")),
+    "17_sasl_initial_cbind": frame(
+        b"p", sasl_initial("SCRAM-SHA-256-PLUS", "p=tls-server-end-point,a=admin,n=user,r=rOprNGfwEbeRWgbNEkqO")
+    ),
+    "18_sasl_response": frame(b"p", b"c=biws,r=rOprNGfwEbeRWgbNEkqO,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ="),
 }
 
 for name, blob in seeds.items():
diff --git a/src/pgwire/src/lib.rs b/src/pgwire/src/lib.rs
index 15085faa26e10..c4755b1bbabc4 100644
--- a/src/pgwire/src/lib.rs
+++ b/src/pgwire/src/lib.rs
@@ -37,7 +37,17 @@ pub use server::{Config, Server};
 /// Internal types re-exported under `cfg(feature = "fuzzing")` so the fuzz
 /// crate can drive the frontend-message decoder directly. Not for
 /// production use.
+///
+/// The auth sub-parsers are included because `Codec::decode` does not call
+/// them: its `b'p'` arm copies the payload verbatim into
+/// `FrontendMessage::RawAuthentication`, and `protocol` picks the parser from
+/// the handshake state. Without these, the pre-auth SASL/password grammars are
+/// unreachable from the decoder alone.
 #[cfg(feature = "fuzzing")]
 pub mod fuzz_exports {
-    pub use crate::codec::Codec;
+    pub use mz_pgwire_common::{Cursor, FrontendMessage};
+
+    pub use crate::codec::{
+        Codec, decode_password, decode_sasl_initial_response, decode_sasl_response,
+    };
 }
diff --git a/src/postgres-util/fuzz/fuzz_targets/postgres_table_desc_proto_roundtrip.rs b/src/postgres-util/fuzz/fuzz_targets/postgres_table_desc_proto_roundtrip.rs
index 9021238101157..bbd77df350360 100644
--- a/src/postgres-util/fuzz/fuzz_targets/postgres_table_desc_proto_roundtrip.rs
+++ b/src/postgres-util/fuzz/fuzz_targets/postgres_table_desc_proto_roundtrip.rs
@@ -12,28 +12,42 @@
 //! here is reachable from a compromised upstream Postgres or on-disk catalog
 //! bytes.
 //!
-//! The first input byte selects the arm. The rest feeds it:
+//! The first input byte selects one of four arms. The rest feeds it:
 //!
 //!  * **Structured arm.** Drives `PostgresTableDesc`'s proptest `Arbitrary`
 //!    (behind mz-postgres-util's `schemas` feature) from the libFuzzer byte
 //!    stream to synthesize a *valid, fully-populated* value, then asserts the
-//!    `value -> proto -> value` chain is the identity AND that re-encoding the
-//!    proto is byte-idempotent. This is what actually reaches the deep shape:
-//!    several `PostgresColumnDesc`s with arbitrary `col_num`/`type_oid`/
-//!    `type_mod`/`nullable`, and a `BTreeSet` of several `PostgresKeyDesc`s,
-//!    each with a `Vec` of `cols`. Random proto bytes decode to a
-//!    near-empty desc, so the populated branches never get covered otherwise.
+//!    `value -> proto -> value` chain is the identity. This is what actually
+//!    reaches the deep shape: several `PostgresColumnDesc`s with arbitrary
+//!    `col_num`/`type_oid`/`type_mod`/`nullable`, and a `BTreeSet` of several
+//!    `PostgresKeyDesc`s, each with a `Vec` of `cols`. Random proto bytes
+//!    decode to a near-empty desc, so the populated branches never get covered
+//!    otherwise.
+//!  * **Narrowing arm.** Hand-builds a proto whose `ProtoPostgresColumnDesc::
+//!    col_num` and `ProtoPostgresKeyDesc::cols` sit in a dense neighborhood of
+//!    the `u16::MAX` boundary, and asserts the `u32 -> u16` narrowing in
+//!    `from_proto` rejects *exactly* the out-of-range values via `Err` (never a
+//!    panic, never a silent truncation) and preserves the in-range ones bit for
+//!    bit. `col_num` is a column's positional identity, matched by equality when
+//!    purification resolves key columns and when a source checks an upstream
+//!    schema for compatibility, so a truncating narrowing would silently remap
+//!    or drop key columns instead of rejecting the input.
+//!  * **Duplicate-keys arm.** Builds a proto whose repeated `keys` field carries
+//!    duplicate and unsorted entries, which must collapse into the Rust
+//!    `BTreeSet` without tripping an ordering assertion and without letting the
+//!    wire order leak into the decoded value. The key bodies come from the input
+//!    so the mutator can vary `cols` (including empty) and the flags while the
+//!    duplicate/unsorted shape stays fixed.
 //!  * **Raw-bytes arm.** Decodes arbitrary bytes straight into the proto then
 //!    `into_rust`, exercising the decoder against malformed/adversarial input,
-//!    then re-encodes the recovered value. This is where the untrusted-bytes
-//!    invariants live: the `u32 -> u16` narrowing for
-//!    `ProtoPostgresColumnDesc::col_num` and `ProtoPostgresKeyDesc::cols` must
-//!    return `Err`, not panic, and the wire's repeated `keys` field with
-//!    duplicate / unsorted entries must collapse cleanly into the Rust
-//!    `BTreeSet` rather than trip an ordering assertion.
+//!    then re-encodes the recovered value. Random bytes essentially never reach
+//!    the boundary or duplicate-key shapes, which is why those get their own
+//!    arms.
 
 #![no_main]
 
+use std::collections::BTreeSet;
+
 use libfuzzer_sys::fuzz_target;
 use mz_postgres_util::desc::{
     PostgresTableDesc, ProtoPostgresColumnDesc, ProtoPostgresKeyDesc, ProtoPostgresTableDesc,
@@ -52,8 +66,12 @@ fn seed_from(bytes: &[u8]) -> [u8; 32] {
     seed
 }
 
-/// Assert the `value -> proto -> value` chain is the identity and that
-/// re-encoding the proto is byte-idempotent.
+/// Read `bytes[i]`, treating a short input as zero-padded.
+fn byte(bytes: &[u8], i: usize) -> u8 {
+    bytes.get(i).copied().unwrap_or(0)
+}
+
+/// Assert the `value -> proto -> value` chain is the identity.
 fn assert_roundtrip(orig: PostgresTableDesc) {
     let proto = >::from_rust(&orig);
     let bytes = proto.encode_to_vec();
@@ -62,11 +80,16 @@ fn assert_roundtrip(orig: PostgresTableDesc) {
     let round: PostgresTableDesc = proto2
         .into_rust()
         .expect("re-encoded PostgresTableDesc must convert back to Rust");
-    assert_eq!(orig, round, "PostgresTableDesc changed across proto roundtrip");
+    assert_eq!(
+        orig, round,
+        "PostgresTableDesc changed across proto roundtrip"
+    );
 
-    // Encoding the recovered value must reproduce the same wire bytes.
-    let bytes2 = >::from_rust(&round)
-        .encode_to_vec();
+    // Implied by the equality above for as long as every field `into_proto`
+    // reads takes part in `PartialEq`. Kept as a guard for the day one doesn't,
+    // not as coverage of a separate invariant.
+    let bytes2 =
+        >::from_rust(&round).encode_to_vec();
     assert_eq!(bytes, bytes2, "proto re-encode was not idempotent");
 }
 
@@ -101,21 +124,25 @@ fuzz_target!(|data: &[u8]| {
         }
         // Targeted arm: hand-build a proto whose `col_num` / `cols` values
         // straddle the u16 boundary, confirming the u32 -> u16 narrowing in
-        // `from_proto` returns `Err` (not a panic) for the out-of-range cases
-        // and succeeds for the in-range ones.
+        // `from_proto` returns `Err` (not a panic, not a truncated value) for
+        // the out-of-range cases and preserves the in-range ones.
         1 => {
-            // Use the first few bytes as little-endian u32 candidates so the
-            // fuzzer can search both sides of the 65535 boundary.
-            let take_u32 = |i: usize| -> u32 {
-                let mut buf = [0u8; 4];
-                let n = rest.len().saturating_sub(i * 4).min(4);
-                if n > 0 {
-                    buf[..n].copy_from_slice(&rest[i * 4..i * 4 + n]);
+            // Draw each candidate from a dense neighborhood of the boundary. A
+            // plain little-endian u32 over 4 input bytes would put the in-range
+            // side at 2^-16 of executions, and the fuzzer gets no coverage
+            // gradient toward it, because the success path is already covered by
+            // the other arms.
+            let candidate = |lo: usize, sel: usize| -> u32 {
+                let base = u32::from(u16::from_le_bytes([byte(rest, lo), byte(rest, lo + 1)]));
+                match byte(rest, sel) % 4 {
+                    0 => base,
+                    1 => u32::from(u16::MAX),
+                    2 => u32::from(u16::MAX) + 1 + base,
+                    _ => u32::MAX - base,
                 }
-                u32::from_le_bytes(buf)
             };
-            let col_num = take_u32(0);
-            let key_col = take_u32(1);
+            let col_num = candidate(0, 2);
+            let key_col = candidate(3, 5);
 
             let proto = ProtoPostgresTableDesc {
                 name: "t".into(),
@@ -139,43 +166,91 @@ fuzz_target!(|data: &[u8]| {
             let bytes = proto.encode_to_vec();
             let decoded = ProtoPostgresTableDesc::decode(bytes.as_slice())
                 .expect("hand-built proto must decode");
-            // Whether the narrowing fits, the only requirement is no panic. If
-            // it converts, the value must round-trip.
             let converted: Result = decoded.into_rust();
-            if let Ok(orig) = converted {
-                assert_roundtrip(orig);
-            }
+            let fits = col_num <= u32::from(u16::MAX) && key_col <= u32::from(u16::MAX);
+            assert_eq!(
+                converted.is_ok(),
+                fits,
+                "u32 -> u16 narrowing must reject exactly the out-of-range values \
+                 (col_num={col_num}, key_col={key_col})"
+            );
+            let Ok(orig) = converted else {
+                return;
+            };
+            // In-range values must survive intact, neither truncated nor clamped.
+            // The arm builds exactly one column and one key, so this indexing
+            // holds.
+            assert_eq!(u32::from(orig.columns[0].col_num), col_num);
+            assert_eq!(
+                orig.keys.iter().next().expect("one key").cols,
+                vec![u16::try_from(key_col).expect("in range")],
+            );
+            assert_roundtrip(orig);
         }
         // Targeted arm: a wire proto with duplicate and unsorted `keys`. The
         // repeated field maps to a Rust `BTreeSet`, which dedups + sorts. This
         // must collapse cleanly with no ordering/dup assertion firing.
         2 => {
-            let mk = |oid: u32, col: u16| ProtoPostgresKeyDesc {
+            // Seed the key bodies from the input so the mutator can vary `cols`
+            // (length 0 to 3, so the empty-key case is reachable), the columns
+            // they reference, and the flags, while the duplicate/unsorted shape
+            // stays fixed.
+            let cols_at = |base: usize| -> Vec {
+                (0..usize::from(byte(rest, base) % 4))
+                    .map(|i| u32::from(byte(rest, base + 1 + i)))
+                    .collect()
+            };
+            let mk = |oid: u32, cols: Vec| ProtoPostgresKeyDesc {
                 oid,
-                name: "k".into(),
-                cols: vec![u32::from(col)],
-                is_primary: false,
-                nulls_not_distinct: false,
+                name: format!("k{oid}"),
+                cols,
+                is_primary: oid & 1 == 0,
+                nulls_not_distinct: oid & 2 == 0,
             };
-            // Intentionally out of order with a duplicate entry.
+            let key_a = mk(u32::from(byte(rest, 0)), cols_at(1));
+            let key_b = mk(u32::from(byte(rest, 5)), cols_at(6));
+
+            // Give every column a key references a matching `col_num`. Nothing in
+            // this arm's purpose needs the desc to be internally inconsistent,
+            // and the `.expect` below would become a false crash the day
+            // `from_proto` starts validating what purification already checks
+            // downstream, that a key's columns exist.
+            let col_nums: BTreeSet = key_a.cols.iter().chain(&key_b.cols).copied().collect();
+            let columns: Vec<_> = col_nums
+                .iter()
+                .map(|&col_num| ProtoPostgresColumnDesc {
+                    name: format!("c{col_num}"),
+                    type_oid: 23,
+                    type_mod: -1,
+                    nullable: false,
+                    col_num: Some(col_num),
+                })
+                .collect();
+
+            // Intentionally out of order with duplicate entries.
             let proto = ProtoPostgresTableDesc {
                 name: "t".into(),
                 namespace: "n".into(),
                 oid: 7,
-                columns: vec![ProtoPostgresColumnDesc {
-                    name: "c".into(),
-                    type_oid: 23,
-                    type_mod: -1,
-                    nullable: false,
-                    col_num: Some(1),
-                }],
-                keys: vec![mk(3, 2), mk(1, 9), mk(3, 2), mk(2, 0)],
+                columns,
+                keys: vec![key_b.clone(), key_a.clone(), key_a, key_b],
             };
             let bytes = proto.encode_to_vec();
             let decoded = ProtoPostgresTableDesc::decode(bytes.as_slice())
                 .expect("hand-built proto must decode");
-            let orig: PostgresTableDesc =
-                decoded.into_rust().expect("duplicate/unsorted keys must convert");
+            let orig: PostgresTableDesc = decoded
+                .clone()
+                .into_rust()
+                .expect("duplicate/unsorted keys must convert");
+
+            // Reversing the wire order must not change what we decode.
+            let mut reversed = decoded;
+            reversed.keys.reverse();
+            let flipped: PostgresTableDesc = reversed
+                .into_rust()
+                .expect("duplicate/unsorted keys must convert");
+            assert_eq!(orig, flipped, "decoded desc depends on wire key order");
+
             assert_roundtrip(orig);
         }
         // Raw-bytes arm: decode adversarial proto bytes, then round-trip.
diff --git a/src/repr/fuzz/Cargo.toml b/src/repr/fuzz/Cargo.toml
index 777a867a1a05f..a8ea570f69ab3 100644
--- a/src/repr/fuzz/Cargo.toml
+++ b/src/repr/fuzz/Cargo.toml
@@ -62,13 +62,6 @@ test = false
 doc = false
 bench = false
 
-[[bin]]
-name = "interval_proto_roundtrip"
-path = "fuzz_targets/interval_proto_roundtrip.rs"
-test = false
-doc = false
-bench = false
-
 [[bin]]
 name = "mz_acl_item_proto_roundtrip"
 path = "fuzz_targets/mz_acl_item_proto_roundtrip.rs"
diff --git a/src/repr/fuzz/fuzz_targets/acl_item_proto_roundtrip.rs b/src/repr/fuzz/fuzz_targets/acl_item_proto_roundtrip.rs
index 1629f7016313b..8fa52b2eaa7f5 100644
--- a/src/repr/fuzz/fuzz_targets/acl_item_proto_roundtrip.rs
+++ b/src/repr/fuzz/fuzz_targets/acl_item_proto_roundtrip.rs
@@ -34,11 +34,11 @@ fn arbitrary_arm(seed: &[u8]) {
     }
     let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &buf);
     let mut runner = TestRunner::new_with_rng(Config::default(), rng);
-    let value =
-        match ::arbitrary().new_tree(&mut runner) {
-            Ok(tree) => tree.current(),
-            Err(_) => return,
-        };
+    let value = match ::arbitrary().new_tree(&mut runner)
+    {
+        Ok(tree) => tree.current(),
+        Err(_) => return,
+    };
 
     let proto = value.into_proto();
     let back = AclItem::from_proto(proto).expect("valid AclItem must round-trip");
@@ -56,8 +56,8 @@ fn raw_arm(data: &[u8]) {
 
     let proto2 = >::from_rust(&orig);
     let bytes2 = proto2.encode_to_vec();
-    let proto3 = ProtoAclItem::decode(bytes2.as_slice())
-        .expect("re-encode of valid AclItem must decode");
+    let proto3 =
+        ProtoAclItem::decode(bytes2.as_slice()).expect("re-encode of valid AclItem must decode");
     let round: AclItem = proto3
         .into_rust()
         .expect("re-encoded AclItem must convert back to Rust");
diff --git a/src/repr/fuzz/fuzz_targets/column_type_proto_roundtrip.rs b/src/repr/fuzz/fuzz_targets/column_type_proto_roundtrip.rs
index 82781158f9fe3..cf5a8dd38fdbf 100644
--- a/src/repr/fuzz/fuzz_targets/column_type_proto_roundtrip.rs
+++ b/src/repr/fuzz/fuzz_targets/column_type_proto_roundtrip.rs
@@ -11,28 +11,113 @@
 //!
 //! Two arms (the first byte selects):
 //!  - Arbitrary arm: drive `SqlColumnType`'s proptest `Arbitrary` strategy from
-//!    the fuzzer bytes to build a *valid* column type pairing a deeply-nested
-//!    `SqlScalarType` with a nullable flag, and assert
-//!    `from_proto(into_proto(v)) == v`. Random proto bytes leave the inner
-//!    scalar type near-empty. This arm reaches the recursive scalar variants.
+//!    the fuzzer bytes to build a *valid* column type pairing a nested
+//!    `SqlScalarType` with a nullable flag, and assert it survives an
+//!    encode/decode through the wire codec. Valid values are where a proto3
+//!    presence bug shows up: a default-valued field that silently stops being
+//!    written to the wire still passes an in-memory `RustType` round-trip.
 //!  - Raw-bytes arm: decode arbitrary bytes as `ProtoColumnType`, into Rust, and
 //!    re-encode, keeping coverage of the bare wire decoder against hostile
-//!    input.
+//!    input. `prost` is built with `no-recursion-limit`, so this is also the arm
+//!    that reaches deep nesting in the inner scalar type.
+//!
+//! Both arms also run a domain oracle over the decoded type parameters. The
+//! round-trip oracle on its own cannot see decoder laxity here: `into_proto`
+//! writes every type parameter back verbatim, so byte preservation holds for
+//! out-of-domain values too and `assert_eq!(orig, round)` degenerates into "did
+//! not panic".
 
 #![no_main]
 
 use libfuzzer_sys::fuzz_target;
-use mz_proto::{ProtoType, RustType};
-use mz_repr::{ProtoColumnType, SqlColumnType};
+use mz_proto::{ProtoType, protobuf_roundtrip};
+use mz_repr::adt::char::CharLength;
+use mz_repr::adt::numeric::NumericMaxScale;
+use mz_repr::adt::timestamp::TimestampPrecision;
+use mz_repr::adt::varchar::VarCharMaxLength;
+use mz_repr::{ProtoColumnType, SqlColumnType, SqlScalarType};
 use proptest::strategy::{Strategy, ValueTree};
 use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
 use prost::Message;
 
+/// Asserts every type parameter reachable from `t` is one the planner could have
+/// produced.
+///
+/// Each newtype enforces its domain in `TryFrom`, and consumers assume that
+/// invariant holds however the value was built: a `Timestamp` precision above
+/// `MAX_PRECISION` panics `CheckedTimestamp::round_to_precision`, which the SQL
+/// Server source decoder reaches with the precision carried by a decoded
+/// `SqlColumnType`.
+fn assert_type_params_in_domain(t: &SqlScalarType) {
+    match t {
+        SqlScalarType::Timestamp { precision } | SqlScalarType::TimestampTz { precision } => {
+            if let Some(p) = precision {
+                assert!(
+                    TimestampPrecision::try_from(i64::from(p.into_u8())).is_ok(),
+                    "out-of-domain timestamp precision {}",
+                    p.into_u8()
+                );
+            }
+        }
+        SqlScalarType::Numeric { max_scale } => {
+            if let Some(s) = max_scale {
+                assert!(
+                    NumericMaxScale::try_from(i64::from(s.into_u8())).is_ok(),
+                    "out-of-domain numeric max scale {}",
+                    s.into_u8()
+                );
+            }
+        }
+        SqlScalarType::Char { length } => {
+            if let Some(l) = length {
+                assert!(
+                    CharLength::try_from(i64::from(l.into_u32())).is_ok(),
+                    "out-of-domain char length {}",
+                    l.into_u32()
+                );
+            }
+        }
+        SqlScalarType::VarChar { max_length } => {
+            if let Some(l) = max_length {
+                assert!(
+                    VarCharMaxLength::try_from(i64::from(l.into_u32())).is_ok(),
+                    "out-of-domain varchar max length {}",
+                    l.into_u32()
+                );
+            }
+        }
+        SqlScalarType::Array(inner)
+        | SqlScalarType::Range {
+            element_type: inner,
+        } => assert_type_params_in_domain(inner),
+        SqlScalarType::List {
+            element_type: inner,
+            ..
+        }
+        | SqlScalarType::Map {
+            value_type: inner, ..
+        } => assert_type_params_in_domain(inner),
+        SqlScalarType::Record { fields, .. } => {
+            for (_, ct) in fields.iter() {
+                assert_type_params_in_domain(&ct.scalar_type);
+            }
+        }
+        _ => {}
+    }
+}
+
 fn arbitrary_arm(seed: &[u8]) {
     let mut buf = [0u8; 32];
     for (dst, src) in buf.iter_mut().zip(seed.iter()) {
         *dst = *src;
     }
+    // NOTE: hashing the fuzzer bytes into a ChaCha seed costs libFuzzer its
+    // mutation gradient, one flipped bit re-rolls the whole value. The obvious
+    // fix, `RngAlgorithm::PassThrough`, hangs: it feeds the fuzzer bytes in as
+    // the random stream and then yields zeros forever once they run out, and
+    // `rand`'s Lemire sampler loops until a draw clears `thresh`, which a zero
+    // never does for a range that is not a power of two. Every strategy here
+    // outdraws a 4096-byte input.
     let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &buf);
     let mut runner = TestRunner::new_with_rng(Config::default(), rng);
     let value = match ::arbitrary()
@@ -42,8 +127,12 @@ fn arbitrary_arm(seed: &[u8]) {
         Err(_) => return,
     };
 
-    let proto = value.into_proto();
-    let back = SqlColumnType::from_proto(proto).expect("valid SqlColumnType must round-trip");
+    // The generator is meant to model what the planner can produce, so a failure
+    // here is a generator defect, not a decoder defect.
+    assert_type_params_in_domain(&value.scalar_type);
+
+    let back = protobuf_roundtrip::<_, ProtoColumnType>(&value)
+        .expect("valid SqlColumnType must round-trip");
     assert_eq!(value, back, "SqlColumnType changed across proto roundtrip");
 }
 
@@ -55,6 +144,7 @@ fn raw_arm(data: &[u8]) {
         Ok(v) => v,
         Err(_) => return,
     };
+    assert_type_params_in_domain(&orig.scalar_type);
 
     let proto2 = >::from_rust(&orig);
     let bytes2 = proto2.encode_to_vec();
diff --git a/src/repr/fuzz/fuzz_targets/interval_proto_roundtrip.rs b/src/repr/fuzz/fuzz_targets/interval_proto_roundtrip.rs
deleted file mode 100644
index 8e01d256cac7f..0000000000000
--- a/src/repr/fuzz/fuzz_targets/interval_proto_roundtrip.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright Materialize, Inc. and contributors. All rights reserved.
-//
-// Use of this software is governed by the Business Source License
-// included in the LICENSE file.
-//
-// As of the Change Date specified in that file, in accordance with
-// the Business Source License, use of this software will be governed
-// by the Apache License, Version 2.0.
-
-//! Fuzz target: `Interval` proto round-trips losslessly.
-//!
-//! Two arms (the first byte selects):
-//!  - Arbitrary arm: drive `Interval`'s proptest `Arbitrary` strategy from the
-//!    fuzzer bytes to build a *valid* interval (months/days/micros across the
-//!    full range) and assert `from_proto(into_proto(v)) == v`.
-//!  - Raw-bytes arm: decode arbitrary bytes as `ProtoInterval`, into Rust, and
-//!    re-encode, keeping coverage of the bare wire decoder against hostile
-//!    input.
-
-#![no_main]
-
-use libfuzzer_sys::fuzz_target;
-use mz_proto::{ProtoType, RustType};
-use mz_repr::adt::interval::{Interval, ProtoInterval};
-use proptest::strategy::{Strategy, ValueTree};
-use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
-use prost::Message;
-
-fn arbitrary_arm(seed: &[u8]) {
-    let mut buf = [0u8; 32];
-    for (dst, src) in buf.iter_mut().zip(seed.iter()) {
-        *dst = *src;
-    }
-    let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &buf);
-    let mut runner = TestRunner::new_with_rng(Config::default(), rng);
-    let value =
-        match ::arbitrary().new_tree(&mut runner) {
-            Ok(tree) => tree.current(),
-            Err(_) => return,
-        };
-
-    let proto = value.into_proto();
-    let back = Interval::from_proto(proto).expect("valid Interval must round-trip");
-    assert_eq!(value, back, "Interval changed across proto roundtrip");
-}
-
-fn raw_arm(data: &[u8]) {
-    let Ok(proto) = ProtoInterval::decode(data) else {
-        return;
-    };
-    let orig: Interval = match proto.into_rust() {
-        Ok(v) => v,
-        Err(_) => return,
-    };
-
-    let proto2 = >::from_rust(&orig);
-    let bytes2 = proto2.encode_to_vec();
-    let proto3 = ProtoInterval::decode(bytes2.as_slice())
-        .expect("re-encode of valid Interval must decode");
-    let round: Interval = proto3
-        .into_rust()
-        .expect("re-encoded Interval must convert back to Rust");
-
-    assert_eq!(orig, round, "Interval changed across proto roundtrip");
-}
-
-fuzz_target!(|data: &[u8]| {
-    let Some((&mode, rest)) = data.split_first() else {
-        return;
-    };
-    if mode & 1 == 0 {
-        arbitrary_arm(rest);
-    } else {
-        raw_arm(rest);
-    }
-});
diff --git a/src/repr/fuzz/fuzz_targets/numeric_arith.rs b/src/repr/fuzz/fuzz_targets/numeric_arith.rs
index 47f2bb87c5f5f..7f600622db442 100644
--- a/src/repr/fuzz/fuzz_targets/numeric_arith.rs
+++ b/src/repr/fuzz/fuzz_targets/numeric_arith.rs
@@ -33,7 +33,7 @@
 
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
-use mz_repr::adt::numeric::{cx_datum, Numeric};
+use mz_repr::adt::numeric::{Numeric, cx_datum};
 use mz_repr::strconv::parse_numeric;
 
 /// Generate one operand from the fuzzer's byte stream.
diff --git a/src/repr/fuzz/fuzz_targets/range_ops.rs b/src/repr/fuzz/fuzz_targets/range_ops.rs
index a280ca311dbd9..dcc7237277eb3 100644
--- a/src/repr/fuzz/fuzz_targets/range_ops.rs
+++ b/src/repr/fuzz/fuzz_targets/range_ops.rs
@@ -27,8 +27,8 @@
 
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
-use mz_repr::adt::range::{Range, RangeBound};
 use mz_repr::Datum;
+use mz_repr::adt::range::{Range, RangeBound};
 
 // None = infinite bound. Some((inclusive, value)) = finite bound.
 type BoundSpec = Option<(bool, i32)>;
@@ -78,11 +78,7 @@ fn check(a: Range>, b: Range>, elem: i32) {
             "union must contain both operands"
         );
     }
-    assert_eq!(
-        a.union(&b),
-        b.union(&a),
-        "union must be commutative"
-    );
+    assert_eq!(a.union(&b), b.union(&a), "union must be commutative");
     if let Ok(uaa) = a.union(&a) {
         assert_eq!(uaa, a, "union must be idempotent (a ∪ a == a)");
     }
@@ -107,10 +103,7 @@ fn check(a: Range>, b: Range>, elem: i32) {
     // --- Difference algebra: a ∖ b ⊆ a and disjoint from b. ---------------
     if let Ok(diff) = a.difference(&b) {
         let diff = to_static(diff);
-        assert!(
-            a.contains_range(&diff),
-            "a ∖ b must be a subset of a"
-        );
+        assert!(a.contains_range(&diff), "a ∖ b must be a subset of a");
         assert!(
             diff.intersection(&b).inner.is_none(),
             "a ∖ b must be disjoint from b"
diff --git a/src/repr/fuzz/fuzz_targets/relation_desc_proto_roundtrip.rs b/src/repr/fuzz/fuzz_targets/relation_desc_proto_roundtrip.rs
index c967acdbfa5e6..ab3067180d40e 100644
--- a/src/repr/fuzz/fuzz_targets/relation_desc_proto_roundtrip.rs
+++ b/src/repr/fuzz/fuzz_targets/relation_desc_proto_roundtrip.rs
@@ -11,46 +11,160 @@
 //! `RelationDesc` is the schema of every persisted collection, so a decoder
 //! bug here corrupts catalog/persist state.
 //!
-//! Two arms (the first byte selects):
+//! Three arms (the first byte selects):
 //!  - Arbitrary arm: drive `RelationDesc`'s proptest `Arbitrary` strategy from
-//!    the fuzzer bytes to build a *valid* desc: column names paired with
-//!    deeply-nested `SqlColumnType`s, where the names<->metadata length
-//!    invariant and the per-version migration metadata are well-formed. Assert
-//!    `from_proto(into_proto(v)) == v`. Random proto bytes never satisfy the
-//!    length invariant, so the encoder's rollup/migration-default paths are
-//!    only reached here.
+//!    the fuzzer bytes, then apply a generated batch of schema migrations to
+//!    it. The strategy alone only ever builds descs through `RelationDesc::new`,
+//!    whose metadata is all-default, so a dropped column, and with it the
+//!    encoder's non-default metadata path, enters the value space only via the
+//!    migrations. Assert the desc survives an encode/decode through the wire
+//!    codec. Valid values are where a proto3 presence bug shows up: a
+//!    default-valued field that silently stops being written to the wire still
+//!    passes an in-memory `RustType` round-trip.
+//!  - Versioned arm: build a desc through `VersionedRelationDesc`'s
+//!    `add_column`/`drop_column` and read it back `at_version`, which is the
+//!    only producer of *sparse* `ColumnIndex` keys. The proto has no field for
+//!    those keys, so the round trip renumbers them densely. Assert what does
+//!    survive, and pin the renumbering itself, since
+//!    `ColumnIndex::to_stable_name` is the arrow field name persist data is
+//!    written under.
 //!  - Raw-bytes arm: decode arbitrary bytes as `ProtoRelationDesc`, into Rust,
 //!    and re-encode, keeping coverage of the bare wire decoder against hostile
-//!    input (including descs that violate the length invariant on decode).
+//!    input. Equality across the re-encode is weak here, `from_proto`
+//!    canonicalizes what it decodes so it holds for anything that decodes at
+//!    all, so the arm also walks the decoded desc: the decoder must not admit
+//!    a shape that panics on first use.
 
 #![no_main]
 
 use libfuzzer_sys::fuzz_target;
-use mz_proto::{ProtoType, RustType};
-use mz_repr::{ProtoRelationDesc, RelationDesc};
+use mz_proto::{ProtoType, protobuf_roundtrip};
+use mz_repr::{
+    ColumnName, ProtoRelationDesc, RelationDesc, RelationVersion, RelationVersionSelector,
+    SqlColumnType, VersionedRelationDesc, arb_relation_desc_diff,
+};
+use proptest::prelude::any;
 use proptest::strategy::{Strategy, ValueTree};
 use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
 use prost::Message;
 
-fn arbitrary_arm(seed: &[u8]) {
+fn runner(seed: &[u8]) -> TestRunner {
     let mut buf = [0u8; 32];
     for (dst, src) in buf.iter_mut().zip(seed.iter()) {
         *dst = *src;
     }
+    // NOTE: hashing the fuzzer bytes into a ChaCha seed costs libFuzzer its
+    // mutation gradient, one flipped bit re-rolls the whole value. The obvious
+    // fix, `RngAlgorithm::PassThrough`, hangs: it feeds the fuzzer bytes in as
+    // the random stream and then yields zeros forever once they run out, and
+    // `rand`'s Lemire sampler loops until a draw clears `thresh`, which a zero
+    // never does for a range that is not a power of two. Every strategy here
+    // outdraws a 4096-byte input.
     let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &buf);
-    let mut runner = TestRunner::new_with_rng(Config::default(), rng);
-    let value = match ::arbitrary()
-        .new_tree(&mut runner)
-    {
-        Ok(tree) => tree.current(),
-        Err(_) => return,
+    TestRunner::new_with_rng(Config::default(), rng)
+}
+
+fn arbitrary_arm(seed: &[u8]) {
+    let mut runner = runner(seed);
+    let strat = any::().prop_flat_map(|desc| {
+        arb_relation_desc_diff(&desc).prop_map(move |diffs| (desc.clone(), diffs))
+    });
+    let Ok(tree) = strat.new_tree(&mut runner) else {
+        return;
     };
+    let (mut value, diffs) = tree.current();
+    for diff in diffs {
+        diff.apply(&mut value);
+    }
 
-    let proto = value.into_proto();
-    let back = RelationDesc::from_proto(proto).expect("valid RelationDesc must round-trip");
+    let back = protobuf_roundtrip::<_, ProtoRelationDesc>(&value)
+        .expect("valid RelationDesc must round-trip");
     assert_eq!(value, back, "RelationDesc changed across proto roundtrip");
 }
 
+fn versioned_arm(seed: &[u8]) {
+    let mut runner = runner(seed);
+    let strat = (
+        any::(),
+        proptest::collection::vec((any::(), any::(), any::()), 0..8),
+        any::(),
+    );
+    let Ok(tree) = strat.new_tree(&mut runner) else {
+        return;
+    };
+    let (desc, ops, version) = tree.current();
+
+    let mut versioned = VersionedRelationDesc::new(desc);
+    for (idx, (drop, pick, typ)) in ops.into_iter().enumerate() {
+        let live = versioned.at_version(RelationVersionSelector::Latest);
+        // `drop_column` panics on a column that is part of a key. `arb_relation_desc` doesn't
+        // generate keys, this only keeps the arm honest if that changes.
+        let droppable: Vec = if live.typ().keys.is_empty() {
+            live.iter_names().cloned().collect()
+        } else {
+            Vec::new()
+        };
+        if drop && !droppable.is_empty() {
+            let name = droppable[usize::from(pick) % droppable.len()].clone();
+            let _ = versioned.drop_column(name);
+        } else {
+            // `add_column` panics on a name that is already live.
+            let name = ColumnName::from(format!("fuzz_added_{idx}"));
+            if live.get_by_name(&name).is_none() {
+                let _ = versioned.add_column(name, typ);
+            }
+        }
+    }
+
+    let value = versioned.at_version(RelationVersionSelector::Specific(
+        RelationVersion::from_raw(u64::from(version)),
+    ));
+    let back = protobuf_roundtrip::<_, ProtoRelationDesc>(&value)
+        .expect("valid RelationDesc must round-trip");
+
+    // The column data must survive in `ColumnIndex` order, as must the keys.
+    assert_eq!(
+        value.arity(),
+        back.arity(),
+        "arity changed: {value:?} {back:?}"
+    );
+    assert!(
+        value.iter().eq(back.iter()),
+        "columns changed across proto roundtrip: {value:?} {back:?}"
+    );
+    assert_eq!(
+        value.typ().keys,
+        back.typ().keys,
+        "keys changed across proto roundtrip: {value:?} {back:?}"
+    );
+
+    // The `ColumnIndex` keys themselves do not survive: the proto carries only the values, in
+    // index order, so decoding renumbers them to `0..n`. Pin that, both to catch indexes
+    // shifting some other way and so that teaching the proto to carry them has to come back
+    // through here and through the `to_stable_name` consumers.
+    assert!(
+        back.iter_all()
+            .enumerate()
+            .all(|(pos, (idx, _, _))| idx.to_raw() == pos),
+        "decoded ColumnIndexes are not dense: {back:?}"
+    );
+    let was_dense = value
+        .iter_all()
+        .enumerate()
+        .all(|(pos, (idx, _, _))| idx.to_raw() == pos);
+    if was_dense {
+        assert_eq!(
+            value, back,
+            "dense RelationDesc changed across proto roundtrip"
+        );
+    }
+
+    // Whatever the indexes were, the renumbered desc is a fixed point.
+    let again = protobuf_roundtrip::<_, ProtoRelationDesc>(&back)
+        .expect("re-encoded RelationDesc must round-trip");
+    assert_eq!(back, again, "RelationDesc roundtrip is not idempotent");
+}
+
 fn raw_arm(data: &[u8]) {
     let Ok(proto) = ProtoRelationDesc::decode(data) else {
         return;
@@ -60,6 +174,20 @@ fn raw_arm(data: &[u8]) {
         Err(_) => return,
     };
 
+    // A desc that decodes must be *usable*. `iter()` indexes `typ.columns()` by `typ_idx` and
+    // `into_iter()` zips the metadata against `column_types` with `zip_eq`, so a shape the
+    // decoder lets through panics at first use, inside whichever timely worker touches the
+    // collection, rather than at the boundary. Walking it here turns that into a fuzz finding.
+    assert_eq!(orig.iter().count(), orig.arity());
+    assert_eq!(orig.clone().into_iter().count(), orig.arity());
+    for key in orig.typ().keys.iter().flatten() {
+        assert!(
+            *key < orig.arity(),
+            "key {key} out of bounds for {} columns",
+            orig.arity()
+        );
+    }
+
     let proto2 = >::from_rust(&orig);
     let bytes2 = proto2.encode_to_vec();
     let proto3 = ProtoRelationDesc::decode(bytes2.as_slice())
@@ -75,9 +203,9 @@ fuzz_target!(|data: &[u8]| {
     let Some((&mode, rest)) = data.split_first() else {
         return;
     };
-    if mode & 1 == 0 {
-        arbitrary_arm(rest);
-    } else {
-        raw_arm(rest);
+    match mode % 3 {
+        0 => arbitrary_arm(rest),
+        1 => versioned_arm(rest),
+        _ => raw_arm(rest),
     }
 });
diff --git a/src/repr/fuzz/fuzz_targets/row_arrow_roundtrip.rs b/src/repr/fuzz/fuzz_targets/row_arrow_roundtrip.rs
index 76e7051dab274..47ebab10915f3 100644
--- a/src/repr/fuzz/fuzz_targets/row_arrow_roundtrip.rs
+++ b/src/repr/fuzz/fuzz_targets/row_arrow_roundtrip.rs
@@ -44,7 +44,7 @@ use mz_repr::adt::array::ArrayDimension;
 use mz_repr::adt::char::CharLength;
 use mz_repr::adt::date::Date;
 use mz_repr::adt::interval::Interval;
-use mz_repr::adt::numeric::{cx_datum, Numeric, NumericMaxScale};
+use mz_repr::adt::numeric::{Numeric, NumericMaxScale, cx_datum};
 use mz_repr::adt::range::{Range, RangeBound, RangeLowerBound, RangeUpperBound};
 use mz_repr::adt::timestamp::CheckedTimestamp;
 use mz_repr::adt::varchar::VarCharMaxLength;
@@ -308,7 +308,9 @@ fn push_datum(
                 .try_push_array(&dims, elems.iter().map(Borrow::borrow))
                 .is_err()
             {
-                packer.try_push_array(&[], std::iter::empty::()).unwrap();
+                packer
+                    .try_push_array(&[], std::iter::empty::())
+                    .unwrap();
             }
         }
         // A `Map` of string keys to `value_type` values. Keys must be unique
@@ -357,10 +359,7 @@ fn gen_scalar_datums<'a>(
 /// Only the `Copy` `Datum` variants (those produced by `gen_element_type` /
 /// `gen_range_element_type`) are handled, so the returned `Datum` carries no
 /// borrow and the caller can collect it into a `Vec`.
-fn gen_scalar_datum<'a>(
-    u: &mut Unstructured,
-    ty: &SqlScalarType,
-) -> arbitrary::Result> {
+fn gen_scalar_datum<'a>(u: &mut Unstructured, ty: &SqlScalarType) -> arbitrary::Result> {
     Ok(match ty {
         SqlScalarType::Bool => {
             if bool::arbitrary(u)? {
@@ -377,7 +376,8 @@ fn gen_scalar_datum<'a>(
         SqlScalarType::Float64 => Datum::Float64(f64::arbitrary(u)?.into()),
         SqlScalarType::Numeric { .. } => Datum::Numeric(gen_numeric(u)?),
         SqlScalarType::Date => Datum::Date(
-            Date::from_pg_epoch(i32::arbitrary(u)?).unwrap_or_else(|_| Date::from_pg_epoch(0).unwrap()),
+            Date::from_pg_epoch(i32::arbitrary(u)?)
+                .unwrap_or_else(|_| Date::from_pg_epoch(0).unwrap()),
         ),
         SqlScalarType::Time => {
             let secs = u.int_in_range(0u32..=86_399)?;
@@ -425,10 +425,7 @@ fn push_range(
     let upper: RangeUpperBound = RangeBound::new(upper_d, bool::arbitrary(u)?);
     // An out-of-order range (lower > upper) is an `InvalidRangeError`. On
     // failure just fall back to the empty range so the column stays valid.
-    if packer
-        .push_range(Range::new(Some((lower, upper))))
-        .is_err()
-    {
+    if packer.push_range(Range::new(Some((lower, upper)))).is_err() {
         let _ = packer.push_range(Range { inner: None });
     }
     Ok(())
diff --git a/src/repr/fuzz/fuzz_targets/row_codec_roundtrip.rs b/src/repr/fuzz/fuzz_targets/row_codec_roundtrip.rs
index ddec355bf8efa..b9db80b1a4667 100644
--- a/src/repr/fuzz/fuzz_targets/row_codec_roundtrip.rs
+++ b/src/repr/fuzz/fuzz_targets/row_codec_roundtrip.rs
@@ -51,7 +51,13 @@ fn arb_relation_desc(u: &mut Unstructured) -> RelationDesc {
     for i in 0..ncols {
         let scalar_type = arb_scalar_type(u);
         let nullable = bool::arbitrary(u).unwrap_or(true);
-        builder = builder.with_column(format!("c{i}"), SqlColumnType { scalar_type, nullable });
+        builder = builder.with_column(
+            format!("c{i}"),
+            SqlColumnType {
+                scalar_type,
+                nullable,
+            },
+        );
     }
     builder.finish()
 }
@@ -91,5 +97,8 @@ fuzz_target!(|data: &[u8]| {
     let mut buf = Vec::new();
     orig.encode(&mut buf);
     let round = Row::decode(&buf, &schema).expect("re-encode of a valid Row must decode");
-    assert_eq!(orig, round, "Row changed across Codec roundtrip (schema = {schema:?})");
+    assert_eq!(
+        orig, round,
+        "Row changed across Codec roundtrip (schema = {schema:?})"
+    );
 });
diff --git a/src/repr/fuzz/fuzz_targets/row_proto_roundtrip.rs b/src/repr/fuzz/fuzz_targets/row_proto_roundtrip.rs
index 4698cb43a6f76..d459887da0b60 100644
--- a/src/repr/fuzz/fuzz_targets/row_proto_roundtrip.rs
+++ b/src/repr/fuzz/fuzz_targets/row_proto_roundtrip.rs
@@ -31,8 +31,7 @@ fuzz_target!(|data: &[u8]| {
 
     let proto2 = >::from_rust(&orig);
     let bytes2 = proto2.encode_to_vec();
-    let proto3 = ProtoRow::decode(bytes2.as_slice())
-        .expect("re-encode of valid Row must decode");
+    let proto3 = ProtoRow::decode(bytes2.as_slice()).expect("re-encode of valid Row must decode");
     let round: Row = proto3
         .into_rust()
         .expect("re-encoded Row must convert back to Rust");
diff --git a/src/repr/fuzz/fuzz_targets/scalar_type_proto_roundtrip.rs b/src/repr/fuzz/fuzz_targets/scalar_type_proto_roundtrip.rs
index 74f020306f4a7..bc73964eb9bc6 100644
--- a/src/repr/fuzz/fuzz_targets/scalar_type_proto_roundtrip.rs
+++ b/src/repr/fuzz/fuzz_targets/scalar_type_proto_roundtrip.rs
@@ -13,19 +13,21 @@
 //!
 //! Two arms (the first byte selects):
 //!  - Arbitrary arm: drive `SqlScalarType`'s proptest `Arbitrary` strategy from
-//!    the fuzzer bytes to build a *valid, deeply-nested* type (the recursive
-//!    `List`/`Map`/`Array`/`Record`/`Range` variants, boundary `max_scale` and
-//!    char/varchar `length`, custom OIDs, etc.) and assert
-//!    `from_proto(into_proto(v)) == v`. Random proto bytes almost never reach
-//!    these variants, so this arm is what actually exercises the encoder.
+//!    the fuzzer bytes to build a *valid* type (boundary `max_scale` and
+//!    char/varchar `length`, custom OIDs, the recursive `List`/`Map`/`Record`
+//!    variants up to the strategy's depth cap of 2) and assert it survives an
+//!    encode/decode through the wire codec. Valid values are where a proto3
+//!    presence bug shows up: a default-valued field that silently stops being
+//!    written to the wire still passes an in-memory `RustType` round-trip.
 //!  - Raw-bytes arm: decode arbitrary bytes as `ProtoScalarType`, into Rust, and
 //!    re-encode, keeping coverage of the bare wire decoder against hostile
-//!    input.
+//!    input. `prost` is built with `no-recursion-limit`, so this is also the arm
+//!    that reaches deep nesting, far past what the strategy above produces.
 
 #![no_main]
 
 use libfuzzer_sys::fuzz_target;
-use mz_proto::{ProtoType, RustType};
+use mz_proto::{ProtoType, protobuf_roundtrip};
 use mz_repr::{ProtoScalarType, SqlScalarType};
 use proptest::strategy::{Strategy, ValueTree};
 use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
@@ -36,6 +38,13 @@ fn arbitrary_arm(seed: &[u8]) {
     for (dst, src) in buf.iter_mut().zip(seed.iter()) {
         *dst = *src;
     }
+    // NOTE: hashing the fuzzer bytes into a ChaCha seed costs libFuzzer its
+    // mutation gradient, one flipped bit re-rolls the whole value. The obvious
+    // fix, `RngAlgorithm::PassThrough`, hangs: it feeds the fuzzer bytes in as
+    // the random stream and then yields zeros forever once they run out, and
+    // `rand`'s Lemire sampler loops until a draw clears `thresh`, which a zero
+    // never does for a range that is not a power of two. Every strategy here
+    // outdraws a 4096-byte input.
     let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &buf);
     let mut runner = TestRunner::new_with_rng(Config::default(), rng);
     let value = match ::arbitrary()
@@ -45,8 +54,8 @@ fn arbitrary_arm(seed: &[u8]) {
         Err(_) => return,
     };
 
-    let proto = value.into_proto();
-    let back = SqlScalarType::from_proto(proto).expect("valid SqlScalarType must round-trip");
+    let back = protobuf_roundtrip::<_, ProtoScalarType>(&value)
+        .expect("valid SqlScalarType must round-trip");
     assert_eq!(value, back, "SqlScalarType changed across proto roundtrip");
 }
 
diff --git a/src/repr/fuzz/fuzz_targets/strconv_parse_date.rs b/src/repr/fuzz/fuzz_targets/strconv_parse_date.rs
index 08fad748842dc..6781078895709 100644
--- a/src/repr/fuzz/fuzz_targets/strconv_parse_date.rs
+++ b/src/repr/fuzz/fuzz_targets/strconv_parse_date.rs
@@ -8,26 +8,37 @@
 // by the Apache License, Version 2.0.
 
 //! Fuzz target: `strconv::parse_date` parses untrusted DATE literal text. A
-//! re-parseable rendering of a parsed value must yield the same value.
+//! rendering of a parsed value must re-parse to the same value, and
+//! `format_date`'s `Nestable` verdict must be honest about that rendering.
 
 #![no_main]
 
 use libfuzzer_sys::fuzz_target;
-use mz_repr::strconv::{format_date, parse_date};
+use mz_repr::strconv::{Nestable, element_needs_escaping, format_date, parse_date};
 
 fuzz_target!(|data: &str| {
     let Ok(d) = parse_date(data) else {
         return;
     };
     let mut buf = String::new();
-    format_date(&mut buf, d);
-    // The renderer can emit text the parser rejects, a known cluster of
-    // date/time round-trip gaps (a leap second `:60` that PG carries, a
-    // >4-digit year, etc.), tracked separately, not panics. Tolerate those and
-    // only assert that a *re-parseable* rendering preserves the value (drift),
-    // and that nothing panics.
-    let Ok(reparsed) = parse_date(&buf) else {
-        return;
-    };
+    // `Nestable` is the other half of `format_date`'s contract, and it is
+    // consumed: `stringify_datum` and the pgwire text encoder use it to decide
+    // whether to escape a date nested in an array, list, map, record or range.
+    // A wrong `Yes` is invisible to the round trip below, because the
+    // wrongly-unquoted rendering still re-parses, so it needs its own assertion.
+    if let Nestable::Yes = format_date(&mut buf, d) {
+        assert!(
+            !element_needs_escaping(buf.as_bytes()),
+            "format_date claimed Nestable::Yes for a rendering that needs escaping: {buf:?}"
+        );
+    }
+    // Re-parse is total over this output space, so a rejection is a renderer bug
+    // rather than something to tolerate. `format_date` writes no time component,
+    // so the leap-second round-trip gap cannot appear here, and a year of more
+    // than four digits parses: `fill_pdt_date` reads a 6-digit leading number
+    // followed by a dash as a full year. Sweeping the whole `Date` range leaves
+    // four reachable forms, a 4, 5 or 6 digit Common Era year and a 4 digit BC
+    // year, and all of them parse.
+    let reparsed = parse_date(&buf).expect("format_date emitted text parse_date rejects");
     assert_eq!(d, reparsed, "date changed across parse/format round trip");
 });
diff --git a/src/repr/fuzz/fuzz_targets/strconv_parse_interval.rs b/src/repr/fuzz/fuzz_targets/strconv_parse_interval.rs
index 6ffdf6cc699cf..cc3767856b809 100644
--- a/src/repr/fuzz/fuzz_targets/strconv_parse_interval.rs
+++ b/src/repr/fuzz/fuzz_targets/strconv_parse_interval.rs
@@ -9,26 +9,35 @@
 
 //! Fuzz target: `strconv::parse_interval` parses untrusted INTERVAL literal
 //! text. It drives a complex datetime token state machine (the most intricate
-//! parser in strconv). Beyond not panicking, its `Display` rendering must
-//! re-parse to the same interval.
+//! parser in strconv). Beyond not panicking, its rendering must re-parse to the
+//! same interval.
+//!
+//! Rendering goes through `strconv::format_interval` rather than `Display`
+//! directly, because that is the entry point `::text` output and the pgwire
+//! encoders call. The two agree today, `format_interval` being a `write!` of
+//! `Display`, so this only keeps the target pointed at the right function if
+//! that ever stops being true.
 
 #![no_main]
 
 use libfuzzer_sys::fuzz_target;
-use mz_repr::strconv::parse_interval;
+use mz_repr::strconv::{format_interval, parse_interval};
 
 fuzz_target!(|data: &str| {
     let Ok(iv) = parse_interval(data) else {
         return;
     };
-    let formatted = iv.to_string();
-    // `Display` collapses sub-day time into one unbounded hours field, and
-    // re-parsing multiplies that hour count back out with checked arithmetic. A
-    // valid interval with micros near `i64::MAX` formats to an hour count whose
-    // re-parse overflows, so re-parse is not total. Tolerate the failure and
-    // only assert the round trip preserves the value when it does re-parse.
-    let Ok(reparsed) = parse_interval(&formatted) else {
-        return;
-    };
-    assert_eq!(iv, reparsed, "interval changed across parse/format round trip");
+    let mut formatted = String::new();
+    format_interval(&mut formatted, iv);
+    // Re-parse is total, so a rejection is a renderer bug rather than something
+    // to tolerate. The hours field is the only unbounded one, and it is derived
+    // as `(micros / 1_000_000).abs() / 3600`, so multiplying it back out during
+    // the re-parse cannot exceed `|micros|` and cannot overflow. `i64::MAX`
+    // micros renders as `2562047788:00:54.775807` and re-parses exactly.
+    let reparsed =
+        parse_interval(&formatted).expect("format_interval emitted text parse_interval rejects");
+    assert_eq!(
+        iv, reparsed,
+        "interval changed across parse/format round trip"
+    );
 });
diff --git a/src/repr/fuzz/fuzz_targets/strconv_parse_numeric.rs b/src/repr/fuzz/fuzz_targets/strconv_parse_numeric.rs
index 38e48f7a1ff92..ba342bb93ba07 100644
--- a/src/repr/fuzz/fuzz_targets/strconv_parse_numeric.rs
+++ b/src/repr/fuzz/fuzz_targets/strconv_parse_numeric.rs
@@ -33,8 +33,18 @@ use mz_repr::strconv::parse_numeric;
 /// Special spellings: the infinity/NaN family the post-parse validation rejects
 /// (or accepts as overflow-`inf`), plus assorted casing.
 const SPECIALS: &[&str] = &[
-    "NaN", "nan", "-NaN", "+NaN", "sNaN", "Infinity", "-Infinity", "+Infinity", "inf", "-inf",
-    "Inf", "INFINITY",
+    "NaN",
+    "nan",
+    "-NaN",
+    "+NaN",
+    "sNaN",
+    "Infinity",
+    "-Infinity",
+    "+Infinity",
+    "inf",
+    "-inf",
+    "Inf",
+    "INFINITY",
 ];
 
 fn push_digits(u: &mut Unstructured, out: &mut String, n: usize) -> arbitrary::Result<()> {
@@ -117,7 +127,10 @@ fn check(s: &str) {
     };
     let formatted = n.0.to_standard_notation_string();
     let reparsed = parse_numeric(&formatted).expect("canonical numeric rendering must re-parse");
-    assert_eq!(n, reparsed, "numeric changed across parse/format round trip");
+    assert_eq!(
+        n, reparsed,
+        "numeric changed across parse/format round trip"
+    );
 }
 
 fuzz_target!(|data: &[u8]| {
diff --git a/src/repr/fuzz/fuzz_targets/strconv_parse_range.rs b/src/repr/fuzz/fuzz_targets/strconv_parse_range.rs
index a48f1da53f5d8..d4dc37bd35e37 100644
--- a/src/repr/fuzz/fuzz_targets/strconv_parse_range.rs
+++ b/src/repr/fuzz/fuzz_targets/strconv_parse_range.rs
@@ -73,22 +73,29 @@ fn run(mut u: Unstructured) -> arbitrary::Result<()> {
         }
     } else {
         // Independent open/close framing so mismatched brackets/parens arise.
-        s.push(if u.int_in_range(0u8..=1)? == 0 { '[' } else { '(' });
+        s.push(if u.int_in_range(0u8..=1)? == 0 {
+            '['
+        } else {
+            '('
+        });
         push_bound(&mut u, &mut s)?;
         s.push(',');
         push_bound(&mut u, &mut s)?;
         // Sometimes drop the closer entirely (truncated framing).
         if u.int_in_range(0u8..=5)? != 0 {
-            s.push(if u.int_in_range(0u8..=1)? == 0 { ']' } else { ')' });
+            s.push(if u.int_in_range(0u8..=1)? == 0 {
+                ']'
+            } else {
+                ')'
+            });
         }
     }
     // 1-in-5: splice extra structural noise to exercise the error paths.
     if u.int_in_range(0u8..=4)? == 0 {
         s.push_str(u.choose(&["[", "]", "(", ")", ",", "\"", "\\", " ", "empty"])?);
     }
-    let _ = mz_repr::strconv::parse_range(&s, |e| {
-        Ok::<_, std::convert::Infallible>(e.into_owned())
-    });
+    let _ =
+        mz_repr::strconv::parse_range(&s, |e| Ok::<_, std::convert::Infallible>(e.into_owned()));
     Ok(())
 }
 
diff --git a/src/repr/fuzz/fuzz_targets/strconv_parse_time.rs b/src/repr/fuzz/fuzz_targets/strconv_parse_time.rs
index 07a22be65983d..ea096cf13ba74 100644
--- a/src/repr/fuzz/fuzz_targets/strconv_parse_time.rs
+++ b/src/repr/fuzz/fuzz_targets/strconv_parse_time.rs
@@ -8,29 +8,61 @@
 // by the Apache License, Version 2.0.
 
 //! Fuzz target: `strconv::parse_time` parses untrusted TIME literal text.
+//!
+//! Unlike TIMESTAMP, TIME has no precision modifier and its cast does not round,
+//! so a `Row` holds every parsed nanosecond while the renderer writes only
+//! microseconds. The renderer is therefore the rounding step, and the oracle is
+//! that it rounds rather than mangles: re-parsing its output must land within
+//! half a microsecond of the value it was handed.
 
 #![no_main]
 
-use chrono::Timelike;
+use chrono::{NaiveTime, Timelike};
 use libfuzzer_sys::fuzz_target;
 use mz_repr::strconv::{format_time, parse_time};
 
+/// The nanosecond count of `t` since midnight, counting chrono's leap second
+/// (spelled as a sub-second of a full second at `23:59:59`) as the nanosecond
+/// after `23:59:59.999999999`.
+///
+/// Rendering can move a value across a second, a minute, or an hour boundary by
+/// rounding its fraction up, so the round trip has to be measured on one number
+/// rather than field by field.
+fn nanos_from_midnight(t: NaiveTime) -> i64 {
+    i64::from(t.num_seconds_from_midnight()) * 1_000_000_000 + i64::from(t.nanosecond())
+}
+
 fuzz_target!(|data: &str| {
     let Ok(t) = parse_time(data) else {
         return;
     };
-    // mz TIME keeps nanosecond precision (its cast, unlike TIMESTAMP's, does not
-    // round) but renders microseconds, and the parser accepts a leap second
-    // (`:60`) the renderer can't round-trip. Both are known TIME/PG-compat gaps
-    // tracked separately. Skip sub-microsecond and leap-second values.
-    let nanos = t.nanosecond();
-    if nanos % 1_000 != 0 || nanos >= 1_000_000_000 {
-        return;
-    }
+
+    // Rendering is unconditional. A sub-microsecond fraction and a leap second
+    // are the two input classes that have broken the renderer before, so they
+    // have to reach it.
     let mut buf = String::new();
     format_time(&mut buf, t);
-    let Ok(reparsed) = parse_time(&buf) else {
-        return;
+    // Every rendering is `HH:MM:SS[.ffffff]`, reaching `:60` only for a leap
+    // second, which the parser accepts back. A rejection means the renderer
+    // emitted text no client can read back.
+    let reparsed = parse_time(&buf).expect("format_time emitted text parse_time rejects");
+
+    // The renderer rounds half away from zero, so a faithful rendering never
+    // moves the value by more than half a microsecond.
+    //
+    // The exception is a round up out of the last second of the day, which has
+    // no second to carry into: a `NaiveTime` wraps to midnight rather than
+    // reaching PostgreSQL's `24:00:00`, so the fraction saturates at `.999999`
+    // and the value can move by just under a full microsecond instead.
+    let saturates = t.num_seconds_from_midnight() == 86_399
+        && (999_999_500..1_000_000_000).contains(&t.nanosecond());
+    let bound = match saturates {
+        true => 1_000,
+        false => 500,
     };
-    assert_eq!(t, reparsed, "time changed across parse/format round trip");
+    let drift = nanos_from_midnight(reparsed) - nanos_from_midnight(t);
+    assert!(
+        drift.abs() <= bound,
+        "time moved {drift}ns across parse/format round trip: {t} -> {buf} -> {reparsed}"
+    );
 });
diff --git a/src/repr/fuzz/fuzz_targets/strconv_parse_timestamp.rs b/src/repr/fuzz/fuzz_targets/strconv_parse_timestamp.rs
index d5ca025c3e0ff..67f16cfa70eb6 100644
--- a/src/repr/fuzz/fuzz_targets/strconv_parse_timestamp.rs
+++ b/src/repr/fuzz/fuzz_targets/strconv_parse_timestamp.rs
@@ -8,42 +8,72 @@
 // by the Apache License, Version 2.0.
 
 //! Fuzz target: `strconv::parse_timestamp` parses untrusted TIMESTAMP literal
-//! text. `CastStringToTimestamp` rounds the parsed value to the type's
-//! precision (microseconds by default) before storage, so mirror that.
+//! text. Two consumers read a parsed value back, and each gets its own oracle.
+//!
+//! `mz_pgrepr::Value::decode_text`, which backs COPY in TEXT and CSV format and
+//! text-format extended-protocol bind parameters, does not round: it stores the
+//! parsed nanoseconds as they are, and `Row` keeps all of them. For that
+//! consumer the renderer is itself the rounding step, so the oracle is that
+//! rendering and re-parsing lands exactly on the microsecond-rounded value.
+//!
+//! `CastStringToTimestamp` instead rounds to the column's precision before
+//! storage. That precision comes from the input rather than being fixed at
+//! microseconds, because the cast carries an arbitrary
+//! `Option` taken from the type modifier. Precisions below 6
+//! are the only ones that reach `round_to_precision`'s rounding branch at all:
+//! at precision 6 the rounding quantum is a single microsecond, so every value
+//! is already on a boundary and the branch is dead.
 
 #![no_main]
 
-use chrono::Timelike;
 use libfuzzer_sys::fuzz_target;
+use mz_repr::adt::timestamp::TimestampPrecision;
 use mz_repr::strconv::{format_timestamp, parse_timestamp};
 
-fuzz_target!(|data: &str| {
+fuzz_target!(|input: (u8, &str)| {
+    let (precision, data) = input;
+    // `None` (the default) plus every declarable precision, 0 through 6.
+    let precision = match precision % 8 {
+        0 => None,
+        p => Some(TimestampPrecision::try_from(i64::from(p) - 1).expect("0..=6 is in range")),
+    };
+
     let Ok(ts) = parse_timestamp(data) else {
         return;
     };
-    // The parser accepts a leap second (`:60`), stored as chrono's leap
-    // representation (sub-second >= 1s). PostgreSQL carries `:60` to the next
-    // minute and our microsecond renderer mis-encodes the leap, so such values
-    // do not round-trip. This is a known parser/PG-compat gap tracked
-    // separately, not a panic. Skip them (rounding to precision below can't
-    // create a leap).
-    if ts.nanosecond() >= 1_000_000_000 {
-        return;
+
+    // The unrounded consumer. `format_timestamp` renders microseconds, so
+    // re-parsing its output has to land on the microsecond-rounded value: any
+    // other result is a value that displays as something it is not.
+    //
+    // `round_to_precision` reports out of range exactly where rounding up leaves
+    // chrono's range, which is the one case the renderer cannot mirror. It has
+    // no error channel, so it saturates the fraction at `.999999` instead, and
+    // there is nothing for the two to agree on.
+    let mut buf = String::new();
+    format_timestamp(&mut buf, &ts);
+    let reparsed =
+        parse_timestamp(&buf).expect("format_timestamp emitted text parse_timestamp rejects");
+    if let Ok(rounded) = ts.round_to_precision(None) {
+        assert_eq!(
+            rounded, reparsed,
+            "rendering a timestamp did not round it to microseconds"
+        );
     }
-    let Ok(ts) = ts.round_to_precision(None) else {
+
+    // The cast. A value already rounded to `precision` sits on a microsecond
+    // boundary, so the renderer has nothing left to round and the round trip is
+    // exact. `expect` rather than a carve-out: a renderer that starts emitting
+    // text the parser rejects is a bug this oracle should report, not tolerate.
+    let Ok(ts) = ts.round_to_precision(precision) else {
         return;
     };
     let mut buf = String::new();
     format_timestamp(&mut buf, &ts);
-    // The renderer can also emit text the parser rejects (e.g. a >4-digit
-    // year), also tracked separately. Tolerate that (re-parse failure) and only
-    // assert that a re-parseable rendering preserves the value, plus no panics.
-    let Ok(reparsed) = parse_timestamp(&buf) else {
-        return;
-    };
-    let Ok(reparsed) = reparsed.round_to_precision(None) else {
-        return;
-    };
+    let reparsed = parse_timestamp(&buf)
+        .expect("format_timestamp emitted text parse_timestamp rejects")
+        .round_to_precision(precision)
+        .expect("re-rounding an already-rounded timestamp cannot overflow");
     assert_eq!(
         ts, reparsed,
         "timestamp changed across parse/format round trip"
diff --git a/src/repr/fuzz/fuzz_targets/strconv_parse_timestamptz.rs b/src/repr/fuzz/fuzz_targets/strconv_parse_timestamptz.rs
index d4428d5b7a5c8..9c3b8d3ee537d 100644
--- a/src/repr/fuzz/fuzz_targets/strconv_parse_timestamptz.rs
+++ b/src/repr/fuzz/fuzz_targets/strconv_parse_timestamptz.rs
@@ -8,42 +8,70 @@
 // by the Apache License, Version 2.0.
 
 //! Fuzz target: `strconv::parse_timestamptz` parses untrusted TIMESTAMPTZ
-//! literal text. `CastStringToTimestampTz` rounds to the type's precision
-//! (microseconds by default) before storage, so mirror that.
+//! literal text. Two consumers read a parsed value back, and each gets its own
+//! oracle. See `strconv_parse_timestamp` for the same pair spelled out at
+//! length.
+//!
+//! `mz_pgrepr::Value::decode_text`, behind COPY and text-format bind parameters,
+//! stores the parsed nanoseconds as they are, so for it the renderer is itself
+//! the rounding step. `CastStringToTimestampTz` instead rounds to the column's
+//! precision first. That precision comes from the input rather than being fixed
+//! at microseconds, because the cast carries an arbitrary
+//! `Option` taken from the type modifier. Precisions below 6
+//! are the only ones that reach `round_to_precision`'s rounding branch at all:
+//! at precision 6 the rounding quantum is a single microsecond, so every value
+//! is already on a boundary and the branch is dead.
 
 #![no_main]
 
-use chrono::Timelike;
 use libfuzzer_sys::fuzz_target;
+use mz_repr::adt::timestamp::TimestampPrecision;
 use mz_repr::strconv::{format_timestamptz, parse_timestamptz};
 
-fuzz_target!(|data: &str| {
+fuzz_target!(|input: (u8, &str)| {
+    let (precision, data) = input;
+    // `None` (the default) plus every declarable precision, 0 through 6.
+    let precision = match precision % 8 {
+        0 => None,
+        p => Some(TimestampPrecision::try_from(i64::from(p) - 1).expect("0..=6 is in range")),
+    };
+
     let Ok(ts) = parse_timestamptz(data) else {
         return;
     };
-    // The parser accepts a leap second (`:60`), stored as chrono's leap
-    // representation (sub-second >= 1s). PostgreSQL carries `:60` to the next
-    // minute and our microsecond renderer mis-encodes the leap, so such values
-    // do not round-trip. This is a known parser/PG-compat gap tracked
-    // separately, not a panic. Skip them (rounding to precision below can't
-    // create a leap).
-    if ts.nanosecond() >= 1_000_000_000 {
-        return;
+    // The unrounded consumer. The renderer writes microseconds, so re-parsing
+    // its output has to land on the microsecond-rounded value. The carve-out is
+    // the one case the renderer cannot mirror: where rounding up would leave
+    // chrono's range it has no error channel and saturates the fraction instead.
+    //
+    // A leap second needs no carve-out here. chrono stores a parsed `:60` as a
+    // sub-second of one second or more, which its `%S` renders back as `60`, and
+    // an offset that would move it off `:59` (where the representation is
+    // unconstructable) is folded during parsing.
+    let mut buf = String::new();
+    format_timestamptz(&mut buf, &ts);
+    let reparsed =
+        parse_timestamptz(&buf).expect("format_timestamptz emitted text parse_timestamptz rejects");
+    if let Ok(rounded) = ts.round_to_precision(None) {
+        assert_eq!(
+            rounded, reparsed,
+            "rendering a timestamptz did not round it to microseconds"
+        );
     }
-    let Ok(ts) = ts.round_to_precision(None) else {
+
+    // The cast. Rounding is idempotent, so re-rounding the re-parsed value to
+    // the same precision must land back on `ts`. `expect` rather than a
+    // carve-out: a renderer that starts emitting text the parser rejects is a
+    // bug this oracle should report rather than tolerate.
+    let Ok(ts) = ts.round_to_precision(precision) else {
         return;
     };
     let mut buf = String::new();
     format_timestamptz(&mut buf, &ts);
-    // The renderer can also emit text the parser rejects (e.g. a >4-digit
-    // year), also tracked separately. Tolerate that (re-parse failure) and only
-    // assert that a re-parseable rendering preserves the value, plus no panics.
-    let Ok(reparsed) = parse_timestamptz(&buf) else {
-        return;
-    };
-    let Ok(reparsed) = reparsed.round_to_precision(None) else {
-        return;
-    };
+    let reparsed = parse_timestamptz(&buf)
+        .expect("format_timestamptz emitted text parse_timestamptz rejects")
+        .round_to_precision(precision)
+        .expect("re-rounding an already-rounded timestamp cannot overflow");
     assert_eq!(
         ts, reparsed,
         "timestamptz changed across parse/format round trip"
diff --git a/src/repr/fuzz/prepare-corpus.sh b/src/repr/fuzz/prepare-corpus.sh
new file mode 100755
index 0000000000000..26ef6d302fd8a
--- /dev/null
+++ b/src/repr/fuzz/prepare-corpus.sh
@@ -0,0 +1,191 @@
+#!/usr/bin/env bash
+
+# Copyright Materialize, Inc. and contributors. All rights reserved.
+#
+# Use of this software is governed by the Business Source License
+# included in the LICENSE file at the root of this repository.
+#
+# As of the Change Date specified in that file, in accordance with
+# the Business Source License, use of this software will be governed
+# by the Apache License, Version 2.0.
+#
+# prepare-corpus.sh populates the corpora of the `ProtoScalarType`-shaped proto
+# round-trip targets with hand-encoded, structurally valid messages.
+#
+# A byte-wise mutator cannot assemble a nested length-delimited protobuf on its
+# own: every enclosing message carries a length prefix that has to agree with
+# the payload it wraps, and prost rejects the whole message the moment one does
+# not, so no partial attempt ever earns coverage feedback to build on. Measured
+# from an empty corpus, 1.4M executions of `column_type_proto_roundtrip` never
+# once produced a `Timestamp` scalar type. Dictionary tokens for the tags do not
+# help, because the tag is not the part the mutator gets wrong.
+#
+# The seeds therefore cover the parameterized variants of `ProtoScalarType`,
+# which are the ones carrying a type parameter that a decoder can get wrong:
+# `Numeric`, `Timestamp`, `TimestampTz`, `Char` and `VarChar`, plus the
+# recursive wrappers `Array`, `List`, `Map`, `Record` and `Range` that nest
+# them. Mutating a valid seed's parameter bytes is cheap, so this is what puts
+# the domain oracles in those targets within reach.
+#
+# Every seed carries the one-byte mode prefix the target reads off the front of
+# the input, which is the byte that selects the raw-bytes arm these seeds are
+# for. The value differs per target, so it is spelled out at each `write` call
+# rather than shared. Without it the seed's own leading tag is eaten as the mode
+# byte and the remainder decodes as garbage.
+
+set -euo pipefail
+
+cd "$(dirname "$0")"
+
+python3 - <<'PY'
+import os
+
+def varint(n: int) -> bytes:
+    out = bytearray()
+    while True:
+        b = n & 0x7F
+        n >>= 7
+        out.append(b | (0x80 if n else 0))
+        if not n:
+            return bytes(out)
+
+def tag(field: int, wire_type: int) -> bytes:
+    return varint((field << 3) | wire_type)
+
+def ld(field: int, payload: bytes) -> bytes:
+    """A length-delimited field: nested message, string or bytes."""
+    return tag(field, 2) + varint(len(payload)) + payload
+
+def vi(field: int, value: int) -> bytes:
+    return tag(field, 0) + varint(value)
+
+# Field numbers from `src/repr/src/relation_and_scalar.proto`. The oneof is not
+# dense and not ordered, so these are spelled out rather than derived.
+EMPTY_KINDS = {"bool": 1, "int64": 4, "date": 8, "string": 15, "jsonb": 18}
+NUMERIC, CHAR, VARCHAR, ARRAY, LIST, RECORD, MAP = 7, 16, 17, 20, 21, 22, 24
+RANGE, TIMESTAMP, TIMESTAMPTZ = 33, 37, 38
+
+def scalar(kind_field: int, payload: bytes = b"") -> bytes:
+    return ld(kind_field, payload)
+
+# ProtoOptionalNumericMaxScale { ProtoNumericMaxScale value = 1 }, whose own
+# `value = 1` is the u8 scale. An absent inner message is `numeric` with no
+# declared scale, which is a distinct decode path worth its own seed.
+def numeric(max_scale: int | None) -> bytes:
+    inner = b"" if max_scale is None else ld(1, vi(1, max_scale))
+    return scalar(NUMERIC, inner)
+
+def timestamp(kind_field: int, precision: int | None) -> bytes:
+    inner = b"" if precision is None else ld(1, vi(1, precision))
+    return scalar(kind_field, inner)
+
+def char_like(kind_field: int, length: int | None) -> bytes:
+    inner = b"" if length is None else ld(1, vi(1, length))
+    return scalar(kind_field, inner)
+
+def column_type(scalar_bytes: bytes, nullable: bool) -> bytes:
+    body = ld(1, scalar_bytes)
+    if nullable:
+        body += vi(2, 1)
+    return body
+
+BOOL = scalar(EMPTY_KINDS["bool"])
+INT64 = scalar(EMPTY_KINDS["int64"])
+
+# The full set the seeds are built from. Values sit at the edges of each
+# parameter's legal domain, which is where an off-by-one in a decoder's bound
+# check shows up.
+SCALARS = {
+    **{name: scalar(field) for name, field in EMPTY_KINDS.items()},
+    "numeric_none": numeric(None),
+    "numeric_0": numeric(0),
+    "numeric_39": numeric(39),
+    "timestamp_none": timestamp(TIMESTAMP, None),
+    "timestamp_0": timestamp(TIMESTAMP, 0),
+    "timestamp_6": timestamp(TIMESTAMP, 6),
+    "timestamptz_none": timestamp(TIMESTAMPTZ, None),
+    "timestamptz_6": timestamp(TIMESTAMPTZ, 6),
+    "char_none": char_like(CHAR, None),
+    "char_1": char_like(CHAR, 1),
+    "char_max": char_like(CHAR, 10_485_759),
+    "varchar_none": char_like(VARCHAR, None),
+    "varchar_1": char_like(VARCHAR, 1),
+    "varchar_max": char_like(VARCHAR, 10_485_759),
+    # Recursive wrappers, each nesting a parameterized leaf so that mutating the
+    # inner parameter stays reachable from a seed.
+    "array_int64": scalar(ARRAY, INT64),
+    "array_timestamp": scalar(ARRAY, timestamp(TIMESTAMP, 6)),
+    "list_char": scalar(LIST, ld(1, char_like(CHAR, 1))),
+    "map_varchar": scalar(MAP, ld(1, char_like(VARCHAR, 1))),
+    "range_timestamp": scalar(RANGE, ld(1, timestamp(TIMESTAMP, 6))),
+    # ProtoRecord { repeated ProtoRecordField fields = 1 }, where a field is
+    # { ProtoColumnName ColumnName = 1, ProtoColumnType ColumnType = 2 }.
+    "record_two_fields": scalar(
+        RECORD,
+        ld(1, ld(1, ld(1, b"a")) + ld(2, column_type(INT64, False)))
+        + ld(1, ld(1, ld(1, b"b")) + ld(2, column_type(numeric(39), True))),
+    ),
+}
+
+def write(target: str, raw_mode: int, seeds: dict[str, bytes]) -> None:
+    corpus = os.path.join("corpus", target)
+    os.makedirs(corpus, exist_ok=True)
+    for stale in os.listdir(corpus):
+        if stale.startswith("seed_") and stale.endswith(".bin"):
+            os.remove(os.path.join(corpus, stale))
+    for name, blob in seeds.items():
+        with open(os.path.join(corpus, f"seed_{name}.bin"), "wb") as f:
+            f.write(bytes([raw_mode]) + blob)
+    print(f"  {corpus:<46} {len(seeds):4d} seeds")
+
+print("Seeded:")
+
+# `mode & 1 == 1` selects the raw-bytes arm.
+write("scalar_type_proto_roundtrip", 1, SCALARS)
+
+# `nullable` is a bare proto3 bool, so `false` is absent from the wire. Both
+# spellings are seeded to keep a presence regression on that field visible.
+write(
+    "column_type_proto_roundtrip",
+    1,
+    {
+        **{f"{name}_notnull": column_type(s, False) for name, s in SCALARS.items()},
+        **{f"{name}_nullable": column_type(s, True) for name, s in SCALARS.items()},
+    },
+)
+
+# ProtoRelationDesc { ProtoRelationType typ = 1, repeated ProtoColumnName names = 2,
+#                     repeated ProtoColumnMetadata metadata = 3 }
+# ProtoRelationType { repeated ProtoColumnType column_types = 1, repeated ProtoKey keys = 2 }
+# The decoder zips `metadata` against `column_types`, so a seed with a mismatched
+# count is rejected at the boundary and teaches the mutator nothing. Seeds keep
+# the counts equal and let mutation break them.
+def relation_desc(scalars: list[bytes], keys: list[list[int]]) -> bytes:
+    columns = b"".join(ld(1, column_type(s, i % 2 == 1)) for i, s in enumerate(scalars))
+    key_msgs = b"".join(ld(2, b"".join(vi(1, k) for k in key)) for key in keys)
+    names = b"".join(ld(2, ld(1, f"c{i}".encode())) for i in range(len(scalars)))
+    # ProtoColumnMetadata { ProtoRelationVersion added = 1, dropped = 2 }, a
+    # version being { uint64 value = 1 }. Column 0 is dropped at version 2 so
+    # that the added/dropped bookkeeping is exercised, not just the happy path.
+    metadata = b"".join(
+        ld(3, ld(1, vi(1, 0)) + (ld(2, vi(1, 2)) if i == 0 else b""))
+        for i in range(len(scalars))
+    )
+    return ld(1, columns + key_msgs) + names + metadata
+
+# This target has three arms and dispatches on `mode % 3`, so 2 is the raw one.
+write(
+    "relation_desc_proto_roundtrip",
+    2,
+    {
+        # `typ` is a required field, so a zero-column desc still carries it. An
+        # entirely empty message is rejected at the boundary like any garbage.
+        "no_cols": relation_desc([], []),
+        "one_col": relation_desc([INT64], []),
+        "keyed": relation_desc([INT64, BOOL], [[0], [0, 1]]),
+        "parameterized": relation_desc(
+            [timestamp(TIMESTAMP, 6), char_like(CHAR, 1), numeric(39)], [[1]]
+        ),
+    },
+)
+PY
diff --git a/src/repr/fuzz/strconv_parse_timestamp.dict b/src/repr/fuzz/strconv_parse_timestamp.dict
new file mode 100644
index 0000000000000..839d58aa11f04
--- /dev/null
+++ b/src/repr/fuzz/strconv_parse_timestamp.dict
@@ -0,0 +1,51 @@
+# libFuzzer dictionary for the strconv_parse_timestamp target, resolved by
+# `dict_for` in test/cargo-fuzz/mzcompose.py.
+#
+# The interesting inputs sit on the two boundaries of `CheckedTimestamp`'s
+# accepted range, and neither is reachable by mutation alone. `HIGH_DATE` is the
+# single date `262142-12-31`, and reaching it means mutating a 6-digit ASCII
+# integer onto one exact value while a late time-of-day is already in place.
+# There is no coverage gradient to climb: the range check yields one edge for
+# "accepted" and one for "rejected", both saturated within the first few
+# executions. Spelling the boundary dates as tokens is what puts the arithmetic
+# just past them within reach.
+#
+# Measured on the un-parameterized version of this target, 13M executions
+# reached `parse_timestamp` successfully 395,498 times and `round_to_precision`
+# returned `Err` zero times, i.e. neither end of the date range was ever
+# approached.
+
+# The range boundaries: `HIGH_DATE` is exactly `chrono::NaiveDate::MAX`, and the
+# low bound is PostgreSQL's 4713 BC.
+"262142-12-31"
+"262143-01-01"
+"4713-12-31"
+"4714-01-01"
+" BC"
+
+# Times that round *up* across the day boundary, one per precision. The 7th
+# fractional digit drives the microsecond nudge, and it is also what makes the
+# renderer carry a full second into the seconds field. The shorter forms drive
+# the rounding branch that only a precision below 6 reaches, where a single `.5`
+# is enough to round up a whole second.
+"23:59:59.9999995"
+"23:59:59.99999"
+"23:59:59.9995"
+"23:59:59.5"
+
+# Leap seconds. chrono stores a parsed `:60` as a sub-second of one second or
+# more, a representation that is unconstructable at any second-of-minute other
+# than `:59`, so anything rebuilding a `NaiveTime` from its parts trips on it.
+":60"
+"23:59:60"
+"00:00:60"
+
+# Separators and forms the parser keys off, so a mutated token still lands in a
+# plausible position.
+"-"
+":"
+"."
+" "
+"T"
+"epoch"
+"infinity"
diff --git a/src/repr/fuzz/strconv_parse_timestamptz.dict b/src/repr/fuzz/strconv_parse_timestamptz.dict
new file mode 100644
index 0000000000000..588780c26c228
--- /dev/null
+++ b/src/repr/fuzz/strconv_parse_timestamptz.dict
@@ -0,0 +1,57 @@
+# libFuzzer dictionary for the strconv_parse_timestamptz target, resolved by
+# `dict_for` in test/cargo-fuzz/mzcompose.py.
+#
+# The interesting inputs sit on the two boundaries of `CheckedTimestamp`'s
+# accepted range, and neither is reachable by mutation alone. `HIGH_DATE` is the
+# single date `262142-12-31`, and reaching it means mutating a 6-digit ASCII
+# integer onto one exact value while a late time-of-day and a westward offset are
+# already in place. There is no coverage gradient to climb: the range check
+# yields one edge for "accepted" and one for "rejected", both saturated within
+# the first few executions. Spelling the boundary dates as tokens is what puts
+# the arithmetic just past them within reach.
+#
+# The offsets and leap-second forms matter for the same reason. Overflow at the
+# high boundary needs the offset to shift the value *out* of range, and the
+# leap-second representation (chrono's sub-second >= 1s, from a `:60` literal) is
+# unconstructable at any second-of-minute other than `:59`, so an offset that is
+# not a whole number of minutes is what exercises the normalization.
+
+# The range boundaries: `HIGH_DATE` is exactly `chrono::NaiveDate::MAX`, and the
+# low bound is PostgreSQL's 4713 BC.
+"262142-12-31"
+"262143-01-01"
+"4713-12-31"
+"4714-01-01"
+" BC"
+
+# Times that round *up* across the day boundary, one per precision. The 7th
+# fractional digit drives the microsecond nudge; the shorter forms drive the
+# rounding branch that only a precision below 6 reaches.
+"23:59:59.9999995"
+"23:59:59.9995"
+"23:59:59.5"
+
+# Leap seconds, at `:59` where chrono can represent them and elsewhere where it
+# cannot.
+":60"
+"23:59:60"
+"00:00:60"
+
+# Offsets. A sub-minute offset shifts the second-of-minute off `:59`; a westward
+# offset is what pushes the high boundary out of range.
+"-01"
+"+01"
+"+00"
+"+00:00:30"
+"-05:30"
+"+16:60"
+" Europe/Amsterdam"
+" UTC"
+
+# Separators the parser keys off, so a mutated token still lands in a plausible
+# position.
+"-"
+":"
+"."
+" "
+"T"
diff --git a/src/repr/src/adt/interval.rs b/src/repr/src/adt/interval.rs
index f306169fa91c0..af24955ec66e3 100644
--- a/src/repr/src/adt/interval.rs
+++ b/src/repr/src/adt/interval.rs
@@ -915,6 +915,9 @@ impl FixedSizeCodec for PackedInterval {
 
 #[cfg(test)]
 mod test {
+    use mz_ore::assert_ok;
+    use mz_proto::protobuf_roundtrip;
+
     use super::*;
     use proptest::prelude::*;
 
@@ -1348,4 +1351,18 @@ mod test {
             sort_intervals(interval);
         });
     }
+
+    // `Interval` <-> `ProtoInterval` is a field-for-field copy today, so this only
+    // bites once the two structs drift: a field added to `Interval` but not carried
+    // through `ProtoInterval` silently decodes as that field's default. NOTE: the
+    // guard is blind unless `Interval::arbitrary` also generates the new field, so
+    // extend the strategy alongside the field.
+    proptest! {
+        #[mz_ore::test]
+        fn interval_protobuf_roundtrip(expect in any::()) {
+            let actual = protobuf_roundtrip::<_, ProtoInterval>(&expect);
+            assert_ok!(actual);
+            assert_eq!(actual.unwrap(), expect);
+        }
+    }
 }
diff --git a/src/repr/src/strconv.rs b/src/repr/src/strconv.rs
index 0382bc47056ad..c60feb78d7d0a 100644
--- a/src/repr/src/strconv.rs
+++ b/src/repr/src/strconv.rs
@@ -2075,6 +2075,18 @@ impl ElementEscaper for RecordElementEscaper {
     }
 }
 
+/// Reports whether `elem` would be quoted by the list, map or record element
+/// escaper.
+///
+/// This is the union of the three, because one rendering can be nested in any of
+/// them. Returning [`Nestable::Yes`] is a promise that this is false, so a
+/// formatter's oracle can check the promise rather than restate the rules.
+pub fn element_needs_escaping(elem: &[u8]) -> bool {
+    ListElementEscaper::needs_escaping(elem)
+        || MapElementEscaper::needs_escaping(elem)
+        || RecordElementEscaper::needs_escaping(elem)
+}
+
 /// Escapes a list, record, or map element in place.
 ///
 /// The element must start at `start` and extend to the end of the buffer. The
diff --git a/src/sql-parser/fuzz/build.rs b/src/sql-parser/fuzz/build.rs
index 6bf65989fbb93..7bce4f670ed5b 100644
--- a/src/sql-parser/fuzz/build.rs
+++ b/src/sql-parser/fuzz/build.rs
@@ -69,7 +69,12 @@ fn main() {
     // Parse each body into alternatives of items.
     let rules: Vec>> = bodies
         .iter()
-        .map(|body| split_unquoted(body, '|').iter().map(|a| tokenize(a)).collect())
+        .map(|body| {
+            split_unquoted(body, '|')
+                .iter()
+                .map(|a| tokenize(a))
+                .collect()
+        })
         .collect();
 
     // Emit the rule table.
@@ -110,7 +115,10 @@ fn main() {
         out.push_str(&format!("    ], leaf_alt: {leaf_alt} }},\n"));
     }
     out.push_str("];\n");
-    out.push_str(&format!("pub static START: usize = {};\n", index_of("statement")));
+    out.push_str(&format!(
+        "pub static START: usize = {};\n",
+        index_of("statement")
+    ));
 
     let out_dir = env::var("OUT_DIR").expect("OUT_DIR");
     fs::write(Path::new(&out_dir).join("grammar.rs"), out).expect("write grammar.rs");
diff --git a/src/sql-parser/fuzz/fuzz_targets/grammar.rs b/src/sql-parser/fuzz/fuzz_targets/grammar.rs
index 3760e29e71ee2..b388d5e3e3850 100644
--- a/src/sql-parser/fuzz/fuzz_targets/grammar.rs
+++ b/src/sql-parser/fuzz/fuzz_targets/grammar.rs
@@ -88,15 +88,6 @@ impl<'a, T: AstInfo> VisitMut<'a, T> for RemoveParens {
     }
 }
 
-/// Reparse errors that are a known printer/parser asymmetry rather than a bug.
-fn benign_reparse_error(msg: &str) -> bool {
-    msg.contains("exceeds nested expression limit")
-        || msg.contains("Expected left square bracket")
-        || msg.contains("Expected left parenthesis")
-        || msg.contains("Expected IN, found")
-        || msg.contains("Expected arrow, found")
-}
-
 fn check_pretty(sql: &str, orig_ast: &Statement) {
     for width in [100, 1] {
         let pretty = match pretty_str_simple(sql, width) {
@@ -106,15 +97,16 @@ fn check_pretty(sql: &str, orig_ast: &Statement) {
         let reparsed = match parse_statements(&pretty) {
             Ok(r) => r,
             Err(e) => {
-                if benign_reparse_error(&e.to_string()) {
-                    continue;
-                }
-                panic!("pretty output failed to reparse: pretty={pretty:?} width={width} err={e}");
+                panic!("pretty output failed to reparse: pretty={pretty:?} width={width} err={e}")
             }
         };
-        let Some(stmt) = reparsed.into_iter().next() else {
-            continue;
-        };
+        assert_eq!(
+            reparsed.len(),
+            1,
+            "pretty output reparsed to {} statements, expected 1\ninput:  {sql:?}\nwidth:  {width}\npretty: {pretty:?}",
+            reparsed.len(),
+        );
+        let stmt = reparsed.into_iter().next().unwrap();
         let mut reparsed_ast = stmt.ast;
         normalize(&mut reparsed_ast);
         assert_eq!(
@@ -128,16 +120,14 @@ fn check_display(orig_ast: &Statement) {
     let displayed = orig_ast.to_ast_string_simple();
     let reparsed = match parse_statements(&displayed) {
         Ok(r) => r,
-        Err(e) => {
-            if benign_reparse_error(&e.to_string()) {
-                return;
-            }
-            panic!("AstDisplay output failed to reparse: displayed={displayed:?} err={e}");
-        }
+        Err(e) => panic!("AstDisplay output failed to reparse: displayed={displayed:?} err={e}"),
     };
-    if reparsed.len() != 1 {
-        return;
-    }
+    assert_eq!(
+        reparsed.len(),
+        1,
+        "AstDisplay output reparsed to {} statements, expected 1\ndisplayed: {displayed:?}",
+        reparsed.len(),
+    );
     let mut reparsed_ast = reparsed.into_iter().next().unwrap().ast;
     // Normalize the reparse too (mirroring `check_pretty`): the parser may
     // re-insert a semantically-redundant `Expr::Nested` (e.g. it parenthesizes a
@@ -168,13 +158,36 @@ fn check_display(orig_ast: &Statement) {
 /// printer's quoting decision (bare names, quoted keyword collisions, names that
 /// only round-trip when quoted).
 const IDENTS: &[&str] = &[
-    "a", "b", "c", "x", "y", "col", "foo", "bar", "t1", "t2", "\"select\"", "\"from\"", "\"any\"",
-    "\"Mixed\"", "\"with space\"", "\"a.b\"", "\"1col\"", "\"qu\"\"ote\"",
+    "a",
+    "b",
+    "c",
+    "x",
+    "y",
+    "col",
+    "foo",
+    "bar",
+    "t1",
+    "t2",
+    "\"select\"",
+    "\"from\"",
+    "\"any\"",
+    "\"Mixed\"",
+    "\"with space\"",
+    "\"a.b\"",
+    "\"1col\"",
+    "\"qu\"\"ote\"",
 ];
 
 /// String literals for `@str`, weighted toward lexing/escaping edge cases.
 const STRINGS: &[&str] = &[
-    "'a'", "''", "'foo bar'", "'it''s'", "'a\"b'", "'%'", "'_'", "'100'",
+    "'a'",
+    "''",
+    "'foo bar'",
+    "'it''s'",
+    "'a\"b'",
+    "'%'",
+    "'_'",
+    "'100'",
 ];
 
 /// Every keyword the lexer knows, for `@kw` (a bare keyword used as an
diff --git a/src/sql-parser/fuzz/fuzz_targets/sql_roundtrip.rs b/src/sql-parser/fuzz/fuzz_targets/sql_roundtrip.rs
index 428d6a553aace..a1aa6dbeb029b 100644
--- a/src/sql-parser/fuzz/fuzz_targets/sql_roundtrip.rs
+++ b/src/sql-parser/fuzz/fuzz_targets/sql_roundtrip.rs
@@ -71,7 +71,9 @@ use libfuzzer_sys::arbitrary::{Arbitrary, Unstructured};
 use libfuzzer_sys::fuzz_target;
 use mz_sql_parser::ast::display::AstDisplay;
 use mz_sql_parser::ast::visit_mut::{self, VisitMut};
-use mz_sql_parser::ast::{AstInfo, Expr, Raw, Statement};
+use mz_sql_parser::ast::{
+    AlterRoleOption, AstInfo, Expr, Op, Raw, RoleAttribute, Statement, Value,
+};
 use mz_sql_parser::parser::parse_statements;
 use mz_sql_pretty::pretty_str_simple;
 
@@ -80,8 +82,12 @@ use mz_sql_pretty::pretty_str_simple;
 // ---------------------------------------------------------------------------
 
 /// Strip syntactic noise so AST equality reflects *semantic* fidelity:
-/// `Declare`/`Prepare` capture raw text, and `Expr::Nested` records parens that
-/// the printer is free to add or drop. See `parse_pretty_roundtrip` for detail.
+/// `Declare`/`Prepare` capture raw text, `Expr::Nested` records parens that the
+/// printer is free to add or drop, and a negative numeric literal is the same
+/// value whether the parser folded the sign in (`Number("-1")`) or left a unary
+/// op (`- 1`). The parser chooses by *context* (a leading `- 1` folds, `a + - 1`
+/// does not), so the two forms must compare equal. See `parse_pretty_roundtrip`
+/// for detail.
 fn normalize(stmt: &mut Statement) {
     match stmt {
         Statement::Declare(d) => {
@@ -97,6 +103,38 @@ fn normalize(stmt: &mut Statement) {
     RemoveParens.visit_statement_mut(stmt);
 }
 
+/// Rewrite every `PASSWORD ''` to the placeholder the `AstDisplay`
+/// printer emits in its place.
+///
+/// `RoleAttribute::Password(Some(_))` renders as the fixed string
+/// `PASSWORD ''` in *every* `AstDisplay` format mode, so a password
+/// cannot survive an `AstDisplay` print/reparse cycle. Applying this to both
+/// sides of that comparison keeps the rest of the statement under the oracle
+/// rather than exempting the whole statement.
+///
+/// Only `check_display` needs this. The pretty printer deliberately preserves
+/// the value (`mz_sql_pretty`'s `doc_role_attribute`), so `check_pretty` compares
+/// passwords exactly and must keep doing so.
+fn redact_passwords(stmt: &mut Statement) {
+    let attrs: &mut [RoleAttribute] = match stmt {
+        // `Declare`/`Prepare` print their inner statement through the same
+        // redacting printer, so a secret nested under them is affected too.
+        Statement::Declare(d) => return redact_passwords(&mut d.stmt),
+        Statement::Prepare(p) => return redact_passwords(&mut p.stmt),
+        Statement::CreateRole(c) => &mut c.options,
+        Statement::AlterRole(a) => match &mut a.option {
+            AlterRoleOption::Attributes(attrs) => attrs,
+            AlterRoleOption::Variable(_) => return,
+        },
+        _ => return,
+    };
+    for attr in attrs {
+        if let RoleAttribute::Password(Some(password)) = attr {
+            *password = "".into();
+        }
+    }
+}
+
 struct RemoveParens;
 
 impl<'a, T: AstInfo> VisitMut<'a, T> for RemoveParens {
@@ -105,18 +143,22 @@ impl<'a, T: AstInfo> VisitMut<'a, T> for RemoveParens {
         if let Expr::Nested(inner) = expr {
             *expr = (**inner).clone();
         }
+        // Canonicalize a negative numeric literal to a unary minus over the bare
+        // number, so it compares equal to the unfolded `- ` form the
+        // parser produces in non-leading position. (Positive literals are never
+        // sign-prefixed by the parser, so only `-` needs handling.)
+        if let Expr::Value(Value::Number(n)) = expr {
+            if let Some(rest) = n.strip_prefix('-') {
+                *expr = Expr::Op {
+                    op: Op::bare("-"),
+                    expr1: Box::new(Expr::Value(Value::Number(rest.to_string()))),
+                    expr2: None,
+                };
+            }
+        }
     }
 }
 
-/// Reparse errors that are a known printer/parser asymmetry rather than a bug.
-fn benign_reparse_error(msg: &str) -> bool {
-    msg.contains("exceeds nested expression limit")
-        || msg.contains("Expected left square bracket")
-        || msg.contains("Expected left parenthesis")
-        || msg.contains("Expected IN, found")
-        || msg.contains("Expected arrow, found")
-}
-
 fn check_pretty(sql: &str, orig_ast: &Statement) {
     // The line width must not affect the AST: wrapping is purely cosmetic, so
     // both a wide layout (everything on one line) and a narrow one (maximally
@@ -130,15 +172,16 @@ fn check_pretty(sql: &str, orig_ast: &Statement) {
         let reparsed = match parse_statements(&pretty) {
             Ok(r) => r,
             Err(e) => {
-                if benign_reparse_error(&e.to_string()) {
-                    continue;
-                }
-                panic!("pretty output failed to reparse: pretty={pretty:?} width={width} err={e}");
+                panic!("pretty output failed to reparse: pretty={pretty:?} width={width} err={e}")
             }
         };
-        let Some(stmt) = reparsed.into_iter().next() else {
-            continue;
-        };
+        assert_eq!(
+            reparsed.len(),
+            1,
+            "pretty output reparsed to {} statements, expected 1\npretty: {pretty:?}\nwidth: {width}",
+            reparsed.len(),
+        );
+        let stmt = reparsed.into_iter().next().unwrap();
         let mut reparsed_ast = stmt.ast;
         normalize(&mut reparsed_ast);
         assert_eq!(
@@ -152,12 +195,7 @@ fn check_display(orig_ast: &Statement) {
     let displayed = orig_ast.to_ast_string_simple();
     let reparsed = match parse_statements(&displayed) {
         Ok(r) => r,
-        Err(e) => {
-            if benign_reparse_error(&e.to_string()) {
-                return;
-            }
-            panic!("AstDisplay output failed to reparse: displayed={displayed:?} err={e}");
-        }
+        Err(e) => panic!("AstDisplay output failed to reparse: displayed={displayed:?} err={e}"),
     };
     // One statement must print as exactly one statement. Any other count means
     // the printer emitted text that reparses to a different number of
@@ -176,6 +214,11 @@ fn check_display(orig_ast: &Statement) {
     // free to add or drop. Stripping them from both sides leaves a genuine
     // structural drift to still trip the assert.
     normalize(&mut reparsed_ast);
+    // `AstDisplay` prints a password as a fixed placeholder, so neither side can
+    // carry the original value through this comparison. See `redact_passwords`.
+    let mut orig_ast = orig_ast.clone();
+    redact_passwords(&mut orig_ast);
+    redact_passwords(&mut reparsed_ast);
     // Compare ASTs *structurally*, not by re-printed string. A printer that drops
     // a needed paren can map two distinct ASTs onto the same string (e.g.
     // `IsExpr(a, DistinctFrom(Or(b, c)))` and `Or(IsExpr(a, DistinctFrom(b)), c)`
@@ -183,7 +226,7 @@ fn check_display(orig_ast: &Statement) {
     // to those collisions, but the structural comparison catches them. The stable
     // strings are still shown for a readable diff.
     assert_eq!(
-        *orig_ast,
+        orig_ast,
         reparsed_ast,
         "AstDisplay roundtrip drifted\ndisplayed: {displayed:?}\norig:     {}\nreparsed: {}",
         orig_ast.to_ast_string_stable(),
@@ -335,9 +378,51 @@ const BIN_OPS: &[&str] = &[
 /// (which must never panic on any input), and occasionally a valid-but-unusual
 /// statement the structured grammar wouldn't assemble.
 const NOISE: &[&str] = &[
-    "(", ")", "[", "]", "{", "}", ",", ";", ".", "::", ":", "*", "@", "?", "!", "\\", "\"", "'",
-    "->", "->>", "#>>", "||", "<>", "=>", "%", "~", "&", "|", "$1", "$$", "''", "\"\"", "/*", "*/",
-    "--", "  ", "\t", "\n", "1e999", "0x1", "-0", ".", "e", "E'\\x41'", "U&'\\0041'",
+    "(",
+    ")",
+    "[",
+    "]",
+    "{",
+    "}",
+    ",",
+    ";",
+    ".",
+    "::",
+    ":",
+    "*",
+    "@",
+    "?",
+    "!",
+    "\\",
+    "\"",
+    "'",
+    "->",
+    "->>",
+    "#>>",
+    "||",
+    "<>",
+    "=>",
+    "%",
+    "~",
+    "&",
+    "|",
+    "$1",
+    "$$",
+    "''",
+    "\"\"",
+    "/*",
+    "*/",
+    "--",
+    "  ",
+    "\t",
+    "\n",
+    "1e999",
+    "0x1",
+    "-0",
+    ".",
+    "e",
+    "E'\\x41'",
+    "U&'\\0041'",
 ];
 
 // The parser's AST source, embedded so the fuzzed connector option space stays
@@ -1236,12 +1321,72 @@ impl<'a, 'u> Gen<'a, 'u> {
         }
     }
 
+    fn kafka_aws_privatelink(&mut self) {
+        self.out.push_str("USING AWS PRIVATELINK ");
+        self.qualified_name();
+        match self.pick(4) {
+            0 => {}
+            1 => self.out.push_str(" (AVAILABILITY ZONE = 'use1-az1')"),
+            2 => self.out.push_str(" (PORT = 9092)"),
+            _ => {
+                self.out
+                    .push_str(" (AVAILABILITY ZONE = 'use1-az1', PORT = 9092)");
+            }
+        }
+    }
+
+    fn kafka_broker(&mut self) {
+        self.string_value();
+        match self.pick(3) {
+            0 => {}
+            1 => {
+                self.out.push_str(" USING SSH TUNNEL ");
+                self.qualified_name();
+            }
+            _ => {
+                self.out.push(' ');
+                self.kafka_aws_privatelink();
+            }
+        }
+    }
+
+    fn kafka_matching_broker_rule(&mut self) {
+        self.out.push_str("MATCHING ");
+        self.one_of(&["'*'", "'*.example.com:*'", "'broker:*'", "'a''b*'"]);
+        self.out.push(' ');
+        self.kafka_aws_privatelink();
+    }
+
+    fn kafka_brokers(&mut self) {
+        self.out.push_str(" = ");
+        let (open, close) = if self.chance(1, 2) {
+            ('(', ')')
+        } else {
+            ('[', ']')
+        };
+        self.out.push(open);
+        let n = 1 + self.pick(3);
+        for i in 0..n {
+            if i > 0 {
+                self.out.push_str(", ");
+            }
+            // Keep at least one static broker in every list. Additional entries
+            // exercise the distinct MATCHING-rule display path.
+            if i == 0 || self.chance(2, 3) {
+                self.kafka_broker();
+            } else {
+                self.kafka_matching_broker_rule();
+            }
+        }
+        self.out.push(close);
+    }
+
     /// One `NAME [= value]` config-option clause. Most options take the generic
     /// `option_value`. The handful with a dedicated value grammar the parser
     /// dispatches by name are special-cased: `PARTITION BY` (an expression),
     /// `RETAIN HISTORY` (`FOR ''`), `TEXT`/`EXCLUDE COLUMNS` (an ident
-    /// sequence), `BROKER` (a broker string), and the `… CONNECTION` / `SSH
-    /// TUNNEL` object references (an item name).
+    /// sequence), `BROKER`/`BROKERS` (broker values), and the `… CONNECTION` /
+    /// `SSH TUNNEL` object references (an item name).
     fn config_option(&mut self, name: &str) {
         self.out.push_str(name);
         match name {
@@ -1265,6 +1410,7 @@ impl<'a, 'u> Gen<'a, 'u> {
                 self.out.push(')');
             }
             "BROKER" => self.out.push_str(" 'localhost:9092'"),
+            "BROKERS" => self.kafka_brokers(),
             "AWS CONNECTION" | "GCP CONNECTION" | "SSH TUNNEL" => {
                 self.out.push_str(" = ");
                 self.ident();
@@ -1403,7 +1549,10 @@ impl<'a, 'u> Gen<'a, 'u> {
                     self.ident_list(1, 3);
                     if self.chance(1, 2) {
                         self.out.push_str(" WITH");
-                        self.config_option_list(option_names("AlterSourceAddSubsourceOptionName"), true);
+                        self.config_option_list(
+                            option_names("AlterSourceAddSubsourceOptionName"),
+                            true,
+                        );
                     }
                 } else {
                     self.out.push_str("VALIDATE CONNECTION ");
diff --git a/src/sql-server-util/fuzz/fuzz_targets/sql_server_table_desc_proto_roundtrip.rs b/src/sql-server-util/fuzz/fuzz_targets/sql_server_table_desc_proto_roundtrip.rs
index dd7b74e849a2f..4a881db411d4a 100644
--- a/src/sql-server-util/fuzz/fuzz_targets/sql_server_table_desc_proto_roundtrip.rs
+++ b/src/sql-server-util/fuzz/fuzz_targets/sql_server_table_desc_proto_roundtrip.rs
@@ -26,11 +26,21 @@
 //!
 //! 2. **Constraint-string arm.** Drives the *raw-ingest* path
 //!    `SqlServerTableConstraint::try_from(SqlServerTableConstraintRaw)`,
-//!    which parses the `constraint_type` *string* (`"PRIMARY KEY"` /
-//!    `"UNIQUE"` are accepted, everything else is rejected). It feeds both
-//!    the two valid strings and fuzzer-controlled garbage, and proto
-//!    round-trips any constraint that parses. This covers the
-//!    string-validation boundary that the proto oneof never sees.
+//!    which parses the `constraint_type` *string*. Only the exact spellings
+//!    `"PRIMARY KEY"` and `"UNIQUE"` are accepted. The arm asserts both
+//!    directions of that boundary and the variant each accepted spelling maps
+//!    to, then proto round-trips the constraint inside a table desc.
+//!
+//!    The assertions are the point of the arm, not decoration. Its input space
+//!    is only `CONSTRAINT_TYPES.len() * 4` fixed cases, so a round-trip-only
+//!    oracle would contribute nothing beyond "does not panic", and it would be
+//!    blind to all three ways this parser can regress: accepting a normalized
+//!    or unknown spelling, rejecting a valid one (which makes the whole arm
+//!    inert and indistinguishable from a passing arm), and swapping the two
+//!    variants (which a round trip preserves). The parsed variant drives
+//!    `is_primary` on the `TableConstraint::Unique` that purification writes
+//!    into the generated subsource DDL, so misparsing a non-unique upstream
+//!    constraint as `PrimaryKey` declares a key that does not hold.
 //!
 //! 3. **Decode-type arm.** Builds a `SqlServerColumnRaw` from a real SQL
 //!    Server type name (`bit`, `tinyint`, `uniqueidentifier`, `xml`,
@@ -46,18 +56,36 @@
 
 #![no_main]
 
-use std::sync::Arc;
+use std::sync::{Arc, OnceLock};
 
 use libfuzzer_sys::fuzz_target;
 use mz_proto::{ProtoType, RustType};
+use mz_sql_server_util::ProtoSqlServerTableDesc;
 use mz_sql_server_util::desc::{
-    SqlServerColumnRaw, SqlServerTableConstraint, SqlServerTableConstraintRaw, SqlServerTableDesc,
+    SqlServerColumnDesc, SqlServerColumnRaw, SqlServerTableConstraint, SqlServerTableConstraintRaw,
+    SqlServerTableConstraintType, SqlServerTableDesc,
 };
-use mz_sql_server_util::{ProtoSqlServerColumnDesc, ProtoSqlServerTableDesc};
-use proptest::strategy::{Strategy, ValueTree};
+use proptest::strategy::{BoxedStrategy, Strategy, ValueTree};
 use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
 use prost::Message;
 
+// `Arbitrary::arbitrary()` rebuilds the entire boxed strategy graph on every
+// call: `SqlScalarType`'s ~31-variant `Union` plus a second copy of it for
+// `Array`, the `prop_recursive` wrapper, and a `.*` regex compile per
+// `any::()` leaf. `Config::default()` re-reads the process environment.
+// Both are per-process constants, so pay for them once instead of once per
+// execution. libFuzzer runs a single execution at a time per process, so a
+// `thread_local` suffices for the non-`Sync` strategy.
+thread_local! {
+    static DESC_STRATEGY: BoxedStrategy =
+        ::arbitrary().boxed();
+}
+
+fn config() -> Config {
+    static CONFIG: OnceLock = OnceLock::new();
+    CONFIG.get_or_init(Config::default).clone()
+}
+
 /// Real SQL Server data-type spellings, chosen to exercise every branch of the
 /// product `parse_data_type` mapping and therefore every supported
 /// `SqlServerColumnDecodeType`. The trailing entries deliberately steer into
@@ -68,8 +96,8 @@ const DATA_TYPES: &[&str] = &[
     "smallint",         // I16
     "int",              // I32
     "bigint",           // I64
-    "real",             // F32 (precision <= 24)
-    "float",            // F64
+    "real",             // F32, selected by max_length == 4
+    "float",            // F64, selected by max_length == 8
     "char",             // String
     "varchar",          // String
     "nvarchar",         // String
@@ -93,7 +121,8 @@ const DATA_TYPES: &[&str] = &[
 ];
 
 /// Constraint-type strings: the two the product accepts, plus garbage that
-/// `SqlServerTableConstraint::try_from` must reject.
+/// `SqlServerTableConstraint::try_from` must reject. Parsing is an exact match,
+/// so the near-misses (case, whitespace) belong in the rejected set.
 const CONSTRAINT_TYPES: &[&str] = &[
     "PRIMARY KEY",
     "UNIQUE",
@@ -108,6 +137,10 @@ const CONSTRAINT_TYPES: &[&str] = &[
 /// Assert that a `SqlServerTableDesc` survives a full Rust round-trip through
 /// its proto representation unchanged, including a re-encode/decode of the
 /// wire bytes.
+///
+/// This covers each column and constraint too: `into_proto`/`from_proto`
+/// delegate per element, and equality is structural, so a leaf that failed to
+/// round-trip would show up here.
 fn assert_rust_roundtrip(orig: &SqlServerTableDesc) {
     let proto = orig.into_proto();
     let bytes = proto.encode_to_vec();
@@ -149,47 +182,62 @@ fn craft_column(data: &[u8], idx: usize) -> SqlServerColumnRaw {
     // could, otherwise we trip the assertion on structurally-impossible input.
     // Every other type legitimately carries a range of lengths, so keep
     // fuzzing those across -1 (max), 16, and assorted small/arbitrary values.
+    //
+    // 4 and 8 are pinned because they are the *only* lengths that reach the
+    // `F32` and `F64` decode types: `real`/`float`/`double precision` choose by
+    // byte width rather than by name or precision. Leaving them to the two
+    // random branches made two documented decode types a coincidence.
     let max_length = if matches!(data_type, "text" | "ntext" | "image") {
         16
     } else {
-        match pick(2) % 4 {
+        match pick(2) % 6 {
             0 => -1,
             1 => 16,
-            2 => i16::from(pick(3)),
+            2 => 4,
+            3 => 8,
+            4 => i16::from(pick(3)),
             _ => i16::from_le_bytes([pick(3), pick(4)]),
         }
     };
+    // Modulo 45, not 39: `parse_data_type` rejects a `precision` above 39, and
+    // rejects a `scale` that `NumericMaxScale` cannot hold. Capping both at 38
+    // made those two branches unreachable by construction rather than merely
+    // rare. `SqlServerColumnDesc::new` turns either rejection into an
+    // `Unsupported` decode type, which still round-trips.
     SqlServerColumnRaw {
         name: format!("col{idx}").into(),
         data_type: data_type.into(),
         is_nullable: pick(1) & 1 == 0,
         max_length,
-        precision: pick(5) % 39,
-        scale: pick(6) % 39,
+        precision: pick(5) % 45,
+        scale: pick(6) % 45,
         is_computed: pick(7) & 1 == 0,
     }
 }
 
 fuzz_target!(|data: &[u8]| {
-    // Reserve the first byte as a mode selector and the next 32 bytes as the
-    // proptest seed. Everything after that feeds the raw-bytes / crafting
-    // logic so a single input can drive any arm.
+    // The first byte selects the arm. Every arm then reads from byte 1 on:
+    // only the proptest arm consumes a seed, and it is the one that consumes it,
+    // so there is no shared reservation to skip past. Carving out a fixed
+    // 32-byte seed window for all four arms would starve the other three, since
+    // libFuzzer grows inputs up from empty and their selector bytes would sit
+    // past the window at a length it takes a long time to reach. It would also
+    // decapitate any genuine encoded `ProtoSqlServerTableDesc` dropped into the
+    // corpus (which `--corpus-sync` accumulates) before the raw-bytes arm saw it.
     let mode = data.first().copied().unwrap_or(0);
-    let mut seed = [0u8; 32];
-    let seed_src = data.get(1..33).unwrap_or(&[]);
-    seed[..seed_src.len()].copy_from_slice(seed_src);
-    let rest = data.get(33..).unwrap_or(&[]);
+    let tail = data.get(1..).unwrap_or(&[]);
 
     match mode % 4 {
         0 => {
-            // Valid-value arm: drive proptest's Arbitrary from the seed.
-            let mut runner = TestRunner::new_with_rng(
-                Config::default(),
-                TestRng::from_seed(RngAlgorithm::ChaCha, &seed),
-            );
-            let value = match ::arbitrary()
-                .new_tree(&mut runner)
-            {
+            // Valid-value arm: drive proptest's Arbitrary from the seed. Padded
+            // with zeros rather than requiring all 32 bytes, so short inputs
+            // still steer generation instead of all reusing the zero seed.
+            let mut seed = [0u8; 32];
+            let n = tail.len().min(32);
+            seed[..n].copy_from_slice(&tail[..n]);
+            let mut runner =
+                TestRunner::new_with_rng(config(), TestRng::from_seed(RngAlgorithm::ChaCha, &seed));
+            let value = match DESC_STRATEGY.with(|s| s.new_tree(&mut runner)) {
                 Ok(tree) => tree.current(),
                 Err(_) => return,
             };
@@ -198,23 +246,56 @@ fuzz_target!(|data: &[u8]| {
         1 => {
             // Constraint-string arm: exercise the raw-ingest string parser for
             // both accepted and rejected `constraint_type` spellings.
-            let ty_idx = rest.first().copied().unwrap_or(0) as usize % CONSTRAINT_TYPES.len();
-            let n_cols = (rest.get(1).copied().unwrap_or(0) % 4) as usize;
-            let columns: Vec = (0..n_cols).map(|i| format!("c{i}")).collect();
+            let ty_idx = tail.first().copied().unwrap_or(0) as usize % CONSTRAINT_TYPES.len();
+            let ty = CONSTRAINT_TYPES[ty_idx];
+            let n_cols = (tail.get(1).copied().unwrap_or(0) % 4) as usize;
+            let column_names: Vec = (0..n_cols).map(|i| format!("c{i}")).collect();
             let raw = SqlServerTableConstraintRaw {
                 constraint_name: "fuzz_constraint".to_string(),
-                constraint_type: CONSTRAINT_TYPES[ty_idx].to_string(),
-                columns,
+                constraint_type: ty.to_string(),
+                columns: column_names.clone(),
             };
-            // Garbage strings must be rejected. Valid ones must parse and then
-            // survive a proto round-trip inside a table desc.
-            let Ok(constraint) = SqlServerTableConstraint::try_from(raw) else {
+            let parsed = SqlServerTableConstraint::try_from(raw);
+
+            let expected = match ty {
+                "PRIMARY KEY" => Some(SqlServerTableConstraintType::PrimaryKey),
+                "UNIQUE" => Some(SqlServerTableConstraintType::Unique),
+                _ => None,
+            };
+            let Some(expected) = expected else {
+                assert!(
+                    parsed.is_err(),
+                    "constraint_type {ty:?} must be rejected, parsed as {parsed:?}"
+                );
                 return;
             };
+            let constraint = parsed.expect("an accepted constraint_type must parse");
+            assert_eq!(
+                constraint.constraint_type, expected,
+                "constraint_type {ty:?} mapped to the wrong variant"
+            );
+
+            // Give the desc the columns its constraint names, so the descriptor
+            // is structurally consistent. A future consistency check in
+            // `from_proto` would otherwise report this arm's own inputs.
+            let columns: Box<[SqlServerColumnDesc]> = column_names
+                .iter()
+                .map(|name| {
+                    SqlServerColumnDesc::new(&SqlServerColumnRaw {
+                        name: name.as_str().into(),
+                        data_type: "int".into(),
+                        is_nullable: false,
+                        max_length: 4,
+                        precision: 0,
+                        scale: 0,
+                        is_computed: false,
+                    })
+                })
+                .collect();
             let desc = SqlServerTableDesc {
                 schema_name: "dbo".into(),
                 name: "fuzz".into(),
-                columns: Box::new([]),
+                columns,
                 constraints: vec![constraint],
             };
             assert_rust_roundtrip(&desc);
@@ -222,17 +303,17 @@ fuzz_target!(|data: &[u8]| {
         2 => {
             // Decode-type arm: run the product type-mapping over real type
             // spellings and round-trip the resulting columns.
-            let n_cols = 1 + (rest.first().copied().unwrap_or(0) % 6) as usize;
+            let n_cols = 1 + (tail.first().copied().unwrap_or(0) % 6) as usize;
             let mut columns = Vec::with_capacity(n_cols);
             for i in 0..n_cols {
                 // Give each column a distinct 8-byte window of the input.
                 let off = 1 + i * 8;
-                let window = rest.get(off..).unwrap_or(&[]);
+                let window = tail.get(off..).unwrap_or(&[]);
                 let raw = craft_column(window, i);
-                let mut desc = mz_sql_server_util::desc::SqlServerColumnDesc::new(&raw);
+                let mut desc = SqlServerColumnDesc::new(&raw);
                 // Occasionally populate the deprecated PK-constraint field so
                 // the `Option>` round-trip is covered too.
-                if rest.get(off).copied().unwrap_or(0) & 0x80 != 0 {
+                if tail.get(off).copied().unwrap_or(0) & 0x80 != 0 {
                     desc.primary_key_constraint = Some(Arc::from("pk_fuzz"));
                 }
                 columns.push(desc);
@@ -244,20 +325,10 @@ fuzz_target!(|data: &[u8]| {
                 constraints: vec![],
             };
             assert_rust_roundtrip(&desc);
-
-            // Also assert the per-column proto leaf round-trips independently,
-            // which isolates the `decode_type` oneof + `column_type` mapping.
-            for col in desc.columns.iter() {
-                let proto: ProtoSqlServerColumnDesc = col.into_proto();
-                let back: mz_sql_server_util::desc::SqlServerColumnDesc = proto
-                    .into_rust()
-                    .expect("column desc must convert back to Rust");
-                assert_eq!(col, &back, "SqlServerColumnDesc changed across roundtrip");
-            }
         }
         _ => {
             // Raw-bytes arm: decode arbitrary bytes directly.
-            check_decoded(rest);
+            check_decoded(tail);
         }
     }
 });
diff --git a/src/sql/src/session/vars/definitions.rs b/src/sql/src/session/vars/definitions.rs
index 8805b0f0bdc92..a4b2ba40919d3 100644
--- a/src/sql/src/session/vars/definitions.rs
+++ b/src/sql/src/session/vars/definitions.rs
@@ -2427,6 +2427,12 @@ mod tests {
         // We do this in a roundabout way, by first constructing all-false `OptimizerFeatures` and
         // then assigning them to their respective system vars, to ensure we don't forget to update
         // this test when new optimizer features are added.
+        //
+        // NOTE: if the new feature ships enabled, also turn it on in
+        // `mz_transform_fuzz::fuzz_features`, which the cargo-fuzz optimizer targets plan with.
+        // That helper falls back to `Default` (all-`false`) for anything it does not name, so a
+        // flag missing from it silently fuzzes the disabled path. This exhaustive destructuring is
+        // the tripwire for both.
         let false_features = OptimizerFeatures::default();
         let OptimizerFeatures {
             enable_eq_classes_withholding_errors,
diff --git a/src/storage-types/fuzz/fuzz_targets/csv_decode.rs b/src/storage-types/fuzz/fuzz_targets/csv_decode.rs
index 7e5526a453bc6..d1f20fcd4a048 100644
--- a/src/storage-types/fuzz/fuzz_targets/csv_decode.rs
+++ b/src/storage-types/fuzz/fuzz_targets/csv_decode.rs
@@ -14,21 +14,46 @@
 //!
 //! The first two bytes pick the decoder config (column count 1..=4, the field
 //! delimiter, and whether the first row is a validated header). The rest is fed
-//! as the CSV stream, draining it through `decode` exactly as the storage decode
-//! operator does. This exercises the buffer-growth (`OutputFull`/`OutputEndsFull`),
-//! column-count-mismatch, invalid-UTF-8, and header-validation paths.
+//! as a CSV object. Once its ordinary input is consumed, the target makes the
+//! empty-input EOF call and resets the decoder as the storage decode operator
+//! does. It then reuses the state for a second object. This exercises the
+//! buffer-growth (`OutputFull`/`OutputEndsFull`), column-count-mismatch,
+//! invalid-UTF-8, header-validation, EOF-finalization, and object-reset paths.
 
 #![no_main]
 
 use libfuzzer_sys::fuzz_target;
 use mz_storage_types::sources::encoding::{ColumnSpec, CsvDecoderState, CsvEncoding};
 
+fn decode_object(state: &mut CsvDecoderState, mut chunk: &[u8]) {
+    // Drain ordinary input a record at a time. Leave the empty slice for the
+    // explicit EOF call below.
+    while !chunk.is_empty() {
+        let before = chunk.len();
+        match state.decode(&mut chunk) {
+            Ok(None) => break,
+            Ok(Some(_)) | Err(_) => {
+                if chunk.len() == before {
+                    return;
+                }
+            }
+        }
+    }
+
+    if chunk.is_empty() {
+        // csv_core needs a separate empty-input call to emit or reject a final
+        // record without a line terminator.
+        let _ = state.decode(&mut chunk);
+        state.reset_for_new_object();
+    }
+}
+
 fuzz_target!(|data: &[u8]| {
     // First two bytes are the config, the remainder is the CSV stream.
     if data.len() < 2 {
         return;
     }
-    let (cfg, mut chunk) = data.split_at(2);
+    let (cfg, input) = data.split_at(2);
     let n_cols = usize::from(cfg[0] % 4) + 1;
     // Usually the standard comma, but sometimes an arbitrary delimiter byte
     // (which csv_core accepts) to reach unusual framing.
@@ -42,20 +67,6 @@ fuzz_target!(|data: &[u8]| {
     };
 
     let mut state = CsvDecoderState::new(CsvEncoding { columns, delimiter });
-
-    // Drain the stream a record at a time, like the decode operator. Each call
-    // returns one decoded record (or an error for a malformed one, having
-    // consumed it) until the input is exhausted (`Ok(None)`). The progress guard
-    // is belt-and-suspenders against a non-advancing call.
-    loop {
-        let before = chunk.len();
-        match state.decode(&mut chunk) {
-            Ok(None) => break,
-            Ok(Some(_)) | Err(_) => {
-                if chunk.len() == before {
-                    break;
-                }
-            }
-        }
-    }
+    decode_object(&mut state, input);
+    decode_object(&mut state, &input[input.len() / 2..]);
 });
diff --git a/src/storage-types/fuzz/fuzz_targets/dataflow_error_proto_roundtrip.rs b/src/storage-types/fuzz/fuzz_targets/dataflow_error_proto_roundtrip.rs
index 8606af95bfcea..2150bcbd0ab8a 100644
--- a/src/storage-types/fuzz/fuzz_targets/dataflow_error_proto_roundtrip.rs
+++ b/src/storage-types/fuzz/fuzz_targets/dataflow_error_proto_roundtrip.rs
@@ -30,9 +30,9 @@
 use libfuzzer_sys::fuzz_target;
 use mz_proto::ProtoType;
 use mz_storage_types::errors::{DataflowError, ProtoDataflowError};
-use prost::Message;
 use proptest::strategy::{Strategy, ValueTree};
 use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
+use prost::Message;
 
 /// Build a 32-byte proptest seed from `bytes` (zero-padded / truncated).
 fn seed_from(bytes: &[u8]) -> [u8; 32] {
@@ -66,8 +66,8 @@ fuzz_target!(|data: &[u8]| {
             Config::default(),
             TestRng::from_seed(RngAlgorithm::ChaCha, &seed),
         );
-        let Ok(tree) = ::arbitrary()
-            .new_tree(&mut runner)
+        let Ok(tree) =
+            ::arbitrary().new_tree(&mut runner)
         else {
             return;
         };
diff --git a/src/storage-types/fuzz/fuzz_targets/source_data_proto_roundtrip.rs b/src/storage-types/fuzz/fuzz_targets/source_data_proto_roundtrip.rs
index 351994c15222d..e3ea748e4ca1a 100644
--- a/src/storage-types/fuzz/fuzz_targets/source_data_proto_roundtrip.rs
+++ b/src/storage-types/fuzz/fuzz_targets/source_data_proto_roundtrip.rs
@@ -34,9 +34,9 @@ use mz_proto::ProtoType;
 use mz_repr::Row;
 use mz_storage_types::errors::DataflowError;
 use mz_storage_types::sources::{ProtoSourceData, SourceData};
-use prost::Message;
 use proptest::strategy::{Strategy, ValueTree};
 use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
+use prost::Message;
 
 /// Build a 32-byte proptest seed from `bytes` (zero-padded / truncated).
 fn seed_from(bytes: &[u8]) -> [u8; 32] {
@@ -72,14 +72,15 @@ fuzz_target!(|data: &[u8]| {
             TestRng::from_seed(RngAlgorithm::ChaCha, &seed),
         );
         let value = if mode & 2 == 0 {
-            let Ok(tree) = ::arbitrary().new_tree(&mut runner)
+            let Ok(tree) =
+                ::arbitrary().new_tree(&mut runner)
             else {
                 return;
             };
             SourceData(Ok(tree.current()))
         } else {
-            let Ok(tree) =
-                ::arbitrary().new_tree(&mut runner)
+            let Ok(tree) = ::arbitrary()
+                .new_tree(&mut runner)
             else {
                 return;
             };
diff --git a/src/storage-types/fuzz/fuzz_targets/source_export_statement_details_proto_roundtrip.rs b/src/storage-types/fuzz/fuzz_targets/source_export_statement_details_proto_roundtrip.rs
index f6e9fef389758..4cdddc59e5a5b 100644
--- a/src/storage-types/fuzz/fuzz_targets/source_export_statement_details_proto_roundtrip.rs
+++ b/src/storage-types/fuzz/fuzz_targets/source_export_statement_details_proto_roundtrip.rs
@@ -25,11 +25,10 @@
 //!  * **Raw-bytes arm.** Decodes arbitrary bytes straight into the proto,
 //!    exercising the decoder against malformed/adversarial wire input (including
 //!    the SQL Server `Lsn` `try_from` length guard, which is only reachable from
-//!    raw bytes since a re-encoded `Lsn` is always exactly 10 bytes).
-//!
-//! `SourceExportStatementDetails` doesn't derive `PartialEq`/`Debug`, so
-//! losslessness is asserted by comparing the canonical re-encoded bytes from
-//! two successive `Rust -> Proto` round trips.
+//!    raw bytes since a re-encoded `Lsn` is always exactly 10 bytes). Since this
+//!    arm has no independently known Rust value, it checks rejection behavior
+//!    and stability, not whether each protobuf discriminant has the right Rust
+//!    mapping.
 
 #![no_main]
 
@@ -40,9 +39,9 @@ use mz_proto::ProtoType;
 use mz_sql_server_util::desc::SqlServerTableDesc;
 use mz_storage_types::sources::load_generator::LoadGeneratorOutput;
 use mz_storage_types::sources::{ProtoSourceExportStatementDetails, SourceExportStatementDetails};
-use prost::Message;
 use proptest::strategy::{Strategy, ValueTree};
 use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
+use prost::Message;
 
 /// Build a 32-byte proptest seed from `bytes` (zero-padded / truncated).
 fn seed_from(bytes: &[u8]) -> [u8; 32] {
@@ -66,7 +65,7 @@ fn encode(details: &SourceExportStatementDetails) -> Vec {
     .encode_to_vec()
 }
 
-/// `Rust -> Proto -> Rust -> Proto` must reproduce the same canonical bytes.
+/// `Rust -> Proto -> Rust` must preserve the value and canonical encoding.
 fn assert_roundtrip(orig: SourceExportStatementDetails) {
     let canonical = encode(&orig);
     let reparsed = ProtoSourceExportStatementDetails::decode(canonical.as_slice())
@@ -74,13 +73,44 @@ fn assert_roundtrip(orig: SourceExportStatementDetails) {
     let round: SourceExportStatementDetails = reparsed
         .into_rust()
         .expect("re-encoded SourceExportStatementDetails must convert back to Rust");
+    assert_eq!(
+        orig, round,
+        "SourceExportStatementDetails value changed across proto roundtrip"
+    );
     assert_eq!(
         canonical,
         encode(&round),
-        "SourceExportStatementDetails changed across proto roundtrip"
+        "SourceExportStatementDetails canonical encoding changed across proto roundtrip"
     );
 }
 
+fn load_generator_output(selector: u8) -> LoadGeneratorOutput {
+    use mz_storage_types::sources::load_generator::{AuctionView, MarketingView, TpchView};
+
+    match selector % 20 {
+        0 => LoadGeneratorOutput::Default,
+        1 => LoadGeneratorOutput::Auction(AuctionView::Organizations),
+        2 => LoadGeneratorOutput::Auction(AuctionView::Users),
+        3 => LoadGeneratorOutput::Auction(AuctionView::Accounts),
+        4 => LoadGeneratorOutput::Auction(AuctionView::Auctions),
+        5 => LoadGeneratorOutput::Auction(AuctionView::Bids),
+        6 => LoadGeneratorOutput::Marketing(MarketingView::Customers),
+        7 => LoadGeneratorOutput::Marketing(MarketingView::Impressions),
+        8 => LoadGeneratorOutput::Marketing(MarketingView::Clicks),
+        9 => LoadGeneratorOutput::Marketing(MarketingView::Leads),
+        10 => LoadGeneratorOutput::Marketing(MarketingView::Coupons),
+        11 => LoadGeneratorOutput::Marketing(MarketingView::ConversionPredictions),
+        12 => LoadGeneratorOutput::Tpch(TpchView::Supplier),
+        13 => LoadGeneratorOutput::Tpch(TpchView::Part),
+        14 => LoadGeneratorOutput::Tpch(TpchView::Partsupp),
+        15 => LoadGeneratorOutput::Tpch(TpchView::Customer),
+        16 => LoadGeneratorOutput::Tpch(TpchView::Orders),
+        17 => LoadGeneratorOutput::Tpch(TpchView::Lineitem),
+        18 => LoadGeneratorOutput::Tpch(TpchView::Nation),
+        _ => LoadGeneratorOutput::Tpch(TpchView::Region),
+    }
+}
+
 fuzz_target!(|data: &[u8]| {
     let Some((&mode, rest)) = data.split_first() else {
         return;
@@ -140,26 +170,15 @@ fuzz_target!(|data: &[u8]| {
                 }
             }
             3 => {
-                // Cover every `LoadGeneratorOutput` discriminant.
-                let output = match rest.first().copied().unwrap_or(0) % 4 {
-                    0 => LoadGeneratorOutput::Default,
-                    1 => LoadGeneratorOutput::Auction(
-                        mz_storage_types::sources::load_generator::AuctionView::Bids,
-                    ),
-                    2 => LoadGeneratorOutput::Marketing(
-                        mz_storage_types::sources::load_generator::MarketingView::Leads,
-                    ),
-                    _ => LoadGeneratorOutput::Tpch(
-                        mz_storage_types::sources::load_generator::TpchView::Customer,
-                    ),
-                };
+                let output = load_generator_output(rest.first().copied().unwrap_or(0));
                 SourceExportStatementDetails::LoadGenerator { output }
             }
             _ => SourceExportStatementDetails::Kafka {},
         };
         assert_roundtrip(value);
     } else {
-        // Raw-bytes arm: decode adversarial wire bytes, then round-trip.
+        // Raw-bytes arm: exercise rejection and fixed-point stability for
+        // adversarial wire bytes. The structured arm verifies known mappings.
         let Ok(proto) = ProtoSourceExportStatementDetails::decode(rest) else {
             return;
         };
diff --git a/src/storage-types/src/sources.rs b/src/storage-types/src/sources.rs
index 9bbfd9ca5e50e..1533ee36906e7 100644
--- a/src/storage-types/src/sources.rs
+++ b/src/storage-types/src/sources.rs
@@ -908,6 +908,7 @@ impl crate::AlterCompatible for SourceExportDetails {
 /// to generate the appropriate `SourceExportDetails` struct during planning.
 /// NOTE that this is serialized as proto to the catalog, so any changes here
 /// must be backwards compatible or will require a migration.
+#[derive(Debug, Eq, PartialEq)]
 pub enum SourceExportStatementDetails {
     Postgres {
         table: mz_postgres_util::desc::PostgresTableDesc,
diff --git a/src/storage/fuzz/fuzz_targets/upsert_consolidate.rs b/src/storage/fuzz/fuzz_targets/upsert_consolidate.rs
index fcaf5bc142556..9f1ab36272766 100644
--- a/src/storage/fuzz/fuzz_targets/upsert_consolidate.rs
+++ b/src/storage/fuzz/fuzz_targets/upsert_consolidate.rs
@@ -121,9 +121,9 @@ fn push_scalar(packer: &mut RowPacker, u: &mut Unstructured) -> arbitrary::Resul
             i32::arbitrary(u)?,
             i64::arbitrary(u)?,
         ))),
-        10 => packer.push(Datum::Uuid(uuid::Uuid::from_bytes(
-            <[u8; 16]>::arbitrary(u)?,
-        ))),
+        10 => packer.push(Datum::Uuid(uuid::Uuid::from_bytes(<[u8; 16]>::arbitrary(
+            u,
+        )?))),
         11 => packer.push(Datum::MzTimestamp(Timestamp::from(u64::arbitrary(u)?))),
         12 => {
             let len = u.int_in_range(0usize..=20)?;
diff --git a/src/storage/fuzz/fuzz_targets/upsert_runtime.rs b/src/storage/fuzz/fuzz_targets/upsert_runtime.rs
index 0f6d2259f1e25..6721ae725ec69 100644
--- a/src/storage/fuzz/fuzz_targets/upsert_runtime.rs
+++ b/src/storage/fuzz/fuzz_targets/upsert_runtime.rs
@@ -73,7 +73,9 @@ use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
 // throughput bottleneck).
 static PARTS: OnceLock = OnceLock::new();
 thread_local! {
-    static CFG: SourceExportCreationConfig = PARTS.get_or_init(FuzzUpsertParts::new).source_config();
+    static CFG: SourceExportCreationConfig = {
+        PARTS.get_or_init(FuzzUpsertParts::new).source_config()
+    };
 }
 
 fn rt() -> &'static tokio::runtime::Runtime {
@@ -148,9 +150,9 @@ fn push_scalar(packer: &mut RowPacker, u: &mut Unstructured) -> arbitrary::Resul
             i32::arbitrary(u)?,
             i64::arbitrary(u)?,
         ))),
-        10 => packer.push(Datum::Uuid(uuid::Uuid::from_bytes(
-            <[u8; 16]>::arbitrary(u)?,
-        ))),
+        10 => packer.push(Datum::Uuid(uuid::Uuid::from_bytes(<[u8; 16]>::arbitrary(
+            u,
+        )?))),
         11 => packer.push(Datum::MzTimestamp(Timestamp::from(u64::arbitrary(u)?))),
         12 => {
             let len = u.int_in_range(0usize..=20)?;
@@ -376,7 +378,11 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
         })
         .collect();
 
-    let drain_to = commands.iter().map(|(ts, ..)| *ts).max().map_or(0, |m| m + 1);
+    let drain_to = commands
+        .iter()
+        .map(|(ts, ..)| *ts)
+        .max()
+        .map_or(0, |m| m + 1);
 
     let hook_commands: Vec<(u64, UpsertKey, u64, Option)> = commands
         .iter()
diff --git a/src/storage/fuzz/fuzz_targets/upsert_state_consolidate.rs b/src/storage/fuzz/fuzz_targets/upsert_state_consolidate.rs
index 0878367694fa4..7544cbdcfb93d 100644
--- a/src/storage/fuzz/fuzz_targets/upsert_state_consolidate.rs
+++ b/src/storage/fuzz/fuzz_targets/upsert_state_consolidate.rs
@@ -129,9 +129,9 @@ fn push_scalar(packer: &mut RowPacker, u: &mut Unstructured) -> arbitrary::Resul
             i32::arbitrary(u)?,
             i64::arbitrary(u)?,
         ))),
-        10 => packer.push(Datum::Uuid(uuid::Uuid::from_bytes(
-            <[u8; 16]>::arbitrary(u)?,
-        ))),
+        10 => packer.push(Datum::Uuid(uuid::Uuid::from_bytes(<[u8; 16]>::arbitrary(
+            u,
+        )?))),
         11 => packer.push(Datum::MzTimestamp(Timestamp::from(u64::arbitrary(u)?))),
         12 => {
             let len = u.int_in_range(0usize..=20)?;
diff --git a/src/storage/fuzz/fuzz_targets/upsert_value_roundtrip_v2.rs b/src/storage/fuzz/fuzz_targets/upsert_value_roundtrip_v2.rs
index 9cccc0f642ead..82cb9b6603139 100644
--- a/src/storage/fuzz/fuzz_targets/upsert_value_roundtrip_v2.rs
+++ b/src/storage/fuzz/fuzz_targets/upsert_value_roundtrip_v2.rs
@@ -83,7 +83,9 @@ fn push_scalar(packer: &mut RowPacker, u: &mut Unstructured) -> arbitrary::Resul
         }
         7 => {
             if let Some(dt) = gen_naive_dt(u)? {
-                packer.push(Datum::Timestamp(CheckedTimestamp::from_timestamplike(dt).unwrap()));
+                packer.push(Datum::Timestamp(
+                    CheckedTimestamp::from_timestamplike(dt).unwrap(),
+                ));
             } else {
                 packer.push(Datum::Null);
             }
@@ -103,9 +105,9 @@ fn push_scalar(packer: &mut RowPacker, u: &mut Unstructured) -> arbitrary::Resul
             i32::arbitrary(u)?,
             i64::arbitrary(u)?,
         ))),
-        10 => packer.push(Datum::Uuid(uuid::Uuid::from_bytes(
-            <[u8; 16]>::arbitrary(u)?,
-        ))),
+        10 => packer.push(Datum::Uuid(uuid::Uuid::from_bytes(<[u8; 16]>::arbitrary(
+            u,
+        )?))),
         11 => packer.push(Datum::MzTimestamp(Timestamp::from(u64::arbitrary(u)?))),
         12 => {
             let len = u.int_in_range(0usize..=20)?;
@@ -218,10 +220,7 @@ fn push_datum(packer: &mut RowPacker, u: &mut Unstructured) -> arbitrary::Result
 
 /// Generate a vector of `n` scalar datums by packing them into a scratch row and
 /// borrowing them back. (Composite packers need an iterator of `Datum`.)
-fn gen_scalar_vec<'a>(
-    u: &mut Unstructured,
-    n: usize,
-) -> arbitrary::Result>> {
+fn gen_scalar_vec<'a>(u: &mut Unstructured, n: usize) -> arbitrary::Result>> {
     // We only emit `Copy`, `'static`-safe scalar datums here so the returned
     // `Datum`s don't borrow from a scratch buffer. That covers ints, bools,
     // numerics, dates, timestamps, intervals, uuids, and mz-timestamps.
@@ -336,7 +335,10 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
     container.push_into(row);
     let decoded = datum_seq_to_upsert_value(container.index(0));
 
-    assert_eq!(value, decoded, "v2 upsert value encoding did not round-trip");
+    assert_eq!(
+        value, decoded,
+        "v2 upsert value encoding did not round-trip"
+    );
     Ok(())
 }
 
diff --git a/src/transform/fuzz/fuzz_targets/full_optimizer_equiv.rs b/src/transform/fuzz/fuzz_targets/full_optimizer_equiv.rs
index 625eda2040522..c20d790b3af99 100644
--- a/src/transform/fuzz/fuzz_targets/full_optimizer_equiv.rs
+++ b/src/transform/fuzz/fuzz_targets/full_optimizer_equiv.rs
@@ -33,47 +33,83 @@
 //!    row is null in it), and
 //!  * exact keys and cardinality inferred from the actual rows.
 //!
-//! `NonNullRequirements`, `Demand`, `ReduceElision` (group key provably unique),
-//! `RedundantJoin`, and `SemijoinIdempotence` all take code paths off that exact
-//! information that opaque, all-nullable `Get`s never trigger. And they run here
-//! in real pipeline order with their interactions, which the per-transform
-//! `mir_relation_transforms` target (transforms in isolation) also cannot reach.
-//! So this target sits in a real gap between the other two.
+//! Two transforms actually see that: `NonNullRequirements`, which runs before the
+//! first `fuse_and_collapse_fixpoint`, and `RedundantJoin`, which runs inside
+//! `FuseAndCollapse` ahead of that fixpoint's trailing constant folding. Both
+//! take code paths off exact nullability and keys that opaque, all-nullable
+//! `Get`s never trigger, as do the `NormalizeOps`/`FuseAndCollapse`
+//! canonicalizations. And they run here in real pipeline order with their
+//! interactions, which the per-transform `mir_relation_transforms` target
+//! (transforms in isolation) cannot reach.
+//!
+//! NOTE: `Demand`, `ReduceElision` and `SemijoinIdempotence` do *not* belong on
+//! that list, though they branch on the same exact information. `Demand` sits in
+//! `fixpoint_logical_01` and the other two in `fixpoint_logical_02`, both after
+//! step 2's trailing `fold_constants_fixpoint`, and every operator this generator
+//! emits has a `FoldConstants` arm, so by then the plan is a single `Constant` and
+//! they are no-ops. The only executions that reach them with real relational
+//! structure are the ones where folding bails at `FOLD_CONSTANTS_LIMIT`, which
+//! needs roughly two nested 4-way joins over 4-row leaves. Nothing in the suite
+//! covers those three against exact key/cardinality facts: the symbolic target's
+//! `Get` deliberately carries the constant's stored all-nullable, keyless type.
+//! Closing that would mean a leaf whose `Get` declares `constant.typ()`, which
+//! belongs in `gen_get` there rather than here.
 //!
 //! Oracle: fold the input to its `(row, diff)` multiset, run the optimizer, fold
 //! the result. When both fold to a constant, the multisets must be equal. A
 //! divergence is a miscompile. The comparison is conservative (we only assert
-//! when both sides fold, and skip when the optimizer returns an error, e.g. the
-//! `Typecheck` pass rejecting a plan shape), so a surviving assertion failure or
-//! a panic inside the optimizer is a genuine finding.
+//! when both sides fold, and skip a plan matching the open bug CLU-137 via
+//! `hits_non_strict_error_fold`), so a surviving assertion failure or a panic
+//! inside the optimizer is a genuine finding. An optimizer *error* is not a skip:
+//! a plan shape the typechecker rejects panics inside `Typecheck` rather than
+//! becoming an `Err`, so every `TransformError` reaching us is an invariant
+//! violation. See `mz_transform_fuzz::optimize`.
 
 #![no_main]
 
 use libfuzzer_sys::arbitrary::Unstructured;
 use libfuzzer_sys::fuzz_target;
-use mz_transform_fuzz::{fold_to_multiset, gen_constant, gen_rel, optimize};
+use mz_transform_fuzz::{
+    Collapse, collapse, fold_to_multiset, gen_constant, gen_rel, hits_non_strict_error_fold,
+    optimize,
+};
 
 fn run(u: &mut Unstructured) -> libfuzzer_sys::arbitrary::Result<()> {
     let mut leaf = gen_constant;
     let (rel, _schema, _nn) = gen_rel(u, 4, &mut leaf)?;
 
+    // Skip the shape of the open bug CLU-137, which the optimizer gets wrong for
+    // reasons unrelated to whatever else the plan exercises.
+    if hits_non_strict_error_fold(&rel) {
+        return Ok(());
+    }
+
     // The input must fold to actual rows for there to be anything to compare.
     let Some(baseline) = fold_to_multiset(rel.clone()) else {
         return Ok(());
     };
 
-    let Some(optimized) = optimize(rel.clone()) else {
-        return Ok(());
-    };
+    let optimized = optimize(rel.clone());
 
     // The optimizer is semantics-preserving: the optimized plan must fold to the
-    // same multiset. We only assert when the optimized plan also folds (it should,
-    // since all leaves are constant), staying conservative about fold limitations.
-    if let Some(after) = fold_to_multiset(optimized) {
-        assert_eq!(
+    // same multiset.
+    //
+    // Fold the optimized side with `collapse`, not a single `FoldConstants` pass.
+    // `RelationCSE` can bind a repeated subexpression to a `Let`, and this
+    // generator hands it perfect candidates because the `Union` arm clones `inner`
+    // into both branches. `FoldConstants` does not propagate constants through
+    // `Let`/`Get`, so a single pass leaves such a plan unfolded and the assertion
+    // is skipped. That skip lands on exactly the executions worth checking: a
+    // `Let` survives only when the plan was still relational after step 2, i.e.
+    // when folding bailed at the row limit, which is also the only window in which
+    // the post-collapse stages ran on real structure at all.
+    match collapse(optimized) {
+        Collapse::Const(after) => assert_eq!(
             baseline, after,
             "the optimizer changed the result multiset\n{rel:?}"
-        );
+        ),
+        // A genuine fold limitation, or still simplifying at the budget.
+        Collapse::StuckFixpoint | Collapse::BudgetExhausted => {}
     }
     Ok(())
 }
diff --git a/src/transform/fuzz/fuzz_targets/mir_relation_transforms.rs b/src/transform/fuzz/fuzz_targets/mir_relation_transforms.rs
index fa6bc491492b5..e6023a2363350 100644
--- a/src/transform/fuzz/fuzz_targets/mir_relation_transforms.rs
+++ b/src/transform/fuzz/fuzz_targets/mir_relation_transforms.rs
@@ -22,7 +22,16 @@
 //! Transforms exercised: `FoldConstants` itself, `CanonicalizeMfp` (Map/Filter/
 //! Project chains), `UnionBranchCancellation`, the structural fusions
 //! (`Filter`/`Project`/`Map`/`Negate`/`Union`) and `ProjectionExtraction`, plus
-//! a hand-written semantics-preserving structural rewrite. Where
+//! a hand-written semantics-preserving structural rewrite.
+//!
+//! NOTE: the generator deliberately builds some nodes as raw variants rather than
+//! through `MirRelationExpr`'s smart constructors. Those constructors normalize
+//! away exactly the shapes the structural fusions collapse: `map` extends an
+//! existing `Map`'s scalar list, `negate` cancels an enclosing `Negate`, `union`
+//! flattens its operands, `project` composes into an existing `Project`, and
+//! `filter` merges, sorts and dedups predicates. Building only through them left
+//! `MapFusion`, `NegateFusion` and `UnionFusion` unable to fire on any input, so
+//! their assertions compared a plan against a clone of itself. Where
 //! `full_optimizer_equiv` runs the whole pipeline over constant-rooted plans,
 //! this target checks each transform in isolation, so a divergence points at a
 //! single transform rather than an interaction.
@@ -42,6 +51,7 @@ use std::collections::BTreeMap;
 
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
+use mz_expr::visit::Visit;
 use mz_expr::{MirRelationExpr, MirScalarExpr};
 use mz_repr::{Diff, ReprRelationType, Row};
 use mz_transform::canonicalization::ProjectionExtraction;
@@ -50,7 +60,8 @@ use mz_transform::fold_constants::FoldConstants;
 use mz_transform::fusion;
 use mz_transform::union_cancel::UnionBranchCancellation;
 use mz_transform_fuzz::{
-    Ty, apply_recursively, fold_to_multiset, gen_constant, gen_scalar, rand_ty,
+    FOLD_ROW_LIMIT, Ty, apply_recursively, fold_to_multiset, gen_constant, gen_scalar,
+    hits_non_strict_error_fold, rand_ty,
 };
 
 fn gen_rel(u: &mut Unstructured, depth: u32) -> arbitrary::Result<(MirRelationExpr, Vec)> {
@@ -62,10 +73,26 @@ fn gen_rel(u: &mut Unstructured, depth: u32) -> arbitrary::Result<(MirRelationEx
         // Filter: 1-2 boolean predicates over the input columns, shape unchanged.
         0 => {
             let n = u.int_in_range(1usize..=2)?;
-            let preds = (0..n)
+            let mut preds = (0..n)
                 .map(|_| gen_scalar(u, Ty::Bool, &schema, 2))
                 .collect::>>()?;
-            (inner.filter(preds), schema)
+            // `filter` merges into an existing `Filter` and sorts/dedups, so the
+            // smart constructor cannot build the nested `Filter` that
+            // `fusion::filter`'s own loop looks for. Split the predicates across
+            // two real nodes sometimes. See `renest` note in the module doc.
+            let rel = if preds.len() == 2 && u.ratio(1u8, 2u8)? {
+                let outer = preds.pop().expect("len 2");
+                MirRelationExpr::Filter {
+                    input: Box::new(MirRelationExpr::Filter {
+                        input: Box::new(inner),
+                        predicates: preds,
+                    }),
+                    predicates: vec![outer],
+                }
+            } else {
+                inner.filter(preds)
+            };
+            (rel, schema)
         }
         // Map: append one computed column.
         1 => {
@@ -73,7 +100,18 @@ fn gen_rel(u: &mut Unstructured, depth: u32) -> arbitrary::Result<(MirRelationEx
             let e = gen_scalar(u, ty, &schema, 2)?;
             let mut s = schema.clone();
             s.push(ty);
-            (inner.map(vec![e]), s)
+            // `map` extends an existing `Map`'s scalar list rather than nesting,
+            // so `Map { input: Map { .. } }` is unbuildable through it, and that
+            // is the only shape `fusion::map` collapses.
+            let rel = if u.ratio(1u8, 2u8)? {
+                MirRelationExpr::Map {
+                    input: Box::new(inner),
+                    scalars: vec![e],
+                }
+            } else {
+                inner.map(vec![e])
+            };
+            (rel, s)
         }
         // Project: pick a (possibly reordered/duplicated) subset of columns.
         2 => {
@@ -83,10 +121,34 @@ fn gen_rel(u: &mut Unstructured, depth: u32) -> arbitrary::Result<(MirRelationEx
             for _ in 0..k {
                 outputs.push(u.int_in_range(0..=len - 1)?);
             }
-            let s = outputs.iter().map(|&i| schema[i]).collect();
-            (inner.project(outputs), s)
+            let s: Vec = outputs.iter().map(|&i| schema[i]).collect();
+            // `project` composes into an existing `Project` instead of nesting.
+            let rel = if u.ratio(1u8, 2u8)? {
+                MirRelationExpr::Project {
+                    input: Box::new(inner),
+                    outputs,
+                }
+            } else {
+                inner.project(outputs)
+            };
+            (rel, s)
+        }
+        3 => {
+            // `negate` cancels against an existing `Negate`, so the doubled shape
+            // `fusion::negate` exists to collapse has to be built by hand. Two
+            // negations are semantically the identity, so the fold result is
+            // `inner`'s either way.
+            let rel = if u.ratio(1u8, 2u8)? {
+                MirRelationExpr::Negate {
+                    input: Box::new(MirRelationExpr::Negate {
+                        input: Box::new(inner),
+                    }),
+                }
+            } else {
+                inner.negate()
+            };
+            (rel, schema)
         }
-        3 => (inner.negate(), schema),
         4 => (inner.distinct(), schema),
         // Union `inner` with a cancelling counterpart. Instead of the trivial
         // `inner ∪ -inner`, the counterpart is `inner` wrapped in a random chain
@@ -145,19 +207,38 @@ fn gen_rel(u: &mut Unstructured, depth: u32) -> arbitrary::Result<(MirRelationEx
             // `inner` filtered by a fresh predicate.
             let distinct_pred = gen_scalar(u, Ty::Bool, &schema, 2)?;
             let extra = inner.clone().filter(vec![distinct_pred]);
-            // Randomize branch order so the matcher's position search is exercised
-            // (`.union` flattens, so this yields a single 3-input `Union`).
+            // Randomize branch order so the matcher's position search is exercised.
             let [b0, b1, b2] = match u.int_in_range(0u8..=2)? {
                 0 => [right, extra, left],
                 1 => [extra, left, right],
                 _ => [left, right, extra],
             };
-            (b0.union(b1).union(b2), schema)
+            // `union` flattens both operands, so it yields a single 3-input
+            // `Union` and never the nested shape `fusion::union` collapses. Build
+            // that shape by hand sometimes; nesting does not change the multiset.
+            let rel = if u.ratio(1u8, 2u8)? {
+                MirRelationExpr::Union {
+                    base: Box::new(b0),
+                    inputs: vec![MirRelationExpr::Union {
+                        base: Box::new(b1),
+                        inputs: vec![b2],
+                    }],
+                }
+            } else {
+                b0.union(b1).union(b2)
+            };
+            (rel, schema)
         }
     })
 }
 
 /// Wrap `rel` in a transformation that preserves its `(row, diff)` multiset.
+///
+/// Every arm is built from raw variants. Through the smart constructors two of
+/// these were unconditional no-ops, which silently reduced the oracle to
+/// `fold(rel) == fold(rel)`: `filter` drops a literal-true predicate outright,
+/// and `negate` cancels against the `Negate` the first call just added, so
+/// `rel.negate().negate()` is the identity on every plan this generator builds.
 fn wrap_preserving(
     u: &mut Unstructured,
     rel: MirRelationExpr,
@@ -165,9 +246,27 @@ fn wrap_preserving(
 ) -> arbitrary::Result {
     let identity = || (0..arity).collect::>();
     Ok(match u.int_in_range(0u8..=3)? {
-        0 => rel.project(identity()),
-        1 => rel.filter(vec![MirScalarExpr::literal_true()]),
-        2 => rel.negate().negate(),
+        // A reversing projection composed with its own inverse.
+        0 => {
+            let rev: Vec = (0..arity).rev().collect();
+            MirRelationExpr::Project {
+                input: Box::new(MirRelationExpr::Project {
+                    input: Box::new(rel),
+                    outputs: rev.clone(),
+                }),
+                outputs: rev,
+            }
+        }
+        // A `Filter` node that is actually present in the tree.
+        1 => MirRelationExpr::Filter {
+            input: Box::new(rel),
+            predicates: vec![MirScalarExpr::literal_true()],
+        },
+        2 => MirRelationExpr::Negate {
+            input: Box::new(MirRelationExpr::Negate {
+                input: Box::new(rel),
+            }),
+        },
         _ => rel
             .map(vec![MirScalarExpr::literal_true()])
             .project(identity()),
@@ -208,6 +307,13 @@ fn assert_same_rows(
 
 fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
     let (rel, schema) = gen_rel(u, 5)?;
+
+    // Skip the shape of the open bug CLU-137. `CanonicalizeMfp` and
+    // `FoldConstants` both reach the `reduce` fold it lives in.
+    if hits_non_strict_error_fold(&rel) {
+        return Ok(());
+    }
+
     let baseline = fold_to_multiset(rel.clone());
 
     // A hand-written semantics-preserving structural rewrite.
@@ -235,32 +341,49 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
     }
 
     // Structural fusions. Each is a purely local, semantics-preserving rewrite
-    // applied across the whole tree (pre-order, matching their real drivers).
-    // None changes the result multiset or the output shape, on any input.
-    for (who, action) in [
+    // applied across the whole tree, in the traversal order its real driver uses:
+    // the `Filter`/`Project`/`Map`/`Negate` fusions call `visit_mut_pre`, while
+    // `fusion::union` and `ProjectionExtraction` call `visit_mut_post`. None
+    // changes the result multiset or the output shape, on any input.
+    for (who, action, post_order) in [
         (
             "FilterFusion",
             fusion::filter::Filter::action as fn(&mut MirRelationExpr),
+            false,
         ),
-        ("ProjectFusion", fusion::project::Project::action),
-        ("MapFusion", fusion::map::Map::action),
-        ("NegateFusion", fusion::negate::Negate::action),
-        ("UnionFusion", fusion::union::Union::action),
-        ("ProjectionExtraction", ProjectionExtraction::action),
+        ("ProjectFusion", fusion::project::Project::action, false),
+        ("MapFusion", fusion::map::Map::action, false),
+        ("NegateFusion", fusion::negate::Negate::action, false),
+        ("UnionFusion", fusion::union::Union::action, true),
+        ("ProjectionExtraction", ProjectionExtraction::action, true),
     ] {
         let mut r = rel.clone();
         let before = r.typ();
-        r.visit_pre_mut(action);
+        let mut action = action;
+        if post_order {
+            r.visit_mut_post(&mut action);
+        } else {
+            r.visit_pre_mut(action);
+        }
         assert_shape(&before, &r.typ(), who, &rel);
         assert_same_rows(&baseline, r, who, &rel);
     }
 
     // FoldConstants: the evaluator itself must at least preserve shape.
     {
-        let mut r = rel;
+        let mut r = rel.clone();
         let before = r.typ();
-        if apply_recursively(FoldConstants { limit: None }, &mut r).is_ok() {
-            assert_shape(&before, &r.typ(), "FoldConstants", &r);
+        if apply_recursively(
+            FoldConstants {
+                limit: Some(FOLD_ROW_LIMIT),
+            },
+            &mut r,
+        )
+        .is_ok()
+        {
+            // Report the input plan, not the folded one: the folded output is the
+            // harder direction to triage a divergence from.
+            assert_shape(&before, &r.typ(), "FoldConstants", &rel);
         }
     }
     Ok(())
diff --git a/src/transform/fuzz/fuzz_targets/optimizer_symbolic_equiv.rs b/src/transform/fuzz/fuzz_targets/optimizer_symbolic_equiv.rs
index 195ce44decf64..16d240bd026ee 100644
--- a/src/transform/fuzz/fuzz_targets/optimizer_symbolic_equiv.rs
+++ b/src/transform/fuzz/fuzz_targets/optimizer_symbolic_equiv.rs
@@ -31,9 +31,13 @@
 //! `Let`/local `Get` bindings (e.g. from CSE) are collapsed by `collapse`, which
 //! iterates `FoldConstants` + `NormalizeLets` until the plan reduces to a
 //! `Constant`. The comparison is conservative (only asserted when both sides
-//! fold, a `Typecheck`/optimizer error is a skip), so a surviving divergence or
-//! an optimizer panic is a genuine finding. It covers the symbolic-input
-//! planning that the constant-rooted target cannot reach.
+//! fold, and a plan matching the open bug CLU-137 is skipped via
+//! `hits_non_strict_error_fold`), so a surviving divergence or an optimizer panic
+//! is a genuine finding. An optimizer *error* is not a skip: a rejected plan
+//! shape panics inside `Typecheck` long before it could become one, so every
+//! `TransformError` that gets here is an invariant violation. See
+//! `mz_transform_fuzz::optimize`. It covers the
+//! symbolic-input planning that the constant-rooted target cannot reach.
 
 #![no_main]
 
@@ -42,11 +46,10 @@ use std::collections::BTreeMap;
 use libfuzzer_sys::arbitrary::{self, Unstructured};
 use libfuzzer_sys::fuzz_target;
 use mz_expr::{Id, MirRelationExpr};
-use mz_repr::optimize::OptimizerFeatures;
-use mz_repr::{Diff, GlobalId, Row};
-use mz_transform::fold_constants::FoldConstants;
-use mz_transform::normalize_lets::NormalizeLets;
-use mz_transform_fuzz::{Ty, apply_recursively, gen_constant, gen_rel, optimize};
+use mz_repr::GlobalId;
+use mz_transform_fuzz::{
+    Collapse, Ty, collapse, gen_constant, gen_rel, hits_non_strict_error_fold, optimize,
+};
 
 /// A symbolic `Get` leaf bound (in `data`) to a fresh constant collection.
 fn gen_get(
@@ -89,69 +92,6 @@ fn substitute(mut rel: MirRelationExpr, data: &BTreeMap) -
     rel
 }
 
-/// Outcome of trying to fold a (`Get`-free) plan all the way to a `Constant`.
-enum Collapse {
-    /// Reduced to a `Constant` of `Ok` rows. The consolidated `(row, diff)`
-    /// multiset is the actual result.
-    Const(BTreeMap),
-    /// Reached a fixpoint of `FoldConstants` + `NormalizeLets` (applying them no
-    /// longer changes the plan) that is *not* a constant, e.g. the plan errors,
-    /// or folding genuinely cannot evaluate it. This is a legitimate
-    /// fold-limitation skip, not a coverage gap.
-    StuckFixpoint,
-    /// Hit the iteration budget without reaching either a constant or a
-    /// fixpoint. The plan was still simplifying when we ran out of passes. Kept
-    /// distinct from `StuckFixpoint` only to name the two skip reasons.
-    /// `FoldConstants` does not promise a constant input collapses to a
-    /// `Constant` within any limit, so this is a conservative skip too.
-    BudgetExhausted,
-}
-
-/// Fold a (now `Get`-free) plan to a `Constant` by iterating `FoldConstants` +
-/// `NormalizeLets` (to collapse any `Let`s the optimizer's CSE introduced) until
-/// it either becomes a `Constant`, reaches a fixpoint, or exhausts the budget.
-///
-/// This loops to a genuine fixpoint (stops only when a pass leaves the plan
-/// unchanged), so a plan that just needs a few more passes converges rather than
-/// being dropped. The budget is a generous guard against a non-terminating
-/// rewrite.
-fn collapse(mut rel: MirRelationExpr) -> Collapse {
-    let features = OptimizerFeatures::default();
-    const BUDGET: usize = 64;
-    for _ in 0..BUDGET {
-        let before = rel.clone();
-        if apply_recursively(FoldConstants { limit: None }, &mut rel).is_err() {
-            return Collapse::StuckFixpoint;
-        }
-        if rel.as_const().is_some() {
-            break;
-        }
-        if NormalizeLets::new(true)
-            .action(&mut rel, &features)
-            .is_err()
-        {
-            return Collapse::StuckFixpoint;
-        }
-        // A full pass that changed nothing means we will never reach a constant.
-        if rel == before {
-            return Collapse::StuckFixpoint;
-        }
-    }
-    let Some(constant) = rel.as_const() else {
-        // Still simplifying when the budget ran out.
-        return Collapse::BudgetExhausted;
-    };
-    let (Ok(rows), _) = constant else {
-        return Collapse::StuckFixpoint;
-    };
-    let mut multiset: BTreeMap = BTreeMap::new();
-    for (row, diff) in rows {
-        *multiset.entry(row.clone()).or_insert(Diff::ZERO) += *diff;
-    }
-    multiset.retain(|_, d| *d != Diff::ZERO);
-    Collapse::Const(multiset)
-}
-
 fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
     let mut next_id = 0u64;
     let mut data = BTreeMap::new();
@@ -160,18 +100,35 @@ fn run(u: &mut Unstructured) -> arbitrary::Result<()> {
         gen_rel(u, 3, &mut leaf)?
     };
 
+    // Skip the shape of the open bug CLU-137, which the optimizer gets wrong for
+    // reasons unrelated to whatever else the plan exercises. The bound data is
+    // checked too: a `Get`'s constant collection carries no scalars today, but
+    // `substitute` inlines it into the plan the optimizer sees.
+    if hits_non_strict_error_fold(&plan) || data.values().any(hits_non_strict_error_fold) {
+        return Ok(());
+    }
+
     // Ground truth: inline the data into the input plan and fold. Only proceed
     // when the *input* (which has no optimizer-introduced `Let`s) folds to a
     // constant, that is what gives us a result to compare against.
+    // Optimize with the Gets still symbolic, then inline the same data and fold.
+    //
+    // Optimize *before* deciding whether the baseline is comparable. A plan whose
+    // result is an evaluation error folds to an `Err` constant, which `collapse`
+    // reports as a skip, and returning at that point would mean never calling the
+    // optimizer on it at all. That is a large and deliberately generated class:
+    // `gen_scalar` spends one of its three literal-leaf choices on
+    // `EvalError::DivisionByZero`, and any such literal under a `Map`/`Filter`
+    // over a non-empty collection poisons the whole fold, as does a `Reduce` over
+    // a net-negative collection. Optimizing first keeps the "a panic in the
+    // optimizer is a finding" coverage over those inputs, which is the coverage
+    // they were generated for.
+    let optimized = optimize(plan.clone());
+
     let baseline = match collapse(substitute(plan.clone(), &data)) {
         Collapse::Const(b) => b,
         Collapse::StuckFixpoint | Collapse::BudgetExhausted => return Ok(()),
     };
-
-    // Optimize with the Gets still symbolic, then inline the same data and fold.
-    let Some(optimized) = optimize(plan.clone()) else {
-        return Ok(());
-    };
     match collapse(substitute(optimized, &data)) {
         Collapse::Const(after) => assert_eq!(
             baseline, after,
diff --git a/src/transform/fuzz/src/lib.rs b/src/transform/fuzz/src/lib.rs
index 01d0ed32b7961..cb09a96ee3d3e 100644
--- a/src/transform/fuzz/src/lib.rs
+++ b/src/transform/fuzz/src/lib.rs
@@ -36,6 +36,7 @@ use mz_repr::optimize::OptimizerFeatures;
 use mz_repr::{Datum, Diff, GlobalId, ReprColumnType, ReprRelationType, ReprScalarType, Row};
 use mz_transform::dataflow::DataflowMetainfo;
 use mz_transform::fold_constants::FoldConstants;
+use mz_transform::normalize_lets::NormalizeLets;
 use mz_transform::{Optimizer, Transform, TransformCtx, TransformError, typecheck};
 
 /// The scalar types the fuzz targets generate over.
@@ -233,6 +234,14 @@ fn gen_aggregate(u: &mut Unstructured, schema: &[Ty]) -> arbitrary::Result<(Aggr
 /// `Get` (and records its backing data on the side). Either way `leaf` returns a
 /// relation and its column schema; leaves are assumed non-negative.
 ///
+/// `leaf` must return **at least one column**. A zero-arity leaf makes the
+/// `Project` arm's `int_in_range(1..=arity)` an empty range and underflows
+/// `arity - 1`, and the `Reduce` fallback references `column(0)`. Worse than
+/// either, `MirRelationExpr::join_scalars` drops an arity-0 single-row input
+/// *after* `join` has computed the equivalences' global column offsets over the
+/// full input list, so both the equivalences and the schema returned here would
+/// silently point at the wrong columns.
+///
 /// The non-negativity flag is the contract `TopK` (and every dataflow reduction)
 /// requires of its input, so we only place a `TopK` directly over a non-negative
 /// subtree. See the `TopK` arm.
@@ -415,6 +424,59 @@ where
     })
 }
 
+/// The `OptimizerFeatures` every target here plans with: production's defaults.
+///
+/// NOTE: `OptimizerFeatures::default()` is all-`false`, which is *not* what any
+/// deployment runs. Several transforms branch on these, and
+/// `EquivalencePropagation` reads three of them, so planning with the derived
+/// default exercises only the legacy paths. `enable_eq_classes_withholding_errors`
+/// is the pointed one: it exists to stop equivalence propagation suppressing
+/// errors, and `gen_scalar` deliberately seeds error literals, so the one feature
+/// built for this generator's input class was the one turned off.
+///
+/// Only the flags whose production default is `true` are listed; the rest come
+/// from `Default`. A newly added flag therefore arrives here as `false`, which is
+/// wrong if it ships enabled, but the tripwire for that lives where it belongs:
+/// `mz_sql`'s `optimizer_features_no_enable_for_item_parsing` destructures
+/// `OptimizerFeatures` exhaustively, so a new field fails a fast unit test rather
+/// than this crate's multi-minute sanitizer build. Source of truth for the values
+/// is each flag's `default:` in `mz_sql::session::vars::definitions`.
+pub fn fuzz_features() -> OptimizerFeatures {
+    OptimizerFeatures {
+        enable_new_outer_join_lowering: true,
+        enable_reduce_mfp_fusion: true,
+        enable_variadic_left_join_lowering: true,
+        enable_letrec_fixpoint_analysis: true,
+        enable_projection_pushdown_after_relation_cse: true,
+        enable_less_reduce_in_eqprop: true,
+        enable_dequadratic_eqprop_map: true,
+        enable_eq_classes_withholding_errors: true,
+        enable_cast_elimination: true,
+        enable_simplify_quantified_comparisons: true,
+        enable_simplify_from_less_existence: true,
+        enable_coalesce_case_transform: true,
+        enable_will_distinct_propagation: true,
+        enable_fixed_correlated_cte_lowering: true,
+        persist_fast_path_limit: 25,
+        ..Default::default()
+    }
+}
+
+/// Row cap for the oracles' constant folding.
+///
+/// `FoldConstants`' join arm materializes the full cross product *before*
+/// applying equivalences, and `limit: None` disables its only size check, so an
+/// unbounded fold is the harness asking for a `Vec<(Row, Diff)>` bounded only by
+/// the product of every leaf's row count. Generated join trees nest, so that is
+/// reachable in principle and would surface as an `oom-*`/`timeout-*` artifact
+/// blamed on the optimizer.
+///
+/// Declining to fold is already a benign skip on both oracles, so a cap costs
+/// nothing: it is well above the largest product measured over these generators
+/// (~5e5 rows) while keeping peak allocation in the hundreds of MB against the
+/// runner's `-rss_limit_mb=4096`.
+pub const FOLD_ROW_LIMIT: usize = 1_000_000;
+
 /// Apply `transform` over the whole plan through its recursive driver
 /// (`Transform::transform` -> `actually_perform_transform`), not `action`.
 ///
@@ -427,7 +489,7 @@ pub fn apply_recursively(
     transform: T,
     rel: &mut MirRelationExpr,
 ) -> Result<(), TransformError> {
-    let features = OptimizerFeatures::default();
+    let features = fuzz_features();
     let typecheck_ctx = typecheck::empty_typechecking_context();
     let mut df_meta = DataflowMetainfo::default();
     let mut ctx = TransformCtx::local(
@@ -442,8 +504,26 @@ pub fn apply_recursively(
 
 /// Fold `rel`. If it reduces to a `Constant` of `Ok` rows, return the
 /// consolidated `(row, diff)` multiset, otherwise `None`.
+///
+/// `None` covers two benign cases: the plan did not fold all the way down to a
+/// `Constant`, and it folded to an `EvalError`. The latter is not a blind spot
+/// but a required tolerance. The optimizer is knowingly imprecise about error
+/// semantics, `predicate_pushdown` will push a predicate that can error into a
+/// join input and manufacture an error the unoptimized plan never raises (see
+/// the comment there and database-issues#6258), so one side folding to an error
+/// while the other yields rows is accepted behaviour and would otherwise fire
+/// roughly once in every 1,500 executions.
+///
+/// A `TransformError` is a different matter and panics, see [`optimize`].
 pub fn fold_to_multiset(mut rel: MirRelationExpr) -> Option> {
-    apply_recursively(FoldConstants { limit: None }, &mut rel).ok()?;
+    if let Err(e) = apply_recursively(
+        FoldConstants {
+            limit: Some(FOLD_ROW_LIMIT),
+        },
+        &mut rel,
+    ) {
+        panic!("FoldConstants returned an error: {e}");
+    }
     let (Ok(rows), _) = rel.as_const()? else {
         return None;
     };
@@ -455,11 +535,60 @@ pub fn fold_to_multiset(mut rel: MirRelationExpr) -> Option>
     Some(multiset)
 }
 
-/// Run the full logical optimizer. Returns `None` if it errors (e.g. the
-/// `Typecheck` pass rejects the plan). Only a panic is a finding here.
+/// True if `rel` has an erroring operand under a non-strict `AND`/`OR`/
+/// `error_if_null`, the shape of the open bug CLU-137.
+///
+/// Those three swallow an operand's error once another operand fixes the result:
+/// `Or::eval` returns `true` the moment it sees a true operand and drops any
+/// error it collected, `And::eval` does the same for `false`, and
+/// `error_if_null` evaluates its message operand only when the first operand is
+/// NULL. `reduce`'s generic variadic fold nonetheless replaces the whole call
+/// with an operand's literal error, and `undistribute_and_or` can recombine an
+/// erroring operand across the short-circuit boundary. Either way the optimizer
+/// can turn a row the plan should emit into an error, and, once the folded
+/// literal is typed non-nullable, into a *different* row: that is how the count
+/// of a nullable aggregate becomes the count of a non-nullable one.
+///
+/// CLU-137 tracks the fix (see the closed PR #37299 for a full one). Until it
+/// lands, the equivalence oracles skip these plans rather than rediscover it on
+/// every run.
+///
+/// Deliberately conservative. It asks whether an operand *could* error rather
+/// than whether it already holds a literal error, because `reduce` folds a
+/// column-free fallible operand (`9223372036854775807 + 1`, `1 / 0`) to a
+/// literal error first and absorbs it after. The cost is that a plan whose
+/// AND/OR operands merely *might* error is skipped even where the fold could not
+/// have fired: measured at 3.6% of `gen_rel(depth = 4)` plans.
+pub fn hits_non_strict_error_fold(rel: &MirRelationExpr) -> bool {
+    let mut hit = false;
+    rel.visit_scalars(&mut |scalar| {
+        // One definition of the shape, in `mz_expr` next to the fold it describes,
+        // so the several oracles that skip CLU-137 cannot drift apart on which
+        // functions count as non-strict. They already had: this predicate covered
+        // `ErrorIfNull` while `mir_scalar_reduce`'s copy did not.
+        hit |= scalar.could_hit_nonstrict_error_fold();
+    });
+    hit
+}
+
+/// Run the full logical optimizer.
+///
+/// NOTE: a `TransformError` from here is itself a finding, so this panics rather
+/// than reporting one. The tempting reading, that a plan shape the optimizer
+/// rejects comes back as an error to skip, is wrong in both directions.
+/// `Typecheck` returns `Ok(())` on every path and routes a type error through
+/// `type_error!(true, ..)` -> `soft_panic_or_log!`, and `Fixpoint`
+/// non-convergence does the same. Soft assertions default to
+/// `cfg!(debug_assertions)`, which cargo-fuzz enables, so a rejected plan is
+/// already a crash before it can reach us. What is left that can error are
+/// optimizer invariant violations: a `Let`/`Get` on an unbound local id, a `Let`
+/// whose type changed under it, an ANF rebinding that lost an identifier. Those
+/// surface to users as `internal error`, so swallowing them would leave this
+/// harness green through exactly the `normalize_lets`/`cse` regressions it is
+/// best placed to catch.
 #[allow(deprecated)]
-pub fn optimize(rel: MirRelationExpr) -> Option {
-    let features = OptimizerFeatures::default();
+pub fn optimize(rel: MirRelationExpr) -> MirRelationExpr {
+    let features = fuzz_features();
     let typecheck_ctx = typecheck::empty_typechecking_context();
     let mut df_meta = DataflowMetainfo::default();
     let mut ctx = TransformCtx::local(
@@ -470,8 +599,83 @@ pub fn optimize(rel: MirRelationExpr) -> Option {
         Some(GlobalId::Transient(1)),
     );
     let optimizer = Optimizer::logical_optimizer(&mut ctx);
-    optimizer
-        .optimize(rel, &mut ctx)
-        .ok()
-        .map(|o| o.into_inner())
+    match optimizer.optimize(rel, &mut ctx) {
+        Ok(optimized) => optimized.into_inner(),
+        Err(e) => panic!("logical optimizer returned an error: {e}"),
+    }
+}
+
+/// Outcome of trying to fold a (`Get`-free) plan all the way to a `Constant`.
+pub enum Collapse {
+    /// Reduced to a `Constant` of `Ok` rows. The consolidated `(row, diff)`
+    /// multiset is the actual result.
+    Const(BTreeMap),
+    /// Either folding reached a fixpoint that is not a constant (a legitimate
+    /// fold limitation), or it folded all the way to an `Err` constant.
+    ///
+    /// NOTE: those two are not the same thing, and the second is not a fold
+    /// limitation at all: an `EvalError` constant is a fully determined result.
+    /// They share a variant because neither yields a `(row, diff)` multiset to
+    /// compare. Splitting them would let an oracle notice a baseline of
+    /// `Ok(rows)` becoming an `Err` after optimization, but only in that
+    /// direction: `Err -> Ok` is legitimate, since `Demand` can drop an unused
+    /// erroring `Map` column. Even the `Ok -> Err` direction is not assertable
+    /// today, because `predicate_pushdown` knowingly manufactures errors the
+    /// unoptimized plan never raises (database-issues#6258).
+    StuckFixpoint,
+    /// Hit the iteration budget without reaching either a constant or a
+    /// fixpoint. The plan was still simplifying when we ran out of passes. Kept
+    /// distinct from `StuckFixpoint` only to name the two skip reasons.
+    /// `FoldConstants` does not promise a constant input collapses to a
+    /// `Constant` within any limit, so this is a conservative skip too.
+    BudgetExhausted,
+}
+
+/// Fold a (now `Get`-free) plan to a `Constant` by iterating `FoldConstants` +
+/// `NormalizeLets` (to collapse any `Let`s the optimizer's CSE introduced) until
+/// it either becomes a `Constant`, reaches a fixpoint, or exhausts the budget.
+///
+/// This loops to a genuine fixpoint (stops only when a pass leaves the plan
+/// unchanged), so a plan that just needs a few more passes converges rather than
+/// being dropped. The budget is a generous guard against a non-terminating
+/// rewrite.
+pub fn collapse(mut rel: MirRelationExpr) -> Collapse {
+    let features = fuzz_features();
+    const BUDGET: usize = 64;
+    for _ in 0..BUDGET {
+        let before = rel.clone();
+        // A `TransformError` here is an optimizer invariant violation, not a
+        // reason to give up on the plan. See `mz_transform_fuzz::optimize`.
+        if let Err(e) = apply_recursively(
+            FoldConstants {
+                limit: Some(FOLD_ROW_LIMIT),
+            },
+            &mut rel,
+        ) {
+            panic!("FoldConstants returned an error: {e}");
+        }
+        if rel.as_const().is_some() {
+            break;
+        }
+        if let Err(e) = NormalizeLets::new(true).action(&mut rel, &features) {
+            panic!("NormalizeLets returned an error: {e}");
+        }
+        // A full pass that changed nothing means we will never reach a constant.
+        if rel == before {
+            return Collapse::StuckFixpoint;
+        }
+    }
+    let Some(constant) = rel.as_const() else {
+        // Still simplifying when the budget ran out.
+        return Collapse::BudgetExhausted;
+    };
+    let (Ok(rows), _) = constant else {
+        return Collapse::StuckFixpoint;
+    };
+    let mut multiset: BTreeMap = BTreeMap::new();
+    for (row, diff) in rows {
+        *multiset.entry(row.clone()).or_insert(Diff::ZERO) += *diff;
+    }
+    multiset.retain(|_, d| *d != Diff::ZERO);
+    Collapse::Const(multiset)
 }
diff --git a/test/cargo-fuzz/mzcompose.py b/test/cargo-fuzz/mzcompose.py
index c68735c34e794..ea2710f0dd010 100644
--- a/test/cargo-fuzz/mzcompose.py
+++ b/test/cargo-fuzz/mzcompose.py
@@ -55,10 +55,14 @@
 # The highest-yield targets, the ones that keep surfacing bugs deep into a run,
 # or that guard a bug-prone path / actively-developed subsystem where a find
 # would be catastrophic. `--profile fruitful` restricts the run to these, which
-# is the right focus for the long (24h) release-qualification run that should
-# spend its cores where bugs still hide. Substring-matched against
+# points a short run at the code where bugs still hide. Substring-matched against
 # `crate::target`, like the positional `filters`.
 #
+# NOTE: This is *not* what release qualification runs. That run passes
+# `--profile all`. A target nobody ever runs is a target whose oracle can quietly
+# stop asserting anything, and the 24h budget is the only place the low-yield
+# targets get exercised at all.
+#
 # This set is pruned by productivity. Targets over well-tested, stable code that
 # fuzz clean round after round (the arithmetic/range oracles, internal
 # encode/decode round-trips, simple jsonb access) are dropped, since they've
@@ -78,10 +82,18 @@
     "strconv_parse_timestamptz",
     "strconv_parse_date",
     "strconv_parse_time",
+    "strconv_parse_interval",
     "strconv_parse_bytes",
     "strconv_parse_uuid",
+    # The two durable-state decoders (rollup = full snapshot, state diff =
+    # incremental). Both are read from blob/consensus on every state load, and a
+    # decode panic there makes the shard unloadable, so they stay paired here.
     "rollup_proto_roundtrip",
+    "state_diff_proto_roundtrip",
     "copy_decode",
+    # The pgwire frontend decoder and the pre-auth SASL/password grammars behind
+    # it, reachable by any client that can open a socket.
+    "codec_decode",
     "protobuf_decode_fuzzed_schema",
     "json_encode",
     "avro_decode_fuzzed_schema",
@@ -332,9 +344,16 @@ class FuzzRunner:
     fail_fast: bool
     triple: str = ""
     # None => don't pass --sanitizer (use cargo-fuzz's default, i.e. ASan).
-    # The CLI defaults this to "none" (see below): our targets find panics /
-    # round-trip drifts, not memory-corruption bugs, so ASan adds no detection
-    # power here but ~2-3x slowdown. Pass --sanitizer=address to opt back in.
+    # The CLI defaults this to "none" (see below): our targets mostly find
+    # panics / round-trip drifts, and ASan costs a ~2-3x slowdown. Pass
+    # --sanitizer=address to opt back in.
+    #
+    # NOTE: a few targets do reach memory-unsafe code through FFI, e.g.
+    # mz-repr's `ProtoNumeric` decode calling libdecnumber's unchecked
+    # `decPackedToNumber`. ASan would not report those writes even when
+    # enabled: cargo-fuzz instruments via RUSTFLAGS only, while the C is built
+    # by the `cc` crate, so covering it needs CFLAGS=-fsanitize=address too.
+    # Bound such input in the decoder rather than relying on a sanitizer.
     sanitizer: str | None = None
     wall_budget: int = 0
     minimize: bool = True
@@ -1045,8 +1064,8 @@ def workflow_default(c: Composition, parser: WorkflowArgumentParser) -> None:
         help="`fruitful` restricts the run to the historically high-yield "
         "targets (see FRUITFUL): the SQL-parser round-trip oracles and the rich "
         "hand-written PG parsers/decoders that keep finding bugs, ideal for a "
-        "long local run. `all` (default) runs every target. A `filters` list "
-        "narrows further within the profile.",
+        "short local run. `all` (default, and what release qualification uses) "
+        "runs every target. A `filters` list narrows further within the profile.",
     )
     parser.add_argument(
         "filters",