Skip to content

Web UI prototype for framing formats - #110210

Draft
alexey-milovidov wants to merge 211 commits into
masterfrom
framing-web-ui
Draft

Web UI prototype for framing formats#110210
alexey-milovidov wants to merge 211 commits into
masterfrom
framing-web-ui

Conversation

@alexey-milovidov

@alexey-milovidov alexey-milovidov commented Jul 13, 2026

Copy link
Copy Markdown
Member

Prototype changes to the Web UI (programs/server/play.html) to test and showcase the framing formats feature. This is a draft for experimentation and review of the client-side experience, not intended to merge as-is.

It builds on the framing-formats server work in #110127 (this branch is based on that PR, so the diff includes those commits until it merges into master).

What the prototype does:

  • Streams every query with the EventStream framing format over HTTP (framing_output_format=EventStream, send_logs_level=trace), decoding data / progress / log / profile_events / exception packets as they arrive - so progress and logs work regardless of the output format (including FORMAT Pretty, binary formats via base64, and images).
  • Shows realtime CPU, memory, and disk usage like clickhouse-client, aggregated per host (total and max/host), switching to peak RAM when the query finishes. The meter state is owned by each tab: the CPU counters in profile_events packets are per-packet increments, so a query running in a background tab keeps accumulating them, and reopening the tab continues the meter from its live values (rather than restarting near zero).
  • Merges elapsed time, realtime metrics, and rows/bytes read stats into a single progress area with the progress-bar gradient rendered behind all of them; the text is tinted by a mask clipped from the same gradient so it stays readable over the fill.
  • Displays server logs in real time, colored like the client, with a Logs button available even on exception; the log view stays fast for 100k+ lines while keeping native browser search, bounding retention to the most recent 100k lines (older lines are dropped and counted in a marker line). The full response text kept for the tab/history snapshot is also capped as it is collected (at ~100 KB, the size above which the snapshot is dropped), so a single large framed data/log burst is not retained in full only to be discarded later. A failed run whose reply outgrew that cap is persisted as a compact snapshot that keeps the exception carrier in the form its framing kind replays (the terminal event: exception block, the {"packet":"exception",...} line, or the plain {"exception":...} line, captured at stream-read time since the capped text stops growing before the terminal exception arrives), so reopening or reloading a failed tab still shows the error reason.
  • Base64-decodes every data/totals/extremes payload (the EventStream wire encodes each block as a single base64 data: field and the Content-Type carries payload=base64), deciding the rendering by the output format: the default format is reassembled into the table, an image format (or bytes carrying an image signature) is rendered as an inline image once the stream completes, and any other format is decoded and shown incrementally as raw text.
  • Uses a framing-compatible default format for queries without an explicit FORMAT clause: the framed request asks for JSONCompactStringsEachRowWithNamesAndTypes (the framing rejects the in-band-progress JSONStringsEachRowWithProgress) and the client reassembles the compact rows into the table renderer's shapes; on the server side (Framing formats: multiplex data, totals, extremes, progress, logs, and profile events in the HTTP response stream #110127) the compact family emits totals and extremes under framing, so WITH TOTALS and extremes-based column coloring keep working on the default path. When such a format is shown as raw text instead (an explicit FORMAT JSONCompactEachRow), only the data packets are concatenated, so the rendered text is exactly the plain output of that format rather than one carrying the totals and extremes rows the format itself drops.
  • Propagates a framed exception packet as a query failure even when the HTTP status was already 200 (so Run all stops at the failed statement) - and, when a query chooses its own JSONEachPacket* framing that fails before the 200 OK header (coming back as a non-200 application/x-ndjson packet stream ending with a {"packet":"exception",...} line), shows those packets verbatim and records framing_kind = 'ndjson_packets' for replay rather than rendering the whole stream as one opaque error string; retries framing-incompatible explicit formats (e.g. FORMAT JSONEachRowWithProgress, FORMAT Template) once without framing for read-only queries (read-only-ness is resolved with a CTE-aware lexer walk - WITH y AS (SELECT 1) INSERT INTO t SELECT * FROM y is a write - shared with Run all's grouping, so such a statement is also a barrier there and never runs in parallel with the reads that follow it; the port with regression coverage lives in src/Parsers/tests/gtest_play_query_is_read_only.cpp); a retried JSON*EachRowWithProgress format that itself reports a failure in-band - a trailing {"exception":...} object while the HTTP status stays 200 (http_write_exception_in_output_format) - is detected as a failure too, keyed off the output format rather than only a user-chosen JSONEachPacket* framing, so Run all stops after such a retried query fails; and does not add its own framing to a query that sets its own framing_output_format to a real framing choice (the response is then dispatched by content type, so the requested packets are shown verbatim); a query that sets framing_output_format = 'None' is refused client-side instead, since this page's rendering depends on framing (values the page cannot know upfront - = DEFAULT, a reset to the session/server default, and query-parameter placeholders like = {fmt:String} - are classified conservatively the same way and refused, rather than sent with a request shape the response might not match); likewise a standalone SET framing_output_format = ... is refused, because it would change the setting for the whole session (with a session_id) while the page keeps adding its own framing per request - a query-level SETTINGS framing_output_format = ... clause is the supported way to choose framing for one query. A query that carries its own framing_output_format is also refused for download (the setting would override the download's chosen default_format, so the file would be the framing packet stream), and if such a query fails, its history snapshot - the raw JSONEachPacket* packet stream - is replayed as raw text on tab-switch/reload rather than as one opaque error string.
  • Pins framing_output_format=None on every request that expects an unframed response (the plain/chart request, the compatibility retry, the download, and the panel/server-status/completion queries), rather than only omitting the setting - otherwise a framing carried by the connection URL or by the HTTP session behind it would frame those responses too. A query-level SETTINGS framing_output_format = ... clause is applied after the URL parameters, so a query that intentionally chooses a framing still overrides the pin.
  • Records the framing kind (event_stream / ndjson_packets / none) with each result snapshot and keys the history/tab replay off it, instead of guessing from the payload's first bytes - so a raw result whose text happens to start with event: (e.g. SELECT 'event: data' FORMAT RawBLOB) or {"packet": is not reparsed as a framing stream after a tab switch or reload. A snapshot recorded as ndjson_packets is replayed as raw text regardless of whether the run succeeded and of its underlying output format, matching the live path - so a successful user-framed JSONEachPacket* result whose format has its own restore path (a table or a JSONCompactColumns chart) is not reparsed as that format's JSON. The snapshot also records whether the framed stream was truncated, so replay keeps the live fail-closed behavior for images: a cut-off framed FORMAT PNG response that showed only its error live reopens from history / Back / Forward as that error too, never as a partially decoded picture (a failed but complete stream - a terminal exception packet after the payload - still renders its collected image, as live).
  • Detects both the query's FORMAT clause and its framing_output_format setting with the WASM lexer rather than a raw text match, so a mention inside a string literal or a comment - e.g. SELECT 'FORMAT JSONCompactColumns' - does not make the page silently opt out of its own framing. The detection is positional, not keyword-adjacent: a settings context is recognized by its name = value list grammar (a column merely named settings does not open one), and a FORMAT clause candidate must follow a token that ends an expression (so in WITH 1 AS format SELECT format JSONCompactColumns SETTINGS max_threads = 1 both format words are identifiers, not a clause). The download reuses the same FORMAT-clause detection (now returning the clause span) to strip only a real trailing FORMAT clause from the download query, leaving text or ordinary SQL like SELECT 'FORMAT TSV' AS s untouched. Both detectors also accept a quoted spelling of the name - the server parses setting names and FORMAT names with identifier parsers, so a backquoted framing_output_format or format name is real - comparing by the unquoted name. Regression coverage for both lives in src/Parsers/tests/gtest_play_detect_explicit_format.cpp and gtest_play_detect_framing_setting.cpp (ports of the token walking onto the real DB::Lexer).
  • Dispatches on the response output format case-insensitively. Format names are case-insensitive in ClickHouse (FormatFactory looks them up by their lowercased name), while X-ClickHouse-Format echoes the identifier exactly as the FORMAT clause spelled it, so FORMAT jsoncompactcolumns used to lose the chart renderer, a lowercased default format lost the table renderer, and the late in-band exception probes did not recognize FORMAT xml / FORMAT json / FORMAT jsoneachrowwithprogress - a query failing after its 200 OK header was then reported as a success and Run all continued past it. Every dispatch now compares a lowercased copy of the format name.
  • On the server side (Framing formats: multiplex data, totals, extremes, progress, logs, and profile events in the HTTP response stream #110127), framed pulling SELECT queries now also end with the documented final progress packet carrying result_rows / result_bytes / memory_usage, matching the native protocol and the no-result path.

Changelog category (leave one):

  • Not for changelog (changelog entry is not required)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Prototype Web UI changes to test framing formats (draft).

Related: #110127

alexey-milovidov and others added 15 commits July 11, 2026 22:43
…, logs, and profile events in the HTTP response stream

A framing format multiplexes different response parts of the query in a single
stream - chunks of data, totals and extremes, progress packets, profile events
(metrics), and server logs - everything that the native protocol supports.
Framing formats are independent of output formats: they encapsulate bytes
produced by any output format. The framing format is selected by the new query
setting `framing_output_format` and currently applies to the HTTP protocol.

Implemented framing formats:
- `None` - transparently routes everything applicable to the output format,
  so everything works as it is by default.
- `EventStream` - frames packets as HTTP server-sent events (`text/event-stream`).
- `JSONEachPacketBase64` - every packet is a JSON object; the formatted data
  is base64-encoded.
- `JSONEachPacketString` - every packet is a JSON object; the formatted data
  is put into a string.

The framing format works as a multiplexor: the output format writes into the
framing format's payload buffer, and `IOutputFormat` notifies it on packet
boundaries, which wraps everything accumulated since the previous boundary
into a packet. The concatenation of the payloads of all `data`, `totals`,
and `extremes` packets is exactly what the output format would have produced
without framing. Auxiliary packets (progress, logs, profile events,
exceptions) are represented as JSON. Server logs and profile events reuse
`InternalTextLogsQueue` and `ProfileEvents::getProfileEvents` - the same
mechanisms as the native protocol - newly attached for HTTP queries.
Exceptions are always written as the last packet of the stream, so the client
can parse the response as a stream of packets.

Processing of multiple queries at once is out of scope of the first
implementation, but the design allows extending every packet with the
information about the query index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the last payload line has no trailing `\n` (for example `FORMAT JSON`),
`find_first_symbols<'\n'>` returns `end`, and `pos = line_end + 1` formed a
pointer past the one-past-the-end position - undefined behavior, even though
the loop exited immediately afterwards. Stop when `line_end == end` instead.

Addresses a review comment on #110127

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…treaming

`IFramingFormat` wrote each packet to `out` and called `out.next()`, which only
flushes the outermost buffer. When the HTTP output is wrapped in compression
(`wrapWriteBufferWithCompressionMethod` / a `WriteBufferWithOwnMemoryDecorator`),
`next` leaves the compressed bytes in the nested buffer, so `data`, `progress`,
and `log` packets could sit there until the end of the query instead of being
delivered interactively.

Add a `flushOut` helper that also flushes the nested buffer - mirroring
`IOutputFormat::flushImpl` - and use it at every packet boundary and on finalize.

Addresses a review comment on #110127

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`IOutputFormat` notifies the framing format of a `Totals` / `Extremes` packet
right after `consumeTotals` / `consumeExtremes`. That assumes the bytes are
already in the payload buffer, which holds for row formats but not for
`Template` (`areTotalsAndExtremesUsedInFinalize` is true): it stores totals and
extremes and emits them later from `finalizeImpl`, where the framing format can
no longer tell them apart from the main data and would mislabel them as `data`
packets.

Reject such formats in `setFraming` with a clear exception instead of producing
wrong output.

Addresses a review comment on #110127

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… rejection

The `GROUP BY ... WITH TOTALS` queries in `04512_framing_formats` emitted a
nondeterministic number of `data` packets: a `data` packet follows every output
block boundary, and the number of blocks depends on the number of threads,
two-level aggregation, the output block size, and external sorting - all of
which the CI settings randomizer varies. This made the test fail in the
`arm_asan_ubsan` targeted run and in the `amd_debug` / `amd_asan_ubsan` /
`amd_tsan` / `amd_msan` flaky checks.

Pin the settings that determine the block boundaries for those queries so the
result is a single block. Also add a case asserting that a framing format is
rejected for `Template`, which defers totals and extremes to finalize.

CI report: #110127

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… and HTTP compression

Cover the two remaining test suggestions from the review:
- `EventStream` with a payload whose last line has no trailing newline
  (`FORMAT Values` produces `(0),(1),(2)`), exercising the end-of-buffer fix.
- Framing over an `enable_http_compression` response, so a regression in the
  nested-buffer flush that leaves packets stuck in the compression buffer is
  caught.

Related to #110127

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `EventStream` and `JSONEachPacketString` framing formats embed the raw
output bytes into the response as UTF-8 text: `EventStream` writes them into
`data:` fields of a `text/event-stream` response, and `JSONEachPacketString`
passes them through `writeJSONString`, which does not sanitize invalid UTF-8.

A binary output format such as `Native` or `RowBinary` would therefore produce
invalid SSE / JSON instead of a byte-preserving stream. Reject such
combinations at framing selection time (mirroring the existing rejection of
formats that defer totals and extremes) and point the user to
`JSONEachPacketBase64`, which encodes arbitrary bytes safely.

The output format is classified as text via its content type: text formats
declare a charset (e.g. `text/tab-separated-values; charset=UTF-8`,
`application/json; charset=UTF-8`), while binary formats use types such as
`application/octet-stream` without a charset.

Addresses the AI review blocker on #110127

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The text framing formats (`EventStream`, `JSONEachPacketString`) embed the
output payload as UTF-8 text, so an output format that can emit arbitrary bytes
corrupts the stream. Binary formats (`Native`, `RowBinary`) were already
rejected via their `application/octet-stream` content type, but raw passthrough
formats slipped through: `RawBLOB` writes the column bytes verbatim while
inheriting the default `text/plain; charset=UTF-8` content type, and the
`TSVRaw` / `TabSeparatedRaw` / `LineAsString` / `Raw` family advertise a textual
content type while calling `serializeTextRaw` (no escaping). Both can produce
non-UTF-8 output, reproducing the invalid SSE / JSON stream the earlier review
flagged.

Content type alone is not a reliable signal for text-safety, so make it an
explicit output-format capability: add `may_produce_raw_bytes` to the format
registration (`markOutputFormatMayProduceRawBytes`) and mark the raw formats
with it. `outputFormatProducesText` now rejects any format marked this way in
addition to the content-type check. `JSONEachPacketBase64` is unaffected, since
it encodes arbitrary bytes safely.

Extended `04512_framing_formats` to cover rejecting `RawBLOB` (always raw),
`TSVRaw` and `LineAsString` (text-labeled raw) for text framings, and to confirm
`JSONEachPacketBase64` still carries `RawBLOB` output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`FramingFormatEventStream::writeDataFields` emitted one SSE `data:` field per
`\n`-terminated line of the payload. The client reconstructs `event.data` by
joining the values of consecutive `data:` fields with `\n` and then stripping a
single trailing `\n` (per the server-sent events specification). For a payload
that ends with `\n` - the common case for line-based formats such as
`JSONEachRow`, `TSV`, or `CSV` - the stripped trailing `\n` was lost, so a
payload like `"row\n"` was reconstructed as `"row"`. That broke the central
framing contract that concatenating the payloads of the `data`, `totals`, and
`extremes` packets reproduces the output of the format without framing.

Emit an extra empty `data:` field when the payload ends with `\n`, so the
trailing newline survives the reconstruction. Payloads without a trailing
newline (for example `FORMAT JSON`) are unchanged.

Extended `04512_framing_formats` with a regression that rebuilds `event.data`
from a newline-terminated `EventStream` payload and compares it byte-for-byte
with the unframed output, and updated the existing `EventStream` reference and
the documentation example to reflect the trailing empty `data:` field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pulls in the merged fix #110162 (revert of EXPLAIN ANALYZE pipeline timing, #110162), which fixes the 'clock' logical error that failed the Stress test (amd_msan) on this PR: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=110127&sha=e774374cff8ac2e6fb0e606fe60ed73930998360&name_0=PR&name_1=Stress%20test%20%28amd_msan%29
The `EventStream` (and other) framing formats sent fewer server log packets
than the native protocol: `clickhouse-client` receives log entries from the
start (parsing, planning, analysis - `executeQuery`, `Planner`, `SelectExecutor`,
"Reading approx N rows") and the end (`executeQuery: Read N rows`,
`MemoryTracker: Query peak memory usage`) of the query, while the framing
format captured only the logs emitted during pipeline execution.

Two causes:

- The logs and profile-events queues were attached to the thread only after
  the query had been interpreted, so the interpretation-phase logs were lost.
  Attach them before interpretation (in `attachQueuesForFramingIfApplicable`),
  as the native protocol does, and wire them into the framing format once it
  is created.

- The framing format was finalized during pipeline execution, before the
  query-finish logging ran, so the trailing logs were lost. Defer the framing
  finalization (`IOutputFormat::deferFramingFinalize`) until after `onFinish`,
  while the HTTP response stream is still open, and log the peak memory usage
  explicitly before the drain (mirroring `TCPHandler`). The exception path in
  `HTTPHandler` finalizes the deferred framing format explicitly.

With this change the framing format streams the same set of server logs as the
native protocol.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `EventStream` framing format previously rejected output formats that are not
guaranteed to produce valid UTF-8 text (binary formats such as `Native` or
`RowBinary`, and raw passthrough formats such as `RawBLOB` or `TSVRaw`), because
server-sent events is a text protocol. Instead of rejecting them, `EventStream`
now base64-encodes the `data`, `totals`, and `extremes` payloads (each into a
single `data:` field) so that arbitrary bytes survive the text transport, and
signals this by adding a `payload=base64` parameter to the `Content-Type`
(`text/event-stream; charset=UTF-8; payload=base64`). The client base64-decodes
those payloads; their concatenation is byte-for-byte what the output format would
have produced without framing. Text output formats are still embedded as plain
text, and the auxiliary JSON packets (progress, logs, profile events, exception)
are never encoded.

This lets progress, server logs, and profile events be streamed for any output
format over `EventStream`. `JSONEachPacketString` still requires a text output
format (it puts the bytes into a JSON string and cannot encode arbitrary bytes),
and `Template` is still rejected for all framings because it defers totals and
extremes to finalize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the query progress display in the Web UI to consume the
`EventStream` framing format over HTTP, so it works regardless of the
output format (including `FORMAT Pretty`, binary formats via base64, and
images).

- Stream every query with `framing_output_format=EventStream` and
  `send_logs_level=trace`, decoding data/progress/log/profile_events/
  exception packets as they arrive.
- Show realtime CPU, memory, and disk usage like `clickhouse-client`,
  aggregated per host (total and max/host), switching to peak RAM when
  the query finishes.
- Merge elapsed time and realtime metrics into a single element rendered
  on top of the progress bar; keep the text readable with a
  background-clip gradient that darkens the glyphs only over the colored
  fill (per-theme peak color and fraction).
- Display server logs in real time, colored like the client, with a
  Logs button available even on exception; keep the view fast for 100k+
  lines via batched appends and content-visibility while preserving
  native browser search.
- Render inline images for image output formats, and base64-decode
  binary payloads signalled by the `payload=base64` content type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Iterate on the query progress display:

- Merge the elapsed time, realtime resource metrics, and rows/bytes read
  stats into a single progress area so the progress-bar gradient renders
  behind all of them; the read stats stay right-aligned and wrap when
  long, the elapsed/metrics text keeps its natural width.
- Draw the colored bar only one text line tall, so when the read stats
  wrap and grow the area the fill stays a single-line strip.
- Tint every glyph (left metrics and right stats alike) with a text mask
  clipped from the same full-width `--progress` gradient as the bar, so
  the two correspond exactly and the text stays readable over the fill.
  Light theme uses a plain gray to black; dark theme uses a steep
  gray to white (first quarter) then a jump to black.
- Use a 10pt font for the progress area.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mintlify

mintlify Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
ClickHouse-docs 🟢 Ready View Preview Jul 13, 2026, 11:14 AM

@clickhouse-gh

clickhouse-gh Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [8bd6303]

Summary:

job_name test_name status info comment
Style check FAIL
functional_tests_check FAIL cidb
Code Review DROPPED
Fast test (arm_darwin) DROPPED
Build (amd_debug) DROPPED
Build (amd_asan_ubsan) DROPPED
Build (amd_tsan) DROPPED
Build (amd_msan) DROPPED
Build (amd_binary) DROPPED
Build (arm_debug) DROPPED
Build (arm_asan_ubsan) DROPPED

@clickhouse-gh clickhouse-gh Bot added the pr-not-for-changelog This PR should not be mentioned in the changelog label Jul 13, 2026
alexey-milovidov and others added 2 commits July 13, 2026 04:25
…and framed exceptions

Address the review findings on the framing formats
(#110127):

- `EventStream` now serializes a multi-row `profile_events` batch as a single
  JSON array in one `data:` field. An SSE client reconstructs `event.data` by
  joining consecutive `data:` fields with a newline, so emitting one field per
  row produced `{...}\n{...}`, which is not valid JSON and did not match the
  documented `profile_events` contract (an array of profile events as JSON).
  This mirrors the `profile_events` array of the `JSONEachPacket` framings.

- Raw-byte detection for text framings is now settings-aware. `CustomSeparated`
  with `format_custom_escaping_rule = 'Raw'` writes the column bytes verbatim
  (like `TSVRaw`), so it may produce non-UTF-8 output even though it advertises
  a textual content type. Since this depends on a setting rather than the format
  name, it cannot be marked statically with `markOutputFormatMayProduceRawBytes`;
  a settings-aware checker (`registerOutputFormatMayProduceRawBytesChecker`) is
  added alongside the static flag. `EventStream` now base64-encodes such output,
  and `JSONEachPacketString` rejects it, pointing to `JSONEachPacketBase64`.

- When the failure is the framing/output-format compatibility check itself
  (for example `JSONEachPacketString` with `FORMAT RowBinary`, or a framing with
  a format that defers totals/extremes such as `Template`), the error is now
  delivered as a framed `exception` packet instead of a plain HTTP error body.
  The exception packet is always JSON regardless of the output format, so the
  exception-recovery path creates the framing with a `for_exception` flag that
  skips the data-payload compatibility check and the deferred-totals check.

- The exception-recovery formatter now wires in the logs and profile-events
  queues attached before the query is interpreted, so `log` / `profile_events`
  packets accumulated during parsing and planning are drained on `finalize`
  instead of being dropped when the query fails before producing output.

- Update the `framing_output_format` setting description: `EventStream` no longer
  requires a text output format, it base64-encodes non-UTF-8 output.

Extend `04512_framing_formats` and `04513_framing_formats_logs_profile_events`
with regressions for each of the above.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace the Result/Logs button pair with a single "Logs" toggle at the
  front of the progress line (just before the elapsed/metrics text). It
  switches the result element(s) between the query result and the
  streamed server logs, shows a dotted underline, uses the monospace
  font, and gets a yellow background while the logs view is active.
- Show the toggle only when the query produced at least one log line, so
  it stays hidden when a query has not started or produced no logs.
- Drop the now-meaningless leading `NN.N%, ` from the read stats once the
  query finishes (the percentage only matters while progressing to 100%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Interpreters/executeQuery.cpp
Comment thread src/Interpreters/executeQuery.cpp
Comment thread programs/server/play.html Outdated
alexey-milovidov and others added 4 commits July 13, 2026 08:13
…xception path, document JSONEachPacketString UTF-8 contract

Addresses the AI review of `3a52d17a`.

`HTTPHandler` buffered exception path (`http_wait_end_of_query=1`): on an
exception the buffered output is discarded and the framing format is recreated
from `framing_name` alone, so the `log` and `profile_events` queues that
`executeQuery` attaches before parsing (and wires into the framing) were
dropped - `http_wait_end_of_query=1&framing_output_format=JSONEachPacketString&send_logs_level=trace`
lost the parsing/planning `log` packets that the streaming path and the docs
promise. The queues are now carried into the buffered exception writer: they are
captured by value (the original framing object goes out of scope before the
writer runs) and re-applied to the recreated framing via new `IFramingFormat`
accessors. Added a `wait_end_of_query` regression to
`04513_framing_formats_logs_profile_events`.

`JSONEachPacketString` UTF-8 contract: documented that it puts the payload
bytes into a JSON string without validating or re-encoding them, so text output
formats such as `JSONEachRow`, `TSV` or `CSV` may emit invalid UTF-8 for
`String` / `FixedString` values holding arbitrary bytes (just as ClickHouse's
own `JSONEachRow` does with the default `output_format_json_validate_utf8 = 0`),
in which case the NDJSON stream is not guaranteed to be valid UTF-8; pointed to
`JSONEachPacketBase64` for byte-exact transport.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m snapshot

Address review (#110210): the
history/tab replay path only distinguished the default (table) output from
`rawText`; it never recorded whether the saved `EventStream` response used
`payload=base64` (a binary or raw output format such as `Native` or `RawBLOB`).
A restored snapshot of such a result was therefore replayed as literal base64
text, whereas the live `readEventStream` path decodes the payload to bytes and
renders it as an image or raw text.

Persist an `is_base64` bit with the result snapshot (single-query, multi-query,
and the `saveHistory` / flat-entry serialization) and have `renderEventStreamText`
decode the base64 `data`/`totals`/`extremes` payloads and hand them to
`renderBinaryPayload`, exactly like the live path. A base64 snapshot is rendered
as an image or raw text and is never finished as a table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the Web UI prototype current with master and clear the reported conflict
with the base branch. The only conflict was a single hunk in
`programs/server/play.html`, in the non-OK response branch of `postImpl`:
master added query-error highlighting (`highlightQueryError`, guarded by
`editorInteractionGen === runEditorInteraction`) while this branch added the
`is_base64` field to the returned result and reworked the image `else if`
condition to exclude framed (`text/event-stream`) responses. Resolved by
keeping both: master's error-highlighting block, followed by this branch's
`is_base64` return and framing-aware image branch. The `queryStart` argument
that master threads into `postImpl` is already present (auto-merged), so the
highlight call is in scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread programs/server/play.html Outdated
Comment thread src/Interpreters/executeQuery.cpp Outdated
Comment thread src/Interpreters/executeQuery.cpp Outdated
Comment thread src/Interpreters/executeQuery.cpp Outdated
Comment thread programs/server/play.html Outdated
alexey-milovidov and others added 2 commits July 13, 2026 12:11
…fective query settings

The `log` / `profile_events` queues for a framing format were attached once,
before the query was interpreted, from the pre-parse context. The framing
decision (`createFramingFormatIfApplicable`), however, uses the effective
settings after the query's own `SETTINGS` clause has been applied inside
`executeQueryImpl`. The two could disagree:

- A query that enables framing only in its own `SETTINGS` clause
  (`SELECT ... SETTINGS framing_output_format='JSONEachPacketString'`) still
  built a framed response, but the queues stayed empty, so no `log` /
  `profile_events` packets ever appeared.
- The inverse override (framing or `send_logs_level` / `send_profile_events`
  enabled in the session / URL but turned off by the query's `SETTINGS` clause)
  still allocated the unbounded queues and captured packets that nobody drains.

Replace `attachQueuesForFramingIfApplicable` with an idempotent
`syncFramingQueuesWithSettings` that brings the attached queues into agreement
with the current effective settings (attaching what is wanted, dropping what is
not; the thread group keeps only a weak reference, so resetting the owning
`shared_ptr` detaches the queue). It is called before the query is interpreted
(so parse / plan-phase logs are still captured for framing requested from the
session or URL) and again after `executeQueryImpl` has applied the query's
`SETTINGS` clause, on both the success and the exception paths. A framing
format enabled only by a query-level `SETTINGS` clause is not known before
parsing, so its queues capture from query execution onwards - the parse / plan
phase logs are captured only when framing is requested from the session or URL.

Add regressions to `04513_framing_formats_logs_profile_events` covering
query-level `SETTINGS` for `framing_output_format`, `send_logs_level`, and
`send_profile_events`, including the inverse override.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o read-only queries

Two fixes to the framing `EventStream` consumer in `play.html`, addressing the
AI review of the PR:

- `profile_events`: `EventStream` serializes each `profile_events` packet as a
  single JSON array of event objects in one `data:` field, but the client pushed
  every parsed line as one element, producing `[[...]]`. `feedProfileEvents` then
  saw the whole batch as a single element with no `thread_id` / `name`, so the
  realtime CPU / RAM / disk meters stopped updating for framed queries. Flatten
  the parsed batch into individual events (a bare object is tolerated too, for
  older one-per-line servers).

- No-framing retry: the Web UI enables framing for every query, and a non-pulling
  pipeline (`INSERT`, DDL) never creates a framing stream, so such a statement can
  return a plain-HTTP error after it has already run. The previous code retried
  the whole request on any non-event-stream error, which could duplicate the
  side effect. Gate the retry to read-only queries (`queryIsReadOnly`, the same
  read-only kinds treated as parallelizable), so a side-effecting statement is
  never blindly resubmitted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ss` packet precedes the injected fault

Framed `progress` packets are throttled by `interactive_delay` (100 ms by
default). On a slow run (debug/sanitizer flaky checks), the failing query of
the first case took longer than that, so a `progress` packet was emitted and
flushed straight to the client (the response buffer is pinned to zero) before
`framing_exception_packet_throw` fired, and the aborted response was not
empty: 04817 failed with 'MISMATCH: something was delivered' in all four
flaky checks.

Pin `interactive_delay` to one hour on the streaming request, as
04628_framing_progress_throttle already does.

https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=110127&sha=689e1df9b32e6c8b1bca628856d966a09740aefe&name_0=PR
#110127
…format` setting description

The description said every failed framed stream ends with an `exception`
packet, while the implementation intentionally fails closed - terminating the
stream without a terminal packet - once part of the packet stream has been
produced and can no longer be discarded (a partial packet write, a failure of
the exception delivery itself, or a failure while flushing or closing the
response stream). `Settings.cpp` is the source for `system.settings` and the
generated settings docs, so it over-promised the protocol contract. Carry the
caveat from `docs/en/interfaces/framing-formats.md` into the description.
…k` and its query-log metrics semantics

The comment in `executeQuery.h` still described `query_finish_callback` as a
hook that runs before the `QueryFinish` entry is dumped to `query_log`, and
the comment on the HTTP callback claimed the flush exists to attribute
`NetworkSendElapsedMicroseconds` / `NetworkSendBytes` to the query. For a
framed response both are stale: the framed stream must include the trailing
`log` / `profile_events` packets emitted by the query-finish logging inside
`onFinish` and end with the final `progress` packet, and closing the response
stream must come after that, so the callback necessarily runs after the
`QueryFinish` snapshot. The send counters of the response tail (and, for a
buffered response, of the delayed-results push) are therefore not part of the
query's snapshot - the same semantics as the native protocol, whose trailing
log / profile-events sends after `logQueryFinish` are equally unattributed.

Transmitting the buffered response before `onFinish` would forfeit the
late-error contract instead: a failure in `onFinish` (a query-log write, for
example) could no longer be delivered as a fresh framed `exception` response
with a proper HTTP status (see 04817), which is the purpose of
`wait_end_of_query`. Document the actual contract in `executeQuery.h`,
`HTTPHandler.cpp`, and `framing-formats.md` instead of restructuring the
ordering.
@alexey-milovidov

Copy link
Copy Markdown
Member Author

🕵 CI triage for bc31e02049d5:

Comment thread programs/server/play.html Outdated
alexey-milovidov and others added 5 commits August 9, 2026 04:23
The WASM `tokenize` placed its buffers (the lexer object, the query
bytes and the token out-pointers) starting at memory offset 0, but the
module's own shadow stack occupies the low memory up to `__heap_base`
and its call frames grow down from there - so once the query pushed the
buffers into the frame region (~64 KiB), the module's calls and the
buffers overwrote each other, and the corrupted token stream (garbage
types, never end-of-stream, never an error) made the loop spin forever:
with `detectFramingSetting`, `detectExplicitFormatClause`,
`queryIsReadOnly` and `splitAllQueries` on the mandatory request path,
a 64 KiB-class query hung the tab before any request was sent. The old
64 KiB `max_query_size` additionally flagged every token crossing that
boundary as an error, silently truncating the token stream of a big
query.

Now the buffers start at `__heap_base`, the memory grows to fit the
text, the lexer is created without a size cap, and a token stream that
stops advancing throws instead of looping. The request-path detectors
go through the new `tokenizeOrNull`, which translates any tokenizer
failure (WebAssembly unavailable, growth failure, the progress guard)
into their best-effort text-match fallbacks, so such a query still runs.

Validated by driving the real extracted page functions in Node: the
previously hanging ~64 KiB comment queries now tokenize completely; a
framing setting, a `FORMAT` clause, a read-only classification, a
`Run all` split, a query parameter and the query under cursor are all
resolved past the old window; multi-megabyte queries tokenize (twice -
the grown memory is reused); and with an injected tokenizer failure all
five detectors fall back to their text-match heuristics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ession also fails closed

The fail-close guard in `trySendExceptionToClient` was reported to miss framed
bytes that a transport compressor (e.g. `BrotliWriteBuffer`) has already
consumed into its internal codec state. It does not: `count` is cumulative
(`bytes + offset()`, with `bytes` bumped by `WriteBuffer::next` even when
`nextImpl` throws), and `out_maybe_compressed` is the buffer the framing
writes into - the topmost of the compression wrapper, the internal
`compress=1` layer and the response buffer - so bytes can only reach the
codec state through a buffer whose `count` is then permanently non-zero.
Extend the comment to state this, and add a regression to
`04817_framing_error_with_buffered_response` that runs the framed exception
delivery failure under `Accept-Encoding: br` with `enable_http_compression=1`
and asserts the client never observes a complete response with a plain error
body appended.

#110127
…ls on an absent file

In the Fast test environment the aborted compressed response delivered no bytes
at all, so `curl -o` never created the output file and the subsequent `grep`
spilled "No such file or directory" to stderr, failing the test despite all
assertions passing. Pre-create the file empty: an empty response trivially
contains no plain error body.

https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=110127&sha=066f27a9704c2627bdc8f0d4892aa142a4aeb381&name_0=PR&name_1=Fast%20test
#110127

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexey-milovidov

Copy link
Copy Markdown
Member Author

🕵 CI triage for 99226724c02a (unattended continue-pr): the only red was AST fuzzer (amd_debug)Logical error: Can't determine table for parallel replicas in buildQueryPlanForParallelReplicas (src/Planner/findParallelReplicasQuery.cpp), hit by a fuzzed RIGHT JOIN query over CTEs. This is unrelated to this PR (the branch touches programs/server/play.html plus the #110127 framing server commits it carries; no planner/parallel-replicas code) and is already tracked with a deterministic master reproducer:

Related: #113292

Report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=110210&sha=99226724c02abf8e86a2a89f99f06b916346fb15&name_0=PR&name_1=AST%20fuzzer%20(amd_debug)

@groeneai, no fix PR exists yet for #113292 — please provide a fix in a separate PR (or link it here if already in progress).

Meanwhile I merged the latest origin/framing-formats (2 test/comment-only commits from #110127 review) and origin/master into this branch (both clean, pushed as 38a7c45a0f56); the framing/play stateless tests (04512, 04513, 04547, 04548, 04693, 04816, 04817, 01528_play) all pass locally on the merged binary.

Comment thread programs/server/play.html
Comment thread programs/server/play.html Outdated
Comment thread src/Parsers/tests/gtest_play_detect_explicit_format.cpp Outdated
Comment thread src/Server/HTTPHandler.cpp
Comment thread src/Processors/Formats/Impl/JSONObjectEachRowRowOutputFormat.cpp Outdated
Comment thread programs/server/play.html Outdated
@groeneai

groeneai commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

A fix PR exists and has been open since 2026-08-05: #113515, which carries Closes #113292. No separate PR is needed.

It is the same defect as the fuzzed RIGHT JOIN here, not another instance of the shared Can't determine table for parallel replicas message: for JoinKind::Right it is the left table expression that buildQueryTreeForShard materializes, but getSupportingParallelReplicasQueries descended the left one too, so the query was admitted for the side the rewrite then replaced and the lookup threw at findParallelReplicasQuery.cpp:632.

What it needs is a reviewer. CI is finished on fabe4a59: 155 success, 17 skipped, 2 failures, both trunk and both owned in the finish ledger there. mergeStateStatus is BLOCKED only on the missing approval. I cc'd @ nickitat and @ yakov-olkhovskiy on 2026-08-05 with no response since.

On the triage flow: this is the third time you have asked me for #113292, and I linked #113515 in reply both previous times (#42701 on 08-07, #112152 on 08-08). A check for an open PR whose body links the issue would drop the round trip.

…explicitly

The string fields of the auxiliary framed packets (`log`, `profile_events`,
`exception`) are written through `WriteBufferValidUTF8`, whose tail stays
buffered until the buffer is finalized. `writeJSONStringValidUTF8` left that to
the destructor, which catches and suppresses any exception from `finalize`
(`WriteBufferValidUTF8::~WriteBufferValidUTF8`). Since the buffer writes
straight into the live response stream, a failure to write the last bytes of
such a string was swallowed: the packet was left truncated - hence invalid
JSON - on the wire, while `emitToOut` cleared `writing` and the stream happily
continued to its success terminator.

`finalize` is now called explicitly, so such a failure propagates into the
existing fail-close path: the stream ends with the half-written packet and
carries no terminator at all. `WriteBuffer::finalize` cancels the buffer when
`finalizeImpl` throws, so the destructor adds nothing after that.

Added the `write_buffer_valid_utf8_finalize_throw` fail point (the failure has
to come out of the flush itself, which is only injectable there) and the
regression test `04839_framing_string_flush_error`, covering both a
`profile_events` packet of a successful query and the terminal `exception`
packet. Verified that the test fails without the fix: the previous behavior
delivered `"host_name":,` followed by the final `progress` packet with
`result_rows`, and `{"packet":"exception","exception":}` as a complete
response.
…et_result_details`

Two review findings on the framed error paths.

1. `HTTPHandler`'s fail-close latch for framed responses (`used_output.framed`) was
   only set from the `set_query_result` callback. On the exception path
   `executeQuery` deliberately swallows a failure of that callback (see the
   `execute_query_calling_empty_set_result_func_on_exception` failpoint), so the
   callback may never run even though the response is then written through a
   framing format. A second failure - while delivering the exception packet or
   while closing the response - would then make `trySendExceptionToClient` miss
   the framed fast path and append the generic `__exception__` block after a
   partial packet stream. `handle_exception_in_output_format` now latches the flag
   itself whenever the output format has a framing.

2. The `JSONObjectEachRow` raw-bytes checker only looked at the header names,
   but the values of the column selected by
   `format_json_object_each_row_column_for_object_name` become the outer JSON
   keys and are written verbatim - `JSONUtils::writeCompactObjectStart` emits the
   title with `writeCString`, without escaping or UTF-8 validation. A value
   containing a quote, a newline or non-UTF-8 bytes therefore makes the output
   non-textual, which is data-dependent and not knowable from the header. The
   checker now fails close whenever such a column is selected, so the text
   framings reject it or switch to a base64 payload.

New test `04843_framed_fail_close_without_result_details` covers (1); `04512`
covers (2) - its previous "object-name column is not checked" case is replaced by
the rejection, a byte-exact base64 round trip of an output whose object name
breaks JSON, and the acceptance of the synthesized `row_N` object names.
…xception

The test now exercises the fail-close contract of a framed exception response
whose `set_result_details` callback never ran for both injectable secondary
failures: a throw while the terminal `exception` packet is delivered
(`framing_exception_packet_throw`, the response is cut before any bytes reach the
client) and a throw while the response is closed (`http_output_finalize_throw`,
the complete packet stream is on the wire). In both cases nothing may follow the
packet stream: no generic `__exception__` block, no second `exception` packet,
and the client observes an aborted connection.

The first section's earlier assertion that `log` packets are already on the wire
was wrong: with a query that fails during analysis, the auxiliary packets of the
recovery stream are still buffered when the injected failure hits, so the client
sees an empty response.
…tion packet

Three findings on the Web UI request/response handling, plus the review's test
request for the SQL-lexer ports.

1. `detectFramingSetting` only opened a settings context when the list began with
   a `name = value` entry. `ParserSetQuery` also accepts valueless shorthand
   settings, so a valid query like `SELECT 1 SETTINGS optimize_move_to_prewhere,
   framing_output_format = 'None'` was skipped entirely and the page added its own
   `EventStream` framing instead of honoring - or refusing - the query's choice.
   The walk now consumes shorthand entries. Because that token shape also matches
   an ordinary column named `settings` (`SELECT settings x, framing_output_format =
   'None' FROM t`), a list opened by a shorthand entry alone is trusted only when
   it also ends where a settings list can end (end of query, `;`, or a clause that
   may follow the list); otherwise the walk is rolled back.

2. The framing-compatibility retry matched `is not compatible with framing
   formats` anywhere in the buffered error stream. A failed stream can already
   carry `data` and `log` packets, so a row or log line containing that phrase
   resubmitted the query and discarded the partial stream that had been rendered.
   The decision now comes from the message of the terminal `exception` packet only
   (`framedFailureMessage`).

3. The non-OK `application/x-ndjson` branch assumed that content type always means
   a packet stream. Plain `JSONEachRow` / `JSONLines` / `NDJSON` use it too and,
   with `http_write_exception_in_output_format`, return a non-OK `{"exception":...}`
   body that the generic error branch renders properly - while this branch echoed
   the raw line and skipped the error rendering. The branch is now gated on
   `ndjson_exception_prefix !== null`, i.e. on the cases that really are
   packetized.

The lexer ports in `src/Parsers/tests/gtest_play_*` created `DB::Lexer` with the
old 64 KiB `max_query_size` cap, so they analyzed only a truncated prefix of a
large query and could not cover the no-limit tokenizer the page now uses. They
create it without a limit and report an unexpected error token as a test failure
(except the `getQueryUnderCursor` port, where stopping at an error token IS the
behavior under test). Each of the three request-path ports gained a `>64 KiB`
regression, and `gtest_play_detect_framing_setting` covers the shorthand walk and
its rollback. `04548` pins the new page invariants and the two server contracts
behind them: a plain NDJSON format's non-OK in-band exception body, and a settings
list that begins with a shorthand entry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-not-for-changelog This PR should not be mentioned in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants