Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,58 @@ auto-generated per-PR notes; this file is the curated, human-readable history.

## [Unreleased]

### Added
- **ClickHouse 26.6 `FORMAT PNG` results render as real images** (#307). A
query with a trailing explicit `FORMAT PNG` now runs through a new binary
transport (`runQuery` reads bytes, never `Response.text()` — no base64
anywhere) and is validated client-side (PNG signature, IHDR, 8192×8192 /
32M-pixel / 64 MiB caps in the pure `src/core/png.ts`) before any object
URL exists. The Workbench shows a locked "Image (PNG)" result view —
centered, scrollable, aspect-preserving, alt-named from the tab/saved
query — with exact-bytes `Download PNG` (Copy is disabled); blob URLs are
revoked on rerun, tab close, and result replacement. Dashboards gain a
schema-validated `image` panel type (`fit` contain/cover/actual,
`background` theme/transparent/checkerboard, `alt`): the only tile shape
allowed to carry an authored `FORMAT` clause, and only `PNG` — every
other panel/role keeps the blanket rejection. Tiles render the image in
the full body, repaint on image identity (resize never re-executes), and
revoke the previous URL on replacement in both layout engines. Image
bytes are never persisted (saved queries, history, workspaces, bundles).
New importable demo library `examples/image-panels.json` (spiral, RGB
gradient, RGBA checkerboard glow, and an original ray-traced sphere
crediting ClickHouse's RayTracer demos) — requires ClickHouse 26.6+.

### Fixed
- **`npm run local` can now opt an OAuth connection into `ch_auth: "basic"`.**
`build/local.py` only ever emitted the default `bearer` auth for OAuth
connections read from `~/.clickhouse-client/config.xml`, so there was no way
to locally exercise the ch-jwt-verify path (stock/OSS ClickHouse behind a
Basic-password JWT verifier, `docs/CLICKHOUSE-OSS-OAUTH.md`) — the browser
always sent `Authorization: Bearer`, which such a server rejects
(`AUTHENTICATION_FAILED`, "'Bearer' HTTP Authorization scheme is not
supported"). A `<ch-auth>basic</ch-auth>` tag on the connection now mirrors
`install.sh --ch-auth basic` into the generated `config.json`'s IdP entry.
- **Starring a query in a fresh workspace (and importing favorited queries)
now actually creates Dashboard tiles** (#307 merge-gate finding; pre-existing
Phase-8 regression). `toggleTileMembership` used to no-op while the
workspace's dashboard document was `null`, and **File → Import queries**
never synced favorite→tile membership — so the Dashboard's "star a query to
add it" empty-state was a dead end and README's documented `kpi-panel.json`
import flow showed nothing. Starring a panel-role query now creates the
dashboard document on demand, and `planImportQueries` runs the new
additive-only, idempotent `syncFavoriteTileMembership` so imported
favorited panel-role queries arrive with tiles. A starred query with a
trailing `FORMAT PNG` and no explicit panel type is now auto-inferred as an
Image panel (owner decision) — only explicitly-typed non-image panels keep
the FORMAT rejection, and for PNG that error now says how to fix it ("set
its panel type to Image").
- **Large `FORMAT PNG` results are no longer truncated by the ROWS selector**
(#307 merge-gate finding). The row cap (`max_result_rows` +
`result_overflow_mode=break`) was sent for every raw-format run, so a
1024×1024 image lost all but its first ~64 pixel rows; `runQuery` now
skips the cap for binary formats — the client-side PNG dimension/byte
limits remain the real guards.

### Changed
- **Grafana-grid KPI tiles are polished in both Dashboard modes** (#316,
follow-on to #291). Edit mode keeps the full editing shell but drops the
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ layer) straight from filter inputs, and
[`examples/iceberg-dba-dashboard.json`](examples/iceberg-dba-dashboard.json) (DBA,
with snapshot/metadata **log panels**) explore them with one shared `catalog`
filter across every tile.
The [`examples/image-panels.json`](examples/image-panels.json) library is four
Image panels (a rainbow polar spiral, an RGB gradient, a transparent RGBA glow
over a checkerboard background, and a small ray-traced shaded sphere inspired
by [ClickHouse/RayTracer](https://github.com/ClickHouse/RayTracer)) that
each author an explicit `FORMAT PNG` query, demonstrating the Image result
view and Dashboard Image panel — needs **ClickHouse 26.6+** for `FORMAT PNG`.

## How it works

Expand Down Expand Up @@ -646,6 +652,13 @@ by default, so a stock server works. For an **OAuth** connection you also regist
`http://localhost:8900/sql` as a redirect URI with the IdP. Override the serve port
with `PORT` and the config path with `LOCAL_CH_CONFIG`. Ctrl-C stops it.

If that cluster is stock/OSS ClickHouse behind a
[ch-jwt-verify](https://github.com/Altinity/ch-jwt-verify) deployment rather than
an Antalya `<token_processors>` build, add `<ch-auth>basic</ch-auth>` to the
`<connection>` so the browser sends the JWT as the HTTP Basic password instead of
`Authorization: Bearer` — see
[docs/CLICKHOUSE-OSS-OAUTH.md](docs/CLICKHOUSE-OSS-OAUTH.md).

**From Docker** — the container is a static nginx server that takes an explicit
`config.json` rather than reading `~/.clickhouse-client`. See
[Run in Docker](#run-in-docker) above.
Expand Down
19 changes: 16 additions & 3 deletions build/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
• a plain connection (hostname/user/password) → prefills the credentials form.
• a connection carrying clickhouse-client's OAuth keys (`oauth-url`,
`oauth-client-id`, optional `oauth-client-secret` for a Web client like Google,
`oauth-audience`) → an OAuth sign-in against that cluster.
`oauth-audience`) → an OAuth sign-in against that cluster. Add
`<ch-auth>basic</ch-auth>` if that cluster is stock/OSS ClickHouse behind a
ch-jwt-verify deployment (docs/CLICKHOUSE-OSS-OAUTH.md) rather than a
`<token_processors>` build — the browser then sends the JWT as the HTTP
Basic password instead of `Authorization: Bearer`.

A connection with `<accept-invalid-certificate>1</accept-invalid-certificate>`
is flagged `insecure` in config.json. The browser can't skip TLS validation
Expand Down Expand Up @@ -150,15 +154,24 @@ def collect():
oauth_client = _text(conn, "oauth-client-id", "oauth_client_id")
oauth_secret = _text(conn, "oauth-client-secret", "oauth_client_secret")
oauth_aud = _text(conn, "oauth-audience", "oauth_audience")
# Stock/OSS ClickHouse can't validate a Bearer JWT itself (see
# docs/CLICKHOUSE-OSS-OAUTH.md) — a ch-jwt-verify deployment instead expects
# the JWT as the HTTP Basic *password*. `--ch-auth basic` on install.sh bakes
# this into a real deploy's config.json; mirror it here so a local <connection>
# can opt into the same `ch_auth: "basic"` the browser already understands.
ch_auth = _text(conn, "ch-auth", "ch_auth").lower()
if oauth_url and oauth_client:
idps_by_id.setdefault(name, {
idp = {
"id": name, "label": name, "issuer": oauth_url, "client_id": oauth_client,
# Optional: a Web-client secret (e.g. Google) for the code exchange.
# Empty → public PKCE. clickhouse-client has no such flag, so this is
# a local-only convenience key read from the same connection.
"client_secret": oauth_secret, "audience": oauth_aud,
"bearer": "access_token" if oauth_aud else "id_token",
})
}
if ch_auth == "basic":
idp["ch_auth"] = "basic"
idps_by_id.setdefault(name, idp)
hosts.append({"label": name, "url": url, "auth": "oauth", "idp": name,
"insecure": insecure, "_alts": alts})
else:
Expand Down
5 changes: 5 additions & 0 deletions docs/CLICKHOUSE-OSS-OAUTH.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ token's `email` claim (falling back to `preferred_username` / `sub`):
}
```

For a real deploy this is `install.sh --ch-auth basic` (baked into the rendered
`config.json`). For `npm run local`, add `<ch-auth>basic</ch-auth>` to the OAuth
`<connection>` in `~/.clickhouse-client/config.xml` — `build/local.py` picks it up
and sets the same `ch_auth: "basic"` on that IdP's generated entry.

`bearer` usually stays at its default `id_token` here (ch-jwt-verify validates
the id_token as the password). Combine with `audience` only if your verifier
enforces one.
Expand Down
84 changes: 84 additions & 0 deletions examples/image-panels.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"$schema": "https://altinity.com/schemas/altinity-sql-browser/library-v2.schema.json",
"format": "altinity-sql-browser/saved-queries",
"version": 2,
"exportedAt": "2026-07-19T00:00:00.000Z",
"queries": [
{
"id": "image-spiral",
"sql": "WITH\n 1024 AS size,\n 3000 AS n_points,\n 6 AS turns\nSELECT\n toInt32(round(size / 2 + (4 + (number / n_points) * (size / 2 - 8)) * cos(turns * 2 * pi() * number / n_points))) AS x,\n toInt32(round(size / 2 + (4 + (number / n_points) * (size / 2 - 8)) * sin(turns * 2 * pi() * number / n_points))) AS y,\n toUInt8(round(128 + 127 * sin(turns * 2 * pi() * number / n_points))) AS r,\n toUInt8(round(128 + 127 * sin(turns * 2 * pi() * number / n_points + 2.094))) AS g,\n toUInt8(round(128 + 127 * sin(turns * 2 * pi() * number / n_points + 4.188))) AS b\nFROM numbers(n_points)\nFORMAT PNG\nSETTINGS output_format_image_width = 1024, output_format_image_height = 1024",
"specVersion": 1,
"spec": {
"name": "Image: rainbow spiral (1024×1024)",
"description": "Plots 3000 points along a 6-turn polar spiral as an RGB FORMAT PNG image, hue-cycling colour by phase. Needs ClickHouse 26.6+ for FORMAT PNG. Columns must be named x,y (integer) and r,g,b (UInt8) per the PNG format contract; canvas size comes from output_format_image_width/height (default 1024).",
"favorite": true,
"view": "panel",
"panel": {
"cfg": {
"type": "image",
"fit": "contain",
"background": "theme",
"alt": "A rainbow-coloured spiral traced by 3000 points"
}
}
}
},
{
"id": "image-rgb-gradient",
"sql": "SELECT\n toUInt8(number % 256) AS r,\n toUInt8(intDiv(number, 256) % 256) AS g,\n toUInt8(128 + 64 * sin(number / 4096.0)) AS b\nFROM numbers(65536)\nFORMAT PNG\nSETTINGS output_format_image_width = 256, output_format_image_height = 256",
"specVersion": 1,
"spec": {
"name": "Image: RGB gradient (256×256)",
"description": "A cheap 256×256 RGB FORMAT PNG with no explicit x,y columns — row order alone (row-major, left-to-right then top-to-bottom) fills the canvas. Needs ClickHouse 26.6+ for FORMAT PNG; canvas size set via output_format_image_width/height SETTINGS.",
"favorite": true,
"view": "panel",
"panel": {
"cfg": {
"type": "image",
"fit": "contain",
"background": "theme",
"alt": "A smooth RGB colour gradient"
}
}
}
},
{
"id": "image-rgba-glow",
"sql": "WITH 256 AS size\nSELECT\n toUInt8(60) AS r,\n toUInt8(160) AS g,\n toUInt8(240) AS b,\n toUInt8(255 * exp(-1 * (\n pow((toFloat64(number % size) - size / 2) / (size / 4), 2) +\n pow((toFloat64(intDiv(number, size)) - size / 2) / (size / 4), 2)\n ))) AS a\nFROM numbers(size * size)\nFORMAT PNG\nSETTINGS output_format_image_width = 256, output_format_image_height = 256",
"specVersion": 1,
"spec": {
"name": "Image: transparent RGBA glow (256×256)",
"description": "A soft blue radial glow with a Gaussian alpha falloff — an r,g,b,a FORMAT PNG demonstrating the panel's checkerboard background option for transparent images. Needs ClickHouse 26.6+ for FORMAT PNG.",
"favorite": true,
"view": "panel",
"panel": {
"cfg": {
"type": "image",
"fit": "contain",
"background": "checkerboard",
"alt": "A soft blue glow fading to full transparency at the edges, shown over a checkerboard background"
}
}
}
},
{
"id": "image-raytrace-sphere",
"sql": "-- Inspired by ClickHouse's SQL ray-tracing demos (github.com/ClickHouse/RayTracer);\n-- this is an original, minimal analytic ray/sphere scene, not a copy of that project.\nWITH\n 256 AS size,\n 0.0 AS cx, 0.0 AS cy, -3.0 AS cz, 1.0 AS cr,\n -2.0 AS lx, 3.0 AS ly, -1.0 AS lz\nSELECT\n x, y,\n toUInt8(greatest(0, least(255, round(255 * (0.05 + 0.55 * shade))))) AS r,\n toUInt8(greatest(0, least(255, round(255 * (0.08 + 0.55 * shade))))) AS g,\n toUInt8(greatest(0, least(255, round(255 * (0.35 + 0.60 * shade))))) AS b\nFROM (\n SELECT\n x, y, hit,\n if(hit, greatest(0.0, nx * lux + ny * luy + nz * luz), 1.0 - v * 0.5) AS shade\n FROM (\n SELECT\n x, y, hit,\n (px - cx) / cr AS nx, (py - cy) / cr AS ny, (pz - cz) / cr AS nz,\n (lx - px) / llen AS lux, (ly - py) / llen AS luy, (lz - pz) / llen AS luz,\n v\n FROM (\n SELECT\n x, y, u, v, dx, dy, dz, t, hit,\n t * dx AS px, t * dy AS py, t * dz AS pz,\n sqrt(pow(lx - t * dx, 2) + pow(ly - t * dy, 2) + pow(lz - t * dz, 2)) AS llen\n FROM (\n SELECT\n x, y, u, v, dx, dy, dz,\n (-b - sqrt(greatest(disc, 0))) / 2 AS t,\n disc >= 0 AS hit\n FROM (\n SELECT\n x, y, u, v, dx, dy, dz,\n 2 * (-cx * dx + -cy * dy + -cz * dz) AS b,\n (cx * cx + cy * cy + cz * cz - cr * cr) AS c,\n 2 * (-cx * dx + -cy * dy + -cz * dz) * 2 * (-cx * dx + -cy * dy + -cz * dz)\n - 4 * (cx * cx + cy * cy + cz * cz - cr * cr) AS disc\n FROM (\n SELECT\n x, y, u, v,\n u / len AS dx, v / len AS dy, -1.0 / len AS dz\n FROM (\n SELECT\n x, y, u, v, sqrt(u * u + v * v + 1.0) AS len\n FROM (\n SELECT\n toInt32(number % size) AS x,\n toInt32(intDiv(number, size)) AS y,\n (toFloat64(number % size) / size - 0.5) * 2 AS u,\n (0.5 - toFloat64(intDiv(number, size)) / size) * 2 AS v\n FROM numbers(size * size)\n )\n )\n )\n )\n )\n )\n )\n)\nORDER BY y, x\nFORMAT PNG\nSETTINGS output_format_image_width = 256, output_format_image_height = 256",
"specVersion": 1,
"spec": {
"name": "Image: ray-traced sphere (256×256)",
"description": "An original, minimal analytic ray/sphere-intersection scene with Lambertian shading over a sky-gradient background, rendered entirely in SQL as an RGB FORMAT PNG. Inspired by ClickHouse's SQL ray-tracing demos (github.com/ClickHouse/RayTracer) — not a copy of that project's code. CPU cost grows with the pixel count (size×size rows); keep size small (256 here) unless you deliberately want a slower, higher-resolution render — a 1024 render can take noticeably longer. Needs ClickHouse 26.6+ for FORMAT PNG.",
"favorite": true,
"view": "panel",
"panel": {
"cfg": {
"type": "image",
"fit": "contain",
"background": "theme",
"alt": "A shaded sphere lit from one side, rendered by ray-tracing math in SQL, over a gradient sky background"
}
}
}
}
]
}
53 changes: 52 additions & 1 deletion schemas/generated/library-v2.bundle.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,56 @@
"content"
]
},
{
"title": "Image",
"description": "A single ClickHouse `FORMAT PNG` result rendered as an image. The authored SQL must end in an explicit FORMAT PNG clause.",
"x-altinity-status": "implemented",
"x-altinity-snippet": {
"type": "image"
},
"properties": {
"type": {
"const": "image"
},
"fit": {
"title": "Image fit",
"description": "contain scales the image to fit within the tile preserving aspect ratio, cover fills the tile and crops overflow, and actual renders at intrinsic CSS pixel size in a scrollable body.",
"type": "string",
"enum": [
"contain",
"cover",
"actual"
],
"default": "contain"
},
"background": {
"title": "Image background",
"description": "theme uses the surrounding surface color, transparent shows nothing behind the image, and checkerboard renders a checkerboard pattern (useful for a transparent PNG).",
"type": "string",
"enum": [
"transparent",
"checkerboard",
"theme"
],
"default": "theme"
},
"alt": {
"title": "Alt text",
"description": "Accessible description for the image. Falls back to the tile/query title when absent.",
"type": "string"
}
},
"required": [
"type"
],
"additionalProperties": true,
"x-altinity-order": [
"type",
"fit",
"background",
"alt"
]
},
{
"title": "Future panel type",
"description": "Forward-compatible storage branch for a type implemented by a newer build.",
Expand All @@ -1082,7 +1132,8 @@
"kpi",
"table",
"logs",
"text"
"text",
"image"
]
}
}
Expand Down
Loading
Loading