Skip to content

Fix query(): insert FORMAT before a trailing SETTINGS clause - #972

Open
polyglotAI-bot wants to merge 2 commits into
mainfrom
polyglot/js970-format-before-settings
Open

Fix query(): insert FORMAT before a trailing SETTINGS clause#972
polyglotAI-bot wants to merge 2 commits into
mainfrom
polyglot/js970-format-before-settings

Conversation

@polyglotAI-bot

Copy link
Copy Markdown
Collaborator

Description

Fixes #970.

client.query() builds the wire query via formatQuery(), which unconditionally appended FORMAT <format> to the end of the user-supplied SQL. When the query ends with a top-level SETTINGS ... clause, the client emitted:

<query> SETTINGS k = v
FORMAT JSON

ClickHouse servers whose parser requires SETTINGS to be the trailing clause of a statement (e.g. DESCRIBE / DESC format(...) on servers older than 24.x) reject this. Modern servers (24.x+) accept either order, which is why the bug is masked against a current server — but the client is still producing the wrong ordering. The well-formed query is ... FORMAT <format> SETTINGS ... (this same class broke clickhouse-connect, issue #211).

formatQuery() now detects a trailing top-level SETTINGS clause and inserts FORMAT right before it; when there is no such clause it appends FORMAT at the end exactly as before (byte-for-byte unchanged on the common path).

Changes

  • packages/client-common/src/client.ts
    • formatQuery(): if the (trimmed, semicolon-stripped) query ends with a trailing top-level SETTINGS clause, emit ... FORMAT <format> SETTINGS ...; otherwise append FORMAT at the end as before.
    • New internal trailingSettingsClauseIndex() scans the query with a small tokenizer that is aware of string literals (' / " / ` with backslash and doubled-quote escapes), dollar-quoted heredocs ($$...$$, $tag$...$tag$), line comments (--, #), block comments (/* */), and bracket depth. A SETTINGS keyword is treated as a clause only when it is at top level (bracket depth 0), outside strings/comments, a whole word, and immediately followed by a <name> = ... settings list — so SETTINGS inside a string/comment/subquery and a settings identifier/column are all left untouched.
    • Updated the query() JSDoc to describe the new placement.
  • Since the shared client-common code is bundled into both clients, added a ## Bug fixes entry under the unreleased # 1.24.0 heading in both packages/client-node/CHANGELOG.md and packages/client-web/CHANGELOG.md.

Test

packages/client-common/__tests__/unit/format_query.test.ts (new): a table-driven suite that captures the exact wire string client.query() sends (via a stub connection) and asserts FORMAT placement. It covers the fix (basic trailing SETTINGS, the issue's DESC format(...) reproduction, subquery-with-outer-SETTINGS, block-comment-then-real-clause, trailing semicolon, no-space =) and — as contrast cases that must keep today's append-at-end behavior — no SETTINGS, SETTINGS inside a string literal, a settings identifier, a subquery's own SETTINGS, --/# line comments, and a dollar-quoted heredoc containing SETTINGS-shaped text. The 7 injection cases fail on main and pass with the fix; the contrast cases pass both ways. Expected orderings were verified against a live ClickHouse server (all HTTP 200).

Verified in the devbox: full node unit suite green (419 passed), typecheck, lint (--max-warnings=0), and prettier all clean. (Web unit tests run in a real browser via Playwright, which isn't available in the build sandbox; the shared code is exercised by the node suite through the identical symlinked source, and client-web typechecks.)

Pre-PR validation gate

  • Deterministic repro confirmed (unit test fails on main, passes with the fix)
  • Root cause documented above
  • Fix targets the root cause (query-string assembly in formatQuery, the sole place FORMAT is appended)
  • Test fails without fix, passes with fix; contrast cases pin unchanged behavior
  • No existing tests weakened or removed; full node unit suite still green
  • Convention compliance verified per AGENTS.md (CHANGELOG in both client packages, prettier/lint/typecheck)

Notes

  • Not a breaking change: on modern servers both orderings already work (verified), and queries without a trailing SETTINGS clause are unaffected; queries that were failing on older servers now succeed.
  • Known limitation (kept out of scope): a comment placed between SETTINGS and its first setting name (e.g. SETTINGS /* c */ x = 1) is not recognized and falls back to the previous append-at-end behavior — no regression, just not newly fixed.

client.query() appended `FORMAT <format>` to the very end of the user's
SQL, so a query ending in a top-level `SETTINGS ...` clause was sent as
`... SETTINGS ... FORMAT ...`. ClickHouse servers that require SETTINGS
to be the trailing clause of a statement (e.g. DESCRIBE on < 24.x)
reject that ordering; modern servers accept either order, masking the
bug. formatQuery now detects a trailing top-level SETTINGS clause
(ignoring string literals, comments, subqueries, and dollar-quoted
heredocs) and inserts FORMAT before it, producing
`... FORMAT <format> SETTINGS ...`.

Fixes: #970
Copilot AI review requested due to automatic review settings July 29, 2026 13:28
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes client.query() SQL assembly so the client inserts FORMAT <format> before a trailing top-level SETTINGS clause (instead of always appending FORMAT at the very end), restoring compatibility with ClickHouse servers/statement forms that require SETTINGS to be the final clause.

Changes:

  • Updated formatQuery() to detect a trailing top-level SETTINGS clause and inject FORMAT immediately before it.
  • Added a unit test suite covering FORMAT placement across strings/comments/heredocs/subqueries and semicolon stripping.
  • Added changelog entries to both Node.js and Web client packages for the user-visible fix.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/client-common/src/client.ts Implements trailing top-level SETTINGS detection and adjusts FORMAT placement; updates query() JSDoc accordingly.
packages/client-common/tests/unit/format_query.test.ts New table-driven unit tests asserting exact wire query strings for FORMAT placement scenarios.
packages/client-node/CHANGELOG.md Documents the bug fix in the Node.js client changelog under # 1.24.0.
packages/client-web/CHANGELOG.md Documents the bug fix in the Web client changelog under # 1.24.0.

Comment thread packages/client-common/src/client.ts Outdated
…substring alloc

Close the codecov/patch gap on the formatQuery SETTINGS tokenizer by adding 9
targeted unit cases exercising its previously-uncovered branches: backslash- and
doubled-quote string escapes, unterminated string, a bare `$` / trailing `$tag`
that is not a dollar-quote opener, an unterminated dollar-quoted heredoc, a line
comment whose scan resumes after the newline, an unterminated block comment, and
an unbalanced closing bracket.

Also address the Copilot review note on the scan hot path: make `settingsClauseRe`
sticky (`/…/iy`) and probe it via `matchesSettingsClauseAt(query, i)` (sets
`lastIndex = i`) instead of `settingsClauseRe.test(query.slice(i))`, so the
depth-0 scan no longer allocates a substring per `s`/`S` probe — keeping it O(n).
Behavior is unchanged; all 23 format_query cases pass.
Copilot AI review requested due to automatic review settings July 29, 2026 14:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants