Skip to content

fix(core): parse rejections say SQL statement, not schema DDL (Story 20.4) - #264

Merged
fupelaqu merged 1 commit into
mainfrom
feature/20.4
Sep 2, 2026
Merged

fix(core): parse rejections say SQL statement, not schema DDL (Story 20.4)#264
fupelaqu merged 1 commit into
mainfrom
feature/20.4

Conversation

@fupelaqu

@fupelaqu fupelaqu commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Closes #262

Root fix in core for SOFTNETWORK-APP/softclient4es-jdbc#35 (that issue is closed later by the
jdbc core-bump PR — lead hand-off). Story 20.4, Epic 20 (BI venue enablement), train 0.22.0.

What changed

GatewayApi.run(sql: String) labelled every parse rejection — a mistyped SELECT included —
as Error parsing schema DDL statement: <reason> with operation = Some("schema"). The JDBC
driver puts error.message verbatim into SQLException, so BI analysts were sent to DDL docs for
DQL typos.

Both rejection routes (the Left(ParserError) branch and the thrown-parser route, #250) now go
through one shared builder, GatewayApi.parseRejectionMessage:

Error parsing SQL statement [<statement excerpt>]: <parser reason>

with operation = Some("sql") (the value the empty-query branch already used). Properties, each
pinned by a test:

  • Bounded end to end: each half is single-line and capped at 200 chars — head + ... + tail,
    so the discriminating end of a long Tableau-generated statement (or a statement behind a
    /* app=... */ banner) survives; a validate() reason embedding a whole AST/SCRIPT body cannot
    bury the diagnosis.
  • Safe to render: control characters and line separators collapse to one space (\s misses
    ESC/NUL/BEL — \p{Cntrl} + U+0085/U+2028/U+2029 added), cuts are surrogate-safe. A crafted
    statement cannot forge a log record or emit an ANSI escape.
  • The reason is never empty, and the thrown route never relays attempt's
    Operation failed: null (falls back to the cause's class name).
  • statusCode untouched on both routes: 400 on the normal path; still None on the thrown
    route (an internal fault must not be asserted to be a client-side 400 — the honest 400 arrives
    with Parser.apply can throw ValidationError instead of returning Left on parenthesized WHERE clauses #250).
  • The thrown route now logs the rejection (it logged nothing before).

Files: GatewayApi.scala (2 branch bodies + helpers in the companion object), new
ParseRejectionMessageSpec (20 Docker-free tests via NopeClientApi), testkit template
GatewayApiIntegrationSpec (2 assertions updated + DQL case + batch position-2 case),
documentation/client/gateway.md. The 35 other Some("schema") sites in the file are executor
operations and are deliberately untouched, as are run(statement: Statement) and
PipelineApi (accurate labels).

Verification

  • sbt "+ core/compile", sbt "++ 2.12.20 core/Test/compile", sbt "core/test" (841/841),
    sbt "+ softclient4es-core-testkit/compile", scalafmtAll clean, headerCheck green.
  • Live ES 8.18 (Docker, sbt17): JavaClientGatewayApiSpec 45/45 (incl. the new DQL and batch
    position-2 cases), JavaClient8ReplGatewayIntegrationSpec 56/56 — the two pre-existing
    include("Error parsing") assertions pass unchanged (the stem was kept on purpose).
  • Sweep: no other producer/consumer of the old string in elasticsql, jdbc, arrow, extensions, or
    web.

Release notes (0.22.0)

Changed — SQL parse-rejection messages. A statement the SQL engine cannot parse is now
reported as Error parsing SQL statement [<statement>]: <reason> with operation = "sql",
instead of Error parsing schema DDL statement: <reason> with operation = "schema". The old
wording claimed every rejection — a mistyped SELECT included — was a schema DDL error, and BI
tools surface that text verbatim to end users (jdbc#35). The message now quotes the statement
that was rejected alongside the parser's reason. Both halves are rendered on one line and capped
at 200 characters; a longer one keeps its head and its tail with ... between them.
statusCode is unchanged (400 on the normal rejection path; still absent on the thrown-parser
route, #250). Anything matching on the old prefix must be updated.

Also changed — what a parse rejection writes to the log. The ERROR line a rejection logs now
contains up to 200 characters of the submitted statement, and the thrown-parser route logs at
all for the first time. Operators who run below INFO specifically to keep SQL text out of their
logs should know that a parse rejection is now an exception to that. The text is single-lined
and control-character-stripped, so it cannot forge a log record.

🤖 Generated with Claude Code

…20.4)

Both rejection routes of GatewayApi.run(sql) — the Left(ParserError) branch and
the thrown-parser route (#250) — now emit
'Error parsing SQL statement [<statement excerpt>]: <parser reason>' with
operation = Some("sql") through one shared builder
(GatewayApi.parseRejectionMessage). Each half is rendered on a single line and
capped at 200 characters (head + '...' + tail, so the discriminating end of a
long generated statement survives); control characters and line separators are
collapsed so the text cannot forge a log record or emit an ANSI escape; cuts
are surrogate-safe. The thrown route never relays attempt's 'Operation failed:
null' (falls back to the cause's class name) and now logs the rejection.
statusCode is unchanged on both routes (400 / None).

Testkit template gains a DQL rejection case and a batch position-2 case;
documentation/client/gateway.md shows the new shape. Verified live on ES 8.18:
JavaClientGatewayApiSpec 45/45, JavaClient8ReplGatewayIntegrationSpec 56/56
with the two existing 'Error parsing' assertions untouched.

Closes #262

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Every parse rejection from GatewayApi.run is labelled a schema DDL error

1 participant