Skip to content

Render ClickHouse FORMAT PNG results in Workbench and Dashboard #307

Description

@BorisTyshkevich

Summary

Add first-class support for ClickHouse 26.6 FORMAT PNG results in the Workbench result area and Dashboard tiles.

ClickHouse produces the final PNG. SQL Browser transports, validates, retains, displays, and exports the returned bytes; it must not reconstruct pixels from rows.

This requires a binary result path. The current raw-format path decodes responses with Response.text(), which corrupts PNG bytes, and Dashboard execution rejects every authored FORMAT clause.

ClickHouse contract

PNG is output-only and returns raw PNG bytes by default.

  • Output dimensions come from output_format_image_width and output_format_image_height, both defaulting to 1024.
  • Supported result shapes are RGB (r,g,b), RGBA (r,g,b,a), or grayscale (v), optionally with integer x,y coordinates.
  • output_format_image_terminal_mode must remain empty for browser requests. Terminal protocols such as iterm, kitty, sixel, and auto are not browser image payloads.

Reference: ClickHouse PNG format documentation.

Format detection and scope

Treat a trailing explicit FORMAT PNG case-insensitively as an image result.

Support:

SELECT ... FORMAT PNG
SELECT ... FORMAT PNG SETTINGS output_format_image_width = 640
SELECT ... SETTINGS output_format_image_width = 640 FORMAT PNG

Use the existing SQL-aware format detector. Do not detect FORMAT PNG inside comments, strings, nested subqueries, or identifiers.

This issue supports only PNG. Other binary image formats and terminal image protocols are out of scope.

Binary transport contract

Extend the query transport so raw results can be text or bytes without converting binary data to a JavaScript string.

Recommended shape:

interface BinaryQueryBody {
  bytes: Uint8Array;
  contentType: string;
}

interface RunQueryResult {
  error?: string;
  raw?: string;
  binary?: BinaryQueryBody;
  streamed?: boolean;
}

Requirements:

  • runQuery(..., { format: 'PNG' }) reads the response as ArrayBuffer or equivalent binary chunks.
  • It returns Uint8Array bytes and image/png as the normalized content type.
  • Text raw formats retain their current raw string behavior.
  • Streaming Table/KPI/Filter behavior remains unchanged.
  • HTTP and ClickHouse errors must be parsed as text when the response is not a valid PNG payload.
  • Cancellation must abort the request and must not publish a partial image.
  • Never use base64 as the internal representation.

Result model

Add an explicit image result branch rather than storing PNG data in rawText.

interface ImageResultPayload {
  kind: 'image';
  format: 'PNG';
  mimeType: 'image/png';
  bytes: Uint8Array;
}

The Workbench and Dashboard may wrap this payload in their existing runtime result structures, but there must be one shared binary/image contract.

Do not serialize image bytes into saved queries, Dashboard documents, workspace persistence, history, recent values, or portable bundles. Only SQL and presentation configuration are persisted; every image is regenerated by executing the query.

Validation and resource limits

Before rendering, verify:

  • payload length is non-zero;
  • the first eight bytes match the PNG signature;
  • the IHDR chunk is present and readable;
  • width and height are positive;
  • width, height, pixel count, and payload bytes stay within explicit client limits.

Initial client limits:

MAX_PNG_WIDTH = 8192;
MAX_PNG_HEIGHT = 8192;
MAX_PNG_PIXELS = 32_000_000;
MAX_PNG_BYTES = 64 * 1024 * 1024;

Reject the payload before creating an object URL when any limit fails. Show a normal result error and release the byte buffer when practical.

The existing Dashboard max_result_bytes guard may remain, but PNG validation must not depend exclusively on a ClickHouse query setting because authored SQL can override settings.

Workbench execution and rendering

For an explicit FORMAT PNG query:

  • execute through the binary path;
  • show one Image result view instead of the normal Table / JSON / Panel switcher;
  • do not show PNG bytes as text;
  • display the image centered in the result body;
  • preserve intrinsic aspect ratio;
  • use max-width: 100% and max-height: 100% without upscaling beyond intrinsic size by default;
  • allow scrolling when the intrinsic image is larger than the available result area;
  • show loading, error, and cancelled states through the normal result lifecycle;
  • keep Copy disabled for the binary payload;
  • provide Download PNG using the exact returned bytes.

The image element must have an accessible name derived from the saved-query name or active tab name, with fallback PNG query result.

Object URL lifecycle

Render PNG bytes through a Blob/object URL.

  • Create the URL only after payload validation succeeds.
  • Revoke the previous URL before replacing it with a rerun result.
  • Revoke on tab close, result replacement, view destruction, Dashboard tile destruction, route teardown, and application teardown.
  • A cancelled, failed, stale, or superseded request must not leak a URL or replace a newer image.
  • Detached views, when supported for this result, own and revoke their own URL.

Dashboard execution

Permit FORMAT PNG only for a panel-role tile whose resolved presentation is the image presentation defined by this issue.

Do not generally remove Dashboard's explicit-format rejection. Other authored formats remain rejected because ordinary panels require structured results.

Recommended execution ownership:

  • add an image panel type, for example panel.cfg.type = 'image';
  • the image panel requires authored FORMAT PNG in v1;
  • panelExecution() recognizes the image panel and returns format: 'PNG', rowLimit: 0, and the existing read-only / byte-limit parameters;
  • reject an image panel whose SQL has no FORMAT PNG;
  • reject FORMAT PNG for KPI, chart, table, logs, text, filter, or setup roles;
  • reject any non-PNG explicit format for the image panel.

This keeps result-format ownership explicit and prevents a saved query from silently changing an ordinary Dashboard tile into a binary result.

Image panel schema

Add image to the panel configuration union and result-choice UI.

Minimal v1 configuration:

interface ImagePanelCfgV1 {
  type: 'image';
  fit?: 'contain' | 'cover' | 'actual'; // default contain
  background?: 'transparent' | 'checkerboard' | 'theme'; // default theme
  alt?: string;
}

Rules:

  • contain shows the complete image inside the available body.
  • cover fills the body and may crop visually; it never changes the bytes.
  • actual renders at intrinsic CSS pixel size and scrolls when necessary.
  • alt, when present, is the image accessible name. Otherwise use tile/query title.
  • background: checkerboard helps inspect transparent RGBA output.
  • Presentation variants and tile-local overrides may change fit/background/alt but may not change renderer type under existing Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280 rules.

Regenerate schema-derived types, validators, schema bundles, and documentation.

Dashboard tile rendering

An image tile uses the complete tile body:

  • no nested table, chart, or generic raw-text renderer;
  • preserve image aspect ratio;
  • center according to the chosen fit mode;
  • respond to tile resizing without re-executing the query;
  • use the latest successful image until a refresh succeeds, while showing a non-blocking refreshing state consistent with other tiles;
  • on error, show the normal tile error and release any image produced only by the failed generation;
  • stale-wave protection must prevent an older PNG response from replacing a newer result.

The tile header/footer may remain, but image content receives all remaining body space. grafana-grid@1, flow@1, mobile rendering, fallback rendering, print order, and semantic tile order remain unchanged.

Export behavior

Add PNG metadata to the existing format-to-file mapping:

PNG -> { ext: 'png', mime: 'image/png' }

Download PNG and direct export must stream or write the exact ClickHouse response bytes. Do not decode and re-encode through canvas.

Multi-statement script execution remains unchanged. Script export may export a PNG-producing statement through the existing file export path, but normal multiquery Run does not need inline image rows in the script summary for this issue.

Compatibility

  • Servers that do not support FORMAT PNG return their normal ClickHouse error.
  • No client-side version gate is required.
  • Existing text raw formats retain identical behavior.
  • Existing structured Workbench and Dashboard results retain identical behavior.
  • PNG terminal mode is not enabled or inferred.
  • External Dashboard trust preflight, read-only gates, cancellation, bounded concurrency, and no-execution-on-invalid-document rules remain in force.

Demo library

Add a small importable demo bundle or documented demo set containing:

  1. the 1024×1024 spiral example from this issue;
  2. a small RGB gradient using implicit coordinates;
  3. a transparent RGBA example demonstrating checkerboard background;
  4. one parameterized, low-cost query adapted from ClickHouse/RayTracer.

Do not copy the entire RayTracer repository. Include only queries whose license permits redistribution, preserve required attribution, and use conservative defaults so opening the demo does not start an expensive render automatically.

Ray-tracer queries must expose small default image dimensions and sample count, and should include a warning in their saved-query description about CPU cost.

Tests

Cover:

  • SQL-aware case-insensitive PNG detection with both FORMAT/SETTINGS orders;
  • binary response transport without UTF-8 decoding;
  • text raw formats unchanged;
  • error response parsing;
  • cancellation and stale-result suppression;
  • PNG signature and IHDR parsing;
  • byte, dimension, and pixel limits;
  • Workbench Image view and download bytes;
  • object URL creation and revocation on rerun, replacement, close, and teardown;
  • image panel schema/type generation and result-choice selection;
  • Dashboard permits image+PNG and rejects every incompatible panel/format combination;
  • Dashboard image fit modes and accessible name;
  • tile resize does not rerun SQL;
  • exact .png export metadata;
  • older ClickHouse rejection displays as a normal error;
  • demo bundle validates and imports.

Include a real-browser test proving that a PNG response renders as a visible image in Workbench and fills the available Dashboard tile body without distortion.

Acceptance criteria

  • Workbench renders valid FORMAT PNG output as an image, never as decoded text.
  • PNG responses use a binary transport and shared image-result contract.
  • Invalid or oversized PNG payloads fail before object URL creation.
  • Object URLs are revoked across every replacement and teardown path.
  • Exact returned bytes can be downloaded as .png with MIME image/png.
  • An explicit image panel type is available and schema-validated.
  • Dashboard accepts only the image-panel + FORMAT PNG combination.
  • Dashboard image content uses the full available tile body and preserves aspect ratio.
  • Existing structured, raw-text, export, cancellation, and persistence behavior does not regress.
  • Demo queries include attribution and safe default resource usage.
  • Unit, integration, browser, schema-generation, and build checks pass.

Non-goals

  • parsing PNG pixels into rows;
  • client-side PNG generation or re-encoding;
  • editing images;
  • animated PNG or video playback controls;
  • SVG, JPEG, WebP, PDF, or arbitrary binary preview;
  • iTerm2, Kitty, Sixel, or terminal auto-detection;
  • storing generated image bytes in saved queries, workspaces, history, or portable bundles;
  • automatically running expensive RayTracer queries on import;
  • changing Dashboard layout or multi-Dashboard architecture.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions