Skip to content

fix(gateway): keep the routed error's SQLSTATE class - #360

Merged
farhan-syah merged 2 commits into
mainfrom
fix/gateway-sqlstate-agreement
Sep 23, 2026
Merged

farhan-syah merged 2 commits into
mainfrom
fix/gateway-sqlstate-agreement

Conversation

@EnRaiha

@EnRaiha EnRaiha commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Why

GatewayErrorMap::to_pgwire restated 14 of the direct mapper's arms, and its own catch-all sent every variant beyond them to XX000. The direct table classifies 26 of those, so the same fault reached the client with a different class depending on whether the statement crossed the gateway: a constraint violation as XX000 instead of 23505, a rate rejection, an undefined column, a write conflict, memory exhaustion, a fan-out refusal, and more.

Mechanical evidence: crate::Error has 101 variants, the direct mapper 36 explicit arms, the gateway 14. The red run failed with left: "XX000", right: "23505" on RejectedConstraint; the same test passes after delegation.

What

  • to_pgwire delegates to error_to_sqlstate: one table answers for the direct and the routed path.
  • New agreement test: for 24 classified variants, the gateway class equals the direct class and the direct class is not XX000.
  • RetryableSchemaChanged keeps INTERNAL_ERROR on both paths; its message now comes from the variant's Display, still naming the descriptor.
  • When the mapper-completion PR (fix(pgwire): complete the shaper error classification and keep internal detail in the log #359) merges, the gateway inherits the completed table with no further change here.

How to test

  • cargo test -p nodedb --lib gateway::error_map — 30 pass (agreement + surface tests).
  • cargo test -p nodedb --lib error_map — 32 pass.
  • cargo clippy -p nodedb --lib -- -D warnings — clean · cargo fmt --check — clean.
  • Preflight C1-C7 pass against origin/main.

Notes

  • Still open: 65 of the 101 variants have no explicit arm on either path, so both answer XX000 for them. Wiring the direct fallback to classify() plus the numeric table is the follow-up; the gateway now inherits whatever the direct table answers.
  • The two code vocabularies still disagree on TYPE_MISMATCH (42804 vs 42846) and RATE_EXCEEDED (53300 vs 54001); tracked as stage S3 of the classification plan.

Fixes #362

Tradeoffs

Tradeoff: the gateway keeps no arms of its own — a gateway-only class now belongs in the shared table, not in this mapper.

Copilot AI lite review requested due to automatic review settings September 20, 2026 13:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@EnRaiha EnRaiha added the run-ci Opt this PR into the full test suite; re-add to force a re-run label Sep 20, 2026

@farhan-syah farhan-syah left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delegation verified: every arm the gateway dropped has an identical-class arm in error_to_sqlstate (NotLeader, NoLeader, DeadlineExceeded, CrossCollectionNotColocated, RemoteTyped, DataPlane, BadRequest, PlanError, CollectionNotFound). error_map lib tests 32 pass, clippy --lib -D warnings clean, fmt clean.

Blocker: an unchecked consumer of the changed message.

RetryableSchemaChanged on the routed path rendered schema changed during execution (X); please retry. It now renders the variant's Display, retryable schema change on X, still XX000. Three cluster tests classify a transient retry by that text:

  • nodedb-cluster-tests/tests/common_suite/cases/cross_node_pk_lookup.rs:71-72
  • nodedb-cluster-tests/tests/common_suite/cases/cross_node_pk_write.rs:85-86
  • nodedb-cluster-tests/tests/common_suite/cases/install_snapshot_e2e_cluster.rs:94-95

Their predicate is msg.contains("schema changed during execution") || msg.contains("please retry"). Neither matches the new text and XX000 matches nothing, so a lease conflict during those tests becomes a hard failure. CI here runs stage 1 only, so it never surfaced.

Direction: one message in one place. Put the retry wording in the variant's #[error(...)] (nodedb/src/error/types.rs:350) so the direct path, this mapper, and error_map/http.rs:26-29 (which duplicates the same literal) all render it from Display. The test predicates then match unchanged.

Should-fix: three doc comments narrate the pre-change state (inline).

After the change, rebuild the commits rather than appending fix-up commits on top.

Comment thread nodedb/src/control/gateway/error_map/pgwire.rs
Comment on lines +11 to +15
/// One table answers for the direct and the routed path. This mapper's 14
/// arms restated the direct table, but its own catch-all sent the 26
/// variants the direct table classifies and it did not list to `XX000`.
/// A client must not see a different class because a statement crossed the
/// gateway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Narrates the pre-change state ("arms restated ... catch-all sent the 26 variants"). Comments state what is: one table answers for the direct and the routed path, so a client sees the same class either way.

Comment thread nodedb/src/control/gateway/error_map/pgwire.rs Outdated
Comment thread nodedb/src/control/gateway/error_map/pgwire.rs Outdated
@EnRaiha

EnRaiha commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

All four addressed in 395a0594e:

  • Blocker (:18): the wording now lives in the variant's #[error("schema changed during execution ({descriptor}); please retry")], so every path renders it from Display and the three cluster tests that key on that text are satisfied. The crash harness (nodedb/tests/crash_harness/pgwire.rs) matched the previous Display text; its matcher and doc now read the same Display substring, and the doc no longer claims a fixed SQLSTATE bucket.
  • :15 / :44 / :79: comments restated as the invariant — one table answers for the direct and the routed path, so a client sees the same class and message either way; the two test docs say what they pin, with no pre-change narration.

Evidence: cargo test -p nodedb --lib gateway::error_map — 30 pass; cargo fmt --check clean.

@EnRaiha
EnRaiha requested a review from farhan-syah September 21, 2026 05:48

@farhan-syah farhan-syah left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2, 395a0594e re-verified: error_map 32 pass, clippy --all-targets -D warnings clean (covers the crash-harness edit), fmt clean.

Round-1 point Landed
Blocker: wording at the variant's #[error] Yes — types.rs:351. The cluster-test predicates match.
Consumer trace Yes, and further than asked: the crash-harness matcher keyed on the old Display text and is updated.
Three history comments Yes, all restated as invariants.

Still open — two copies of the wording remain, one now divergent. Neither file is in the diff, so no inline anchor:

  • nodedb/src/control/gateway/error_map/http.rs:28 — a hand-written format! of the same text. Round 1 asked that every path render from Display; this one still does not.
  • nodedb/src/error_classify.rs:181 — plan_error(format!("retryable schema change on {descriptor}")) carries the old wording. It matched Display before this PR and diverges after it. The classify path reaches clients through the bridge, so one path will say one thing and the other path another.

Both become err.to_string(). Then the variant's #[error] is the only source, which is what the new doc line on it claims.

After the change, rebuild the commits rather than appending fix-up commits on top.

@EnRaiha
EnRaiha force-pushed the fix/gateway-sqlstate-agreement branch from 395a059 to 3fd20e7 Compare September 21, 2026 10:59
@EnRaiha

EnRaiha commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Round 2 addressed in the rebuilt commit (3fd20e779, branch force-pushed, no fix-up on top).

Round-2 point Landed
http.rs:28 hand-written copy Yes — (503, err.to_string())
error_classify.rs:181 divergent copy Yes — plan_error(e.to_string())

Auditing the same invariant found three more consumers, all now rendering from Display:

  • error_map/native.rs:29 hand-wrote schema changed ({descriptor}) → err.to_string() (the code still carries the class).
  • error_map/resp.rs:28 hand-wrote ERR schema changed ({descriptor}); please retry → format!("ERR {err}"), same prefix convention as the neighbouring NOPERM/TIMEOUT arms.
  • nodedb-cluster-tests/.../descriptor_lease_planner_integration.rs:126 keyed on the old Display text ("retryable schema change" / "plan error"), so it stopped matching the new wording; the predicate now keys on "schema changed during execution" / "please retry", same shape as the other three cluster tests.

The variant's #[error(...)] is now the only source of this wording.

Local evidence: error_map + error_classify lib tests 35 pass; full cargo test -p nodedb --lib 6979 pass; nextest -p nodedb -E 'test(descriptor_fence)' 12 pass; clippy -p nodedb --all-targets and -p nodedb-cluster-tests --all-targets clean with -D warnings; fmt clean. The drain cluster test itself was not run locally (3-node harness).

GatewayErrorMap::to_pgwire restated 14 of the direct mapper's arms and sent
every variant beyond them to XX000. The direct table classifies 26 of those,
so a routed query reported an internal fault where the direct path reported
the real class: constraint 23505, rate and quota rejections, undefined
column/function/object, write conflict, memory exhaustion, fan-out, and the
rest. The mapper now delegates to error_to_sqlstate, and the agreement test
pins the contract for 24 of them.
…play

The delegation moved this condition's message to the direct table's Display
text, and three cluster tests key on 'schema changed during execution'. The
wording now lives in the #[error(...)] attribute so every path renders it from
Display: the HTTP, RESP, and native gateway maps, the classify table, the
crash harness, and the descriptor-lease integration test all read that same
text. The gateway comments state the invariant (one table answers both paths)
instead of the pre-change layout.
@EnRaiha
EnRaiha force-pushed the fix/gateway-sqlstate-agreement branch from 3fd20e7 to d854b04 Compare September 21, 2026 12:11
@EnRaiha

EnRaiha commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main @ 4664c8213 (d854b04a2). One conflict in resp.rs: main gained the TypeMismatch → WRONGTYPE arm; that arm is kept and the schema-change arm renders format!("ERR {err}"). error/types.rs does not grow past its current C5 count (604/604).

Rebased-tree evidence: full cargo test -p nodedb --lib 7119 pass; error_map + error_classify 36 pass; nextest -p nodedb -E 'test(descriptor_fence)' 12 pass; fmt clean; clippy -p nodedb --all-targets clean apart from a pre-existing nonminimal_bool at nodedb/src/control/planner/sql_plan_convert/dml/vector_primary.rs:161 (byte-identical to main, added by today's upstream pushes, outside this diff).

@farhan-syah
farhan-syah merged commit 1ff3551 into main Sep 23, 2026
6 checks passed
@farhan-syah
farhan-syah deleted the fix/gateway-sqlstate-agreement branch September 23, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-ci Opt this PR into the full test suite; re-add to force a re-run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gateway pgwire mapper flattens 26 classified error variants to XX000

3 participants