Skip to content

fix(engine) #5628: classify client errors on the postgres, redis, mongodb and graphql wire paths - #5700

Merged
robfrank merged 5 commits into
mainfrom
fix/5628-arithmetic-client-error-classification
Aug 1, 2026
Merged

robfrank merged 5 commits into
mainfrom
fix/5628-arithmetic-client-error-classification

Conversation

@robfrank

@robfrank robfrank commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Closes #5628

Summary

#5602 (PR #5612) gave arithmetic failures - 64-bit overflow, division and modulo by zero - their own type so a wire layer could report them as the client errors they are, but only HTTP (400) and Bolt (Neo.ClientError.Statement.ArithmeticError) were wired. postgresw, mongodbw and redisw ended their query paths in a single generic arm, so a caller who divided by zero, hit a unique index or lost an optimistic-concurrency race received a code meaning the server had broken - and, for a conflict, one no driver retries. This adds com.arcadedb.exception.ErrorCategory, which walks the cause chain once with CauseChain so every protocol answers the classification question the same way, and gives each module only the table that translates a category into its own vocabulary. graphql had a related defect of its own: it rewrapped every failure raised by the statement a directive delegates to as a CommandParsingException, reporting a runtime error as invalid syntax; a failure the engine already classified now keeps that classification.

ErrorCategory Postgres SQLSTATE Redis RESP prefix MongoDB code
RETRY 40001 serialization_failure TRYAGAIN 112 WriteConflict
ARITHMETIC 22012 division_by_zero / 22003 numeric_value_out_of_range ERR 2 BadValue
DUPLICATED_KEY 23505 unique_violation ERR 11000 DuplicateKey
NOT_FOUND 02000 no_data ERR (uncoded)
SECURITY 42501 insufficient_privilege NOPERM 13 Unauthorized
VALIDATION 22023 invalid_parameter_value ERR 2 BadValue
PARSING 42601 syntax_error ERR 9 FailedToParse
TIMEOUT 57014 query_canceled ERR 50 MaxTimeMSExpired
SERVER XX000 internal_error ERR (uncoded)

RETRY is decided before ARITHMETIC, matching the precedence BoltNetworkExecutor.classifyExecutionError already documents: a chain carrying both must keep the transient classification, because that is the one a driver acts on.

Two deliberate non-changes:

  • HTTP and Bolt keep the ladders they already had. Both predate this enum, encode protocol-specific ordering it does not model, and are pinned by the regression suites of several shipped issues. ErrorCategory's javadoc records this so the next person does not assume it is the single source.
  • gremlin needs nothing. GremlinQueryEngine.command already passes CommandExecutionException through, so an ArithmeticErrorException survives to the HTTP layer intact. Confirmed by the gremlin-directive test in the graphql suite.

Redis error replies also gain a message that cannot end the reply early: a RESP simple error is one line, and an embedded CR/LF would desynchronise the connection. The -Command not found reply is left byte-identical, since an existing test pins it.

Test plan

  • ErrorCategoryTest (engine, 10 tests) - every category, the three wrapping shapes the engine produces, RETRY beating ARITHMETIC, the CommandParsingException wrapper not hiding an arithmetic error, and a cyclic cause chain terminating.
  • PostgresErrorClassificationTest (8 tests) - the division/overflow SQLSTATE split, an unrecognised arithmetic message still landing in class 22, a wrapped arithmetic error, conflict precedence, and a genuine server fault still reporting XX000.
  • RedisErrorClassificationTest (5 tests) - TRYAGAIN/NOPERM/ERR, an empty message still naming the failure, and a multi-line message flattened to one line.
  • MongoDBErrorClassificationTest (5 tests) - each coded category, a wrapped arithmetic error, and server faults staying uncoded rather than being given a misdirecting client-error code.
  • GraphQLExecutionErrorClassificationTest (4 tests) - an execution failure no longer arriving as a syntax error, an arithmetic error keeping its own type through a directive, a genuine GraphQL syntax error still being a parsing error, and a working directive still returning rows.
  • Fail-first verified: with the graphql passthrough reverted, anExecutionFailureIsNotReportedAsASyntaxError fails while the two guard tests still pass.
  • Regression suites green: graphql 30/30 (includes the gremlin and cypher directive tests), mongodbw 78/78, postgresw unit 258/258, redisw RedisWTest 13/13 (the RESP wire path), bolt BoltErrorClassificationTest 13/13, engine com.arcadedb.exception 42/42.
  • RedisQueryLanguageTest and the server-backed ITs are unverifiable on this machine - a local ArcadeDB holds ports 2480-2482, so they connect to the wrong server. Confirmed identical failures on unmodified main. CI is the gate for these.

🤖 Generated with Claude Code

…godb and graphql wire paths

#5602 gave arithmetic failures their own type so a wire layer could report them as
client errors, but only HTTP and Bolt were wired. The other protocols reported every
execution failure through one generic arm, so a caller who divided by zero, hit a
unique index or lost an optimistic-concurrency race got a code meaning the server
had broken.

Adds com.arcadedb.exception.ErrorCategory, which walks the cause chain once with
CauseChain so every protocol classifies the same way, and gives each module the
table that translates a category into its own vocabulary: a SQLSTATE for Postgres,
a RESP kind word for Redis, an error code for MongoDB. RETRY is decided before
ARITHMETIC, matching the precedence BoltNetworkExecutor already documents.

GraphQL additionally rewrapped every failure raised by the statement a directive
delegates to as a CommandParsingException, reporting runtime errors as invalid
syntax; a failure the engine already classified now keeps that classification.

HTTP and Bolt keep the ladders they already had.
@mergify

mergify Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@codacy-production

codacy-production Bot commented Aug 1, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 38 complexity

Metric Results
Complexity 38

View in Codacy

🟢 Coverage 86.59% diff coverage · -6.84% coverage variation

Metric Results
Coverage variation ✅ -6.84% coverage variation
Diff coverage ✅ 86.59% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (83448ea) 150078 114508 76.30%
Head commit (aa10fad) 182318 (+32240) 126636 (+12128) 69.46% (-6.84%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#5700) 82 71 86.59%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review: wire-protocol client-error classification (issue 5628)

Nicely scoped change with genuinely excellent documentation and test coverage. The core idea - one shared ErrorCategory classifier in the engine that walks the cause chain once, with each wire module owning only the translation table into its own vocabulary - is the right factoring, and the reasoning captured in the javadoc (precedence, cycle-safety, why HTTP/Bolt are left alone) is a pleasure to read. A few things worth a look before merge.

Substantive: SchemaException falls through to SERVER

SchemaException is not in the ErrorCategory.of ladder, so it lands in the final SERVER arm. But it is the class used for a large family of caller mistakes - SELECT FROM NonExistentType, an unknown bucket, a missing materialized view (LocalSchema throws it in many such spots). The result is that SELECT FROM doesNotExist reports as Postgres XX000 internal_error / an uncoded Mongo server fault - the exact "the server broke" misdirection this PR sets out to remove, just for a different (and very common) client error.

The GraphQL test anExecutionFailureIsNotReportedAsASyntaxError actually exercises this path (missingTypeBooks throws SchemaException) but only asserts isNotEqualTo(PARSING), so it passes while the real classification is SERVER. Given the PR own framing ("how does each wire protocol map ArcadeDB exception hierarchy"), mapping SchemaException seems in scope - e.g. Postgres 42P01 undefined_table / 42704 undefined_object, Mongo FailedToParse/NamespaceNotFound, or at minimum VALIDATION. Worth at least a decision recorded in the javadoc if it is a deliberate omission.

Consideration: IllegalArgumentException -> VALIDATION is broad

IllegalArgumentException (and its subclass NumberFormatException) is thrown both for bad user input and for internal invariant violations across the engine. Blanket-classifying it as a client VALIDATION error risks relabelling a genuine server bug as the caller fault, which is the mirror image of the problem being fixed. It is a defensible default, but unlike the other entries it is not self-evidently a client error - consider noting the tradeoff in the javadoc so the next person understands it is intentional.

Minor

  • Arithmetic split via message text (message.contains("by zero") -> 22012 else 22003): fragile, but you already acknowledge it and the class-22 verdict is stable regardless, so this is fine as-is.
  • GraphQL passthrough drops context: broadening the catch to ArcadeDBException means the helpful Error on executing GraphQL query <name> wrapper and the line-numbered query dump are no longer attached to engine-classified failures. Reasonable tradeoff for keeping the classification, just flagging the small debuggability cost.
  • Test altitude: Postgres/Mongo classifications are unit-tested at the sqlStateFor/wireException helper level, not through an actual wire round-trip (Redis has RedisWTest covering the RESP path). You already note the server-backed ITs are CI-gated; one end-to-end IT per protocol asserting the SQLSTATE/code actually reaches a driver would close the loop, but this is not blocking.

Things I verified and liked

  • Precedence RETRY > ARITHMETIC is correct and matches Bolt; DuplicatedKeyException correctly does not extend NeedRetryException, so it stays out of RETRY.
  • SECURITY -> java.lang.SecurityException is correct: LocalDatabase.checkPermissionsOn* throws exactly that on query-time permission denials (not the connection-time ServerSecurityException), so the mapping is reachable.
  • ArithmeticErrorException is tested before its CommandExecutionException supertype and before CommandParsingException - the ordering the javadoc calls out - and the cyclic-chain / depth-cap handling in CauseChain is sound.
  • The Redis CR/LF flattening in respErrorMessage is a real correctness fix (an embedded newline in a RESP simple error would desynchronise the connection), and prepending ERR/TRYAGAIN/NOPERM improves RESP compliance while leaving the pinned -Command not found reply byte-identical.

Overall this is high-quality work; the only item I would want addressed before merge is the SchemaException gap (or an explicit note that it is intentionally left as SERVER).

Reviewed by Claude Code.

…tegory)

Claude's review found that SchemaException fell through to SERVER, so
SELECT FROM NonExistentType - the most common caller mistake there is -
reported as Postgres XX000 and an uncoded MongoDB server fault: the same
misdirection this change exists to remove, for a different exception.

Adds a SCHEMA category mapping to Postgres 42P01 undefined_table and
MongoDB 26 NamespaceNotFound. The GraphQL test asserted only
isNotEqualTo(PARSING), which passed while the classification was still
SERVER; it now asserts the exact category.

Also records in the javadoc why IllegalArgumentException is mapped to
VALIDATION despite being raised for internal invariant violations too:
the HTTP handler has answered it with 400 since long before this enum.
@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review: wire-protocol client-error classification (#5700)

Solid, well-scoped change. Centralizing the "whose fault is it" question in ErrorCategory and giving each protocol only its own translation table is the right shape, and the write-up on the two deliberate non-changes (HTTP/Bolt keep their ladders, gremlin needs nothing) is genuinely helpful. The CauseChain traversal being depth-capped against cyclic initCause chains, and the RETRY-before-ARITHMETIC precedence matching Bolt, are both correct and tested. Coverage is good: unit tests per protocol plus the shared contract test, and the fail-first check on the graphql passthrough is a nice touch.

A few things worth a look:

1. Postgres bypasses the ARITHMETIC-before-PARSING precedence it documents. In PostgresNetworkExecutor both execution catch blocks (lines ~462 and ~561) catch CommandParsingException before the generic arm, and hardcode 42601:

} catch (final CommandParsingException e) {
  ...writeError(..., "42601");        // never reaches sqlStateFor()
} catch (final Exception e) {
  ...writeError(..., sqlStateFor(e)); // ErrorCategory ordering only applies here
}

The whole point of ordering ARITHMETIC ahead of PARSING in ErrorCategory.of() is that a CommandParsingException wrapping an arithmetic (or duplicated-key, or retryable) error should still classify by the inner cause. On the Postgres path that ordering is effectively dead: any failure that arrives wrapped in CommandParsingException reports as 42601 syntax_error regardless. Today the Postgres path executes an already-parsed Statement, so execution errors are not wrapped that way and it is latent rather than live - but it is an inconsistency with the stated design and a footgun the day the engine wraps an execution failure in CommandParsingException (as the query engines already do elsewhere, which is exactly why graphql needed the fix in this PR). Routing CommandParsingException through sqlStateFor() too would keep genuine parse errors at 42601 (via the PARSING arm) while letting a wrapped arithmetic error win, so all protocols would then agree.

2. ErrorCategory.isClientError() is only referenced from ErrorCategoryTest. No production caller. Either wire it into a handler (e.g. to pick a log severity, or to decide whether to setErrorInTx()) or drop it, so it does not read as an API that is actually in use.

3. Postgres NOT_FOUND -> 02000 (no_data). SQLSTATE class 02 is a completion/warning class, not an error class. Most drivers surface any ErrorResponse as an exception regardless, so this is unlikely to misbehave in practice, but P0002 no_data_found (or a class-22 code) carries the client-error verdict more honestly than a "no data" completion code. Minor.

4. Behavior change to confirm at the HTTP layer. GraphQL now propagates the underlying ArcadeDBException from a directive-delegated statement instead of rewrapping every failure as CommandParsingException. That is the fix, and the reasoning is right (a runtime error is not a malformed document), but it does change what the graphql HTTP handler sees: a SchemaException/RecordNotFoundException that used to arrive as a parsing error now arrives as itself. The 30/30 graphql suite covers this; just flagging that the HTTP status for those categories now depends on the HTTP ladder rather than a blanket 400.

Nit: the SECURITY mapping relies on the engine throwing java.lang.SecurityException (which LocalDatabase.checkPermissionsOn* does), not ServerSecurityException (which extends ServerException, not SecurityException). That is correct for the query-reachable permission check, but a one-line comment on ErrorCategory.SECURITY noting which SecurityException it targets would save the next reader the same lookup.

None of these block the change - (1) is the one I would most like addressed before merge, the rest are polish. Nice work on the docs and the exhaustive test matrix.

…ow the graphql passthrough

Review cycle 2 found that both Postgres execution arms caught
CommandParsingException first and hardcoded 42601, which made the
ARITHMETIC-before-PARSING ordering in ErrorCategory dead on that path.
All four arms now route through sqlStateFor: genuine parse errors still
report 42601 via the PARSING arm, while a wrapped arithmetic, conflict or
schema error wins.

The review also flagged that propagating the underlying exception from a
GraphQL directive changes what the HTTP handler sees. It does, and the
broad ArcadeDBException passthrough regressed the unknown-type case from
400 to 500, because SchemaException has no arm in that ladder. Narrowed
the passthrough to CommandParsingException | CommandExecutionException,
which the ladder does model. SchemaException is deliberately left alone:
the class also carries genuine server faults (issue #4122), so mapping it
to 400 would relabel a server bug as the caller's fault. Recorded as a
follow-up instead.

Also drops isClientError(), which had no production caller, and changes
NOT_FOUND from 02000 to P0002 - SQLSTATE class 02 is a completion
condition, not an error class.
@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review: wire-protocol client-error classification (PR 5700)

Reviewed the full diff. This is a high-quality, well-scoped change. The single-point ErrorCategory classifier with per-protocol vocabulary tables is the right design, the ordering rationale is documented where it matters, and the test coverage (engine contract + one classification test per wire module + fail-first verification) is genuinely thorough. The CauseChain depth cap for cyclic chains, the RETRY-before-ARITHMETIC precedence, and the Redis CR/LF flattening are all correct and carefully justified.

A few observations, all minor - nothing blocking.

1. Postgres: message prefix can contradict the SQLSTATE (fragile coupling). In both execution arms the catch (CommandParsingException e) branch hardcodes the human text "Syntax error on executing query: ..." but now derives the code from sqlStateFor(e). Today only genuine parse errors reach this arm (arithmetic/execution failures are CommandExecutionException and land in the generic arm), so text and code agree. But the moment any path routes a CommandParsingException that wraps a non-parse cause through here, sqlStateFor can return e.g. 22012/23505 while the message still reads "Syntax error". Clients branch on SQLSTATE so it is functionally fine, but the text/code divergence is a latent trap. Consider deriving the message prefix from the category too, or a comment pinning the "only real parse errors reach this arm" assumption.

2. IllegalArgumentException -> VALIDATION now reaches Postgres/Redis/MongoDB, not just HTTP. The javadoc honestly records the HTTP-400 precedent for this mapping. Worth being explicit that the behavioral surface is now wider: the engine throws IllegalArgumentException/NumberFormatException for both bad caller input and internal invariant violations, so an internal bug can now surface to a Mongo client as BadValue or a Postgres client as 22023 invalid_parameter_value - i.e. a server fault relabeled as the caller mistake, the exact inversion this PR removes for other exceptions. Acceptable given the HTTP precedent and the documented tradeoff, but a conscious call rather than a free one.

3. Redis RETRY -> TRYAGAIN. TRYAGAIN is the closest RESP has, but in real Redis it is a cluster-mode error; several non-cluster client libraries will not auto-retry on it. Nothing better exists in RESP2, so this is fine - just flagging that the "a driver retries this" guarantee is weaker here than for Postgres 40001 / Bolt.

4. MongoDB hardcoded numeric codes. wireException uses literal 112/26/13/50 for WriteConflict/NamespaceNotFound/Unauthorized/MaxTimeMSExpired while other arms use the ErrorCode enum. Assuming the bundled de.bwaldvogel enum lacks those constants (no other usage in the module), the literals are reasonable - a brief "not in de.bwaldvogel ErrorCode" note would save the next reader the check.

Nits. ErrorCategory.of walks the cause chain up to ~10 times (once per contains), each capped at 32; only on error paths so latency is irrelevant, noting only that a single walk with an instanceof ladder would be equivalent if reused on a hotter path. NOT_FOUND moving 02000 -> P0002 (class 02 is a completion condition, not an error) is a correct catch.

Overall: solid, well-tested, and the deliberate non-changes (HTTP/Bolt ladders, gremlin passthrough, Schema-as-server-fault follow-up) are the right calls and are documented rather than silently assumed. Nice work.

Review cycle 3 raised no blocking items. Four documentation points:
the Postgres 'Syntax error' wording now records why it agrees with the
SQLSTATE today, IllegalArgumentException's widened surface is stated as
a conscious trade, Redis TRYAGAIN is marked as a weaker retry hint than
Postgres 40001, and MongoDB's four literal codes note that the bundled
de.bwaldvogel ErrorCode enum does not define them.

The suggestion to collapse ErrorCategory.of into a single cause-chain
walk is not taken: it is not equivalent. Classification is by category
priority, not by which type the walk meets first, so a chain whose
NeedRetryException sits below an ArithmeticErrorException must still
answer RETRY. Added categoryPriorityBeatsPositionInTheChain so the
javadoc claim has a test behind it.
@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review: client-error classification across the wire protocols (#5628)

Reviewed the full diff plus the surrounding code in ErrorCategory, CauseChain, the exception hierarchy, and each wire module. This is a genuinely high-quality change: one classifier walked once over the cause chain, each module owning only its own translation table. The design instinct (categorise in the engine, translate at the edge) is the right one, and the test suite is unusually careful - precedence, wrapping shapes, cyclic chains, and fail-first are all exercised. The embedded design doc and the reasoning captured in the javadoc are excellent for the next maintainer.

A few observations, none blocking.

Correctness - verified, no issues found

  • Confirmed DuplicatedKeyException extends ArcadeDBException (not NeedRetryException), so the RETRY-first arm does not swallow duplicate-key failures before the DUPLICATED_KEY arm - a real trap the ordering avoids. ConcurrentModificationException/LockTimeoutException/WALVersionGapException all extend NeedRetryException, so RETRY is correct for them.
  • ArithmeticErrorException extends CommandExecutionException and is tested before both PARSING and the generic fallthrough - the anArithmeticErrorIsNotDowngraded... test pins exactly this.
  • SecurityException resolves to java.lang.SecurityException (no shadowing class in com.arcadedb.exception), matching LocalDatabase.checkPermissionsOn*. Good that the javadoc calls out it is not ServerSecurityException.
  • TimeoutException is com.arcadedb.exception.TimeoutException, which is what the query timeout steps actually throw. Correct.
  • The MongoDB and Postgres switch expressions are exhaustive over all 10 categories, so a future category addition fails compilation rather than silently falling through - a nice safety property.

Points worth considering (non-blocking)

  1. MongoDB: a backend-assigned code can be dropped. wireException runs ErrorCategory.of(e), and a de.bwaldvogel MongoServerError thrown by the bundled backend (e.g. the 16459 at line 448) classifies as SERVER, so it is re-wrapped into a plain MongoServerException and its original code/codeName is lost. This is pre-existing (the old new MongoServerException(...) did the same) and the insert path at 459-470 handles its own MongoServerError directly, so it may be unreachable in practice - but a one-line guard at the top of wireException (if (e instanceof MongoServerError me) return me;) would preserve any code the backend already set, at zero cost. Optional.

  2. Redis wire format is now client-visible-different. Replies went from -<message> to -ERR <message> (and -TRYAGAIN/-NOPERM). This is more correct - real Redis always prefixes an error code - but it is a wire-visible change, and a client doing exact-string matching on the old bare message would break. RedisQueryLanguageTest and the server ITs are the ones that would catch this and are CI-gated per the PR notes; worth making sure those go green before merge.

  3. Postgres message/SQLSTATE can disagree. In the catch (CommandParsingException e) arms the human text stays "Syntax error on executing query: ..." while the code now comes from sqlStateFor, so a wrapped non-parse cause would emit a 22012/40001 code under a "Syntax error" message. You have documented this and it is latent today (only genuine parse errors reach that arm). Deriving the message lead-in from the category too would close it - but the code is what drivers branch on, so this is cosmetic.

  4. IllegalArgumentException -> VALIDATION now spans every protocol. Already acknowledged in the javadoc as a conscious trade: an internal invariant violation raised as an IllegalArgumentException will now reach a MongoDB client as BadValue and a Postgres one as 22023. Consistency with the long-standing HTTP 400 is a fair justification; just flagging that the blast radius is wider than HTTP now.

  5. Deferred end-to-end wire ITs. The Postgres SQLSTATE and MongoDB code paths are unit-tested at the helper level only; there is no test asserting the value actually reaches a driver over the wire (Redis has this via RedisWTest). The PR already tracks this under Deferred - reasonable given the local port-conflict constraint, but worth landing follow-up ITs so these codes do not silently regress.

Style / conventions

Consistent with the codebase: final on params/locals, imported names rather than FQNs, assertThat(...) style, Apache headers, @author tags matching the house convention. No new dependencies. No stray System.out.

Nice work - the classifier is a good abstraction and the change removes a real, user-facing misdirection (client mistakes reported as server faults, and conflicts reported as non-retryable). My only genuine suggestion is the MongoDB MongoServerError passthrough (#1); the rest are notes.

Automated review by Claude. Verified against the repo's exception hierarchy and wire-module sources.

A de.bwaldvogel MongoServerError is not an ArcadeDBException, so it
classified as SERVER and wireException re-wrapped it into a plain
MongoServerException - dropping the code the client needs. It is now
returned untouched.

The 16459 insert case is caught locally and never reaches this path, but
the bundled aggregation code raises coded errors that do. Pre-existing
behaviour rather than a regression from this PR, fixed because the guard
is one line and loses nothing.
@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review: #5628 client-error classification across wire protocols

Thorough, well-structured piece of work. Centralising the "which category of failure is this?" question in a single ErrorCategory.of(...) that every wire module translates into its own vocabulary is the right shape, the cause-chain walk correctly fixes the "inspect only getCause()" bug the wrappers created, and the test coverage (unit-level per module plus the GraphQL integration cases) is genuinely good. The javadoc-as-rationale style makes the deliberate ordering (RETRY before ARITHMETIC before PARSING) easy to verify. Exhaustive switches with no default in the Postgres and MongoDB tables are a nice touch: a future category is a compile error rather than a silent XX000.

A few points worth discussing before merge, none of them blockers.

1. SchemaException is a client error on SQL wire paths but explicitly NOT on GraphQL/HTTP

This is the one real inconsistency. GraphQLSchema narrows its passthrough to CommandParsingException | CommandExecutionException precisely so SchemaException is NOT propagated, and the comment argues it "is not purely a caller error - Dictionary and TransactionManager raise it for genuine server faults (e.g. issue 4122) - so it cannot just be mapped to 400." But ErrorCategory.of maps SchemaException to SCHEMA, which is Postgres 42P01 undefined_table / MongoDB 26 NamespaceNotFound, i.e. a client error. So the exact failure judged unsafe to call a client error over HTTP is now reported as one over Postgres and MongoDB. In the rare case a SchemaException from Dictionary/TransactionManager surfaces on a query commit through those paths, it reads as 42P01 (caller fault) instead of XX000 (server fault) - the same misdirection this PR removes, in the opposite direction. The query-reachable case is overwhelmingly "unknown type" so the pragmatic choice is defensible, but the two paths disagree and the reasoning that justifies one contradicts the other. Worth either aligning them or noting the divergence in the SCHEMA javadoc.

2. IllegalArgumentException to VALIDATION widens the client-error surface

IllegalArgumentException (and its subclass NumberFormatException) is thrown from many internal invariant checks, not just bad-parameter validation. Anything internal failing with an IAE is now reported to the client as their VALIDATION / BadValue / 22023 error rather than a server fault. Acknowledged and pinned to HTTP-handler consistency, a reasonable call, but it does mean a class of server-side bugs gets relabelled as caller errors on these paths. The javadoc note is good; no change strictly required.

3. RETRY precedence is by category, not chain position

of() returns RETRY if a NeedRetryException appears anywhere in the chain, even beneath the real top-level failure. An unrelated, already-handled nested retry left in the cause chain of a genuinely non-retryable error would tell the driver to retry a non-idempotent op. This matches the precedence BoltNetworkExecutor already documents, so it is consistent, and categoryPriorityBeatsPositionInTheChain pins the intent - just flagging that the risk rides along with the design.

4. Redis reply format change is a (minor) wire-compat change

Error replies go from -message to -PREFIX message. More RESP-correct, and the CR/LF flattening in respErrorMessage is a good safety fix (an embedded newline really would desync the connection). But any client that string-matched the raw message body now sees a leading ERR / TRYAGAIN / NOPERM token. Low risk given RedisWTest stays green and the -Command not found reply is byte-identical; worth a release-notes line.

5. Minor

  • MongoDBDatabaseWrapper.wireException returns a pre-coded MongoServerError untouched (good catch in the final commit), but drops the contextual message and only inspects the top-level throwable, not a wrapped one. Acceptable, just noting the edges.
  • Test-coverage gap is honestly disclosed and deferred: no end-to-end IT asserts the Postgres SQLSTATE / MongoDB code actually reaches a driver over the wire (Redis has this via RedisWTest). Reasonable to defer, but that is exactly the layer where a translation-table typo would hide, so the follow-up IT is worth prioritising.

Verification note

I reviewed the final state across all 5 commits (the passthrough was correctly narrowed from ArcadeDBException to CommandParsingException | CommandExecutionException in cycle 2, with tests updated to match). I could not run the server-backed ITs here, so I am relying on the stated CI results for the wire-level paths.

Nice work overall - the classifier is clean, the precedence is deliberate and tested, and the two deliberate non-changes (HTTP/Bolt ladders, gremlin) are well justified.

@robfrank
robfrank merged commit a49b2a5 into main Aug 1, 2026
24 of 26 checks passed
@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.14634% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.52%. Comparing base (83448ea) to head (aa10fad).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
...com/arcadedb/postgres/PostgresNetworkExecutor.java 69.56% 5 Missing and 2 partials ⚠️
.../java/com/arcadedb/redis/RedisNetworkExecutor.java 66.66% 4 Missing ⚠️
...ava/com/arcadedb/mongo/MongoDBDatabaseWrapper.java 84.61% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5700      +/-   ##
==========================================
+ Coverage   67.49%   67.52%   +0.03%     
==========================================
  Files        1770     1772       +2     
  Lines      150078   150326     +248     
  Branches    31809    31882      +73     
==========================================
+ Hits       101290   101504     +214     
- Misses      35607    35618      +11     
- Partials    13181    13204      +23     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

robfrank added a commit that referenced this pull request Aug 14, 2026
…godb and graphql wire paths (#5700)

(cherry picked from commit a49b2a5)
@robfrank
robfrank deleted the fix/5628-arithmetic-client-error-classification branch August 26, 2026 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Misc] Arithmetic and client-error classification is wired for HTTP and Bolt only

1 participant