Skip to content
Merged
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
## [Unreleased]

### Changed
- **The SQL editor is now CodeMirror 6** (#21) — the deliberate 4th bundled
runtime dependency, replacing the hand-rolled textarea editor wholesale
behind the #143 `EditorPort` seam (`src/editor/codemirror-adapter.js`;
`main.js` swaps one injected factory). What changes for users: **per-tab
undo history** (the shared textarea undo stack couldn't do this), real
IME/touch editing, CM6's find/replace panel (`⌘F`), and measured-text
rendering (no more fixed-glyph-width geometry). Highlighting still tracks
the connected server's `system.keywords`/`functions` — the sets now feed a
ClickHouse `SQLDialect` swapped via a Compartment on connect — and
completion keeps the pure `core/completions.js` candidate set + ranking
(CM6 renders the UI; `filter: false` preserves our order). Global shortcuts
(`⌘↵` run, `⌘⇧↵` format, `⌘S`/`⌘⇧S`) stay on the document handler — CM6's
conflicting `Mod-Enter` binding is stripped so an open completion can never
swallow the run chord. Deleted with the cutover: the textarea adapter,
`editor-complete/intel/search`, `core/editor-{marks,geometry,brackets,search}`
and the `maskLiterals`-based literal masking (~2,600 LOC incl. tests) —
execCommand undo, four-way scroll sync, and the editor's `html{zoom}`
popover bridging all go with them. Signature help is dropped in this parity
v0 (#60 rebuilds docs properly); function docs show as the completion info
tooltip and on hover. Bundle: **+402,911 B raw (+83%) / +132,903 B gzip
(+85%)** (484,674 → 887,585 raw; 155,810 → 288,713 gzip) — over the issue's
raw estimate, accepted at the plan gate as the price of the Phase-4 editor
foundation (#84 schema-aware autocomplete builds directly on this).
- **The SQL editor now sits behind an injected `EditorPort` seam** (#143): the
hand-rolled textarea editor moved from `src/ui/` to `src/editor/` and is the
first adapter (`createTextareaEditor`) of a small port interface
Expand Down
17 changes: 10 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Contributor guide — altinity-sql-browser

A modular ES-module SPA that builds to one self-contained HTML file served from
ClickHouse. No framework; runtime deps are rare and deliberate (currently three,
ClickHouse. No framework; runtime deps are rare and deliberate (currently four,
all bundled — see hard rule 4). Quality is held by tests.

## Hard rules
Expand All @@ -25,7 +25,8 @@ all bundled — see hard rule 4). Quality is held by tests.
(see README "Configuring OAuth").
4. **The build is esbuild only; runtime deps are rare and deliberate.** Source
files are the tested files; esbuild bundles `src/main.js` → `dist/sql.html`.
There are **three** bundled runtime dependencies — **Chart.js** (the Chart
There are **four** bundled runtime dependencies — **CodeMirror 6** (the SQL
editor, behind the `EditorPort` seam — #21), **Chart.js** (the Chart
result view), **@dagrejs/dagre** (the EXPLAIN pipeline-graph layout), and
**@preact/signals-core** (the reactivity primitive — see
`docs/ADR-0001-reactivity.md`) — all inlined into the artifact, so the page
Expand All @@ -35,8 +36,10 @@ all bundled — see hard rule 4). Quality is held by tests.
keep the testable logic pure in `src/core/` (chart axis/role/pivot math in
`src/core/chart-data.js`; DOT→positions in `src/core/dot-layout.js`, both
100%-covered) and make the library call an **injected seam** (`app.Chart` /
`app.Dagre`, like the fetch/crypto seams) so the DOM wrapper stays fully tested
rather than dropping below the coverage gate.
`app.Dagre` / `env.Editor`, like the fetch/crypto seams) so the DOM wrapper
stays fully tested rather than dropping below the coverage gate. (The CM6
adapter is the port-shaped variant: the *factory* is injected, and the
adapter is unit-tested against the real library under happy-dom.)
5. **No UI framework; signals for state, imperative adapters for islands.** State
reactivity is `@preact/signals-core` (`signal`/`effect`/`computed`/`batch`),
migrated slice-by-slice (ADR-0001). **No React/Preact/Solid** — a Preact spike
Expand All @@ -45,9 +48,9 @@ all bundled — see hard rule 4). Quality is held by tests.
paradigm the roadmap doesn't justify. The hard, third-party, or
high-frequency-pointer surfaces (the editor, the EXPLAIN/schema graphs,
Chart.js, result-grid resize/sort) stay **imperative behind an injected seam** —
signals coordinate state, they don't own every mousemove. CodeMirror 6 is the
pre-approved next runtime dep, behind an `EditorPort` seam, to land when
schema-aware autocomplete (#84) does (#21). When a *second* consumer of a
signals coordinate state, they don't own every mousemove. The editor is
**CodeMirror 6** behind the `EditorPort` seam (#21, landed ahead of #84 —
the completion source swaps to from-scope data there). When a *second* consumer of a
complex UI pattern appears, extract a shared primitive (e.g. `EditorPort`,
`GraphSurface`, a result-view registry, `Drawer`) rather than copy it — but
don't build a primitive speculatively for a single caller.
Expand Down
81 changes: 44 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ schema-aware autocomplete, streaming results with table / JSON / chart views,
saved queries, history, and shareable links. It ships as a
**single self-contained HTML file served from ClickHouse itself** (no Node
server, no CDN, no external fonts) — the page makes **zero third-party
requests** and renders in the OS's native UI font. Its two bundled runtime
dependencies — **Chart.js** (the chart result view) and **@dagrejs/dagre** (the
EXPLAIN pipeline-graph layout) — are inlined into that one file.
requests** and renders in the OS's native UI font. Its four bundled runtime
dependencies — **CodeMirror 6** (the SQL editor), **Chart.js** (the chart
result view), **@dagrejs/dagre** (the EXPLAIN pipeline-graph layout), and
**@preact/signals-core** (state reactivity) — are inlined into that one file.

Refactored from a single-file SPA into a fully modular, test-first codebase
held at **100% test coverage**.
Expand All @@ -34,29 +35,37 @@ only moving parts are ClickHouse's HTTP handlers and your OAuth provider.

## SQL editor

The editor is a hand-rolled `<textarea>` over a syntax-highlighted `<pre>` (no
editor library — it adds nothing to the single served file). On top of that:
The editor is **CodeMirror 6** behind an injected `EditorPort` seam (#143/#21)
— bundled and inlined like the other runtime deps, so the page still makes
zero third-party requests. On top of it:

- **Find / replace** — `Cmd/Ctrl+F` opens a panel with a live match count,
prev/next (Enter / Shift+Enter), case / whole-word / regex toggles, and a
replace row. Matches highlight via a transparent overlay layered below the
syntax tokens, so highlighting and search never interfere.
- **Per-tab undo** — each query tab keeps its own edit history; switching tabs
parks and restores it.
- **Find / replace** — `Cmd/Ctrl+F` opens CM6's search panel (app-styled) with
prev/next, case / whole-word / regex toggles, and replace.
- **Bracket matching + auto-close** — typing `(` `[` or a quote inserts the
pair (or wraps the selection); typing a closer or quote steps over it;
Backspace inside an empty pair deletes both. The pair adjacent to the caret
is highlighted. (`{`/`}` auto-close is intentionally omitted.)
- **Autocomplete** — typing a word (or after `table.`) opens a ranked dropdown
of keywords, functions, databases, tables, and already-loaded columns;
↑/↓/Enter/Tab/Esc and click to accept; functions insert `name(`.
- **Signature help + hover docs** — inside a function call, a popover shows the
signature with the active argument bolded; hovering a function or a
ClickHouse keyword shows its signature/description. Both read the same cached
reference data — `system.functions.{syntax,description}` (loaded with #25) and
a small built-in keyword-doc set — so they never query on the keystroke path.
Backspace inside an empty pair deletes both; the pair adjacent to the caret
is highlighted. Auto-close stays quiet inside strings and comments, and
`{`/`}` is intentionally omitted — it would fight the `{name:Type}` query
variables.
- **Autocomplete** — typing a word (or after `table.`) opens a ranked list of
keywords, functions, databases, tables, and already-loaded columns —
the candidate set and ranking are the app's own (`core/completions.js`),
rendered through CM6's completion UI; ↑/↓/Enter/Tab/Esc and click to accept;
functions insert `name()` with the caret between the parens, and the active
row's description shows in an info tooltip.
- **Hover docs** — hovering a function or a ClickHouse keyword shows its
signature/description from the same cached reference data —
`system.functions.{syntax,description}` (loaded with #25) and a small
built-in keyword-doc set — so they never query on the keystroke path.
(In-call signature help was dropped in the CM6 parity cut; the reference
docs pane (#60) rebuilds it properly.)
- **Drag to insert** — drag a schema table/column, or a **Library/History** row,
onto the editor: a schema identifier drops as text at the caret, and a
saved/history query drops as a `( … )` subquery at the drop point (its trailing
`FORMAT`/`;` stripped). Undoable; click-to-load still works for keyboard users.
onto the editor: a schema identifier drops as text at the drop point (the
drop cursor tracks the pointer), and a saved/history query drops there as a
`( … )` subquery (its trailing `FORMAT`/`;` stripped). Undoable;
click-to-load still works for keyboard users.
Dragging a **database or table onto the results pane** instead renders a
[data flow graph](#data-flow-graph).
- **Query variables** — write a ClickHouse typed placeholder like
Expand All @@ -77,8 +86,8 @@ the server's keyword and function lists — is fetched **once per connection**
from `system.keywords` and `system.functions` (best-effort; it falls back to a
built-in set on older ClickHouse), cached in memory, and merged with the
in-memory schema. Highlighting then tracks the connected server's actual
keyword/function set, so it's version-correct. Folding and multi-cursor are out
of scope for a textarea and tracked separately (CodeMirror, issue #21).
keyword/function set — the lists feed a ClickHouse `SQLDialect` that is
reconfigured on connect — so it's version-correct.

> Design source of truth: the "Altinity Play" Claude Design project (external).
> Production is the vanilla ES-module code under `src/` — there is no React in
Expand Down Expand Up @@ -418,14 +427,13 @@ Preview the rendered artifacts without touching ClickHouse:
```
src/
core/ pure logic — format, jwt, pkce, sql-highlight, share, sort,
stream, storage, chart-data, and the editor logic: completions
(reference data + ranking), editor-search (find), editor-brackets
(match/auto-close), editor-marks (overlay), editor-geometry
(caret) — no DOM, no globals
stream, storage, chart-data, completions (editor reference data
+ ranking) — no DOM, no globals
net/ oauth-config, oauth, ch-client (injected fetch seam)
ui/ dom (hyperscript), icons, + render modules (login, editor +
editor-search/editor-complete, tabs, schema, results,
saved-history, shortcuts, splitters, toast, app)
editor/ the EditorPort seam (editor-port) + its CodeMirror 6 adapter
(codemirror-adapter) — injected via createApp(env)
ui/ dom (hyperscript), icons, + render modules (login, tabs, schema,
results, saved-history, shortcuts, splitters, toast, app)
state.js state model + pure operations
main.js bootstrap (OAuth callback, share-links, initial render)
styles.css
Expand Down Expand Up @@ -453,7 +461,7 @@ Engines do diverge on one thing — **viewport units under `zoom`**: Chromium's
on both (#70). An engine that can't parse `zoom` at all falls back via
`@supports not (zoom: 1)` to a consistent 1× layout.

CI exercises the editor-alignment, editor-insertion, schema-graph and
CI exercises the editor (CM6 behaviors + insertion paths), schema-graph and
EXPLAIN-pipeline specs on all three engines (`webkit` added in #69), plus a
panel-sizing spec. **Caveat:** Playwright's WebKit applies `zoom` to
`getBoundingClientRect`/viewport units like Chromium, *not* like real Safari, so
Expand Down Expand Up @@ -495,11 +503,10 @@ suite needs no mocking libraries.

### End-to-end (real browser)

happy-dom has no real layout or scrollbars, so render-layer bugs (e.g. the
editor highlight drifting behind the selection when a scrollbar shrinks the
textarea's client box) can't be caught by the unit suite. A small Playwright
harness mounts the real `src/` modules in **Chromium, Firefox and WebKit** for
those cases — WebKit is the Safari proxy and the engine most likely to diverge
happy-dom has no real layout or scrollbars, so render-layer bugs (keyboard
routing through the real engine, completion popup timing, drop-point geometry)
can't be caught by the unit suite. A small Playwright harness mounts the real
`src/` modules in **Chromium, Firefox and WebKit** for those cases — WebKit is the Safari proxy and the engine most likely to diverge
on the `html{zoom}`-based layout (see [Supported browsers](#supported-browsers)).

```bash
Expand Down
36 changes: 33 additions & 3 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Third-party notices

The Altinity SQL Browser is licensed under Apache-2.0 (see `LICENSE`). The built
single-file artifact (`dist/sql.html`) inlines the three runtime dependencies
below; this file reproduces their MIT license texts as required, and the same
notices are embedded as a comment at the top of the built artifact.
single-file artifact (`dist/sql.html`) inlines the four runtime dependencies
below (CodeMirror 6 ships as several packages); this file reproduces their MIT
license texts as required, and the same notices are embedded as a comment at
the top of the built artifact.

---

Expand Down Expand Up @@ -46,3 +47,32 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

---

## CodeMirror 6 (the SQL editor) and its dependencies

The editor is composed of these MIT-licensed packages, all inlined into the
artifact. Each carries the license text below with its own copyright line:

- `@codemirror/state` — v6.7.0, `@codemirror/view` — v6.43.4,
`@codemirror/commands` — v6.10.4, `@codemirror/language` — v6.12.4,
`@codemirror/lang-sql` — v6.10.0, `@codemirror/autocomplete` — v6.20.3,
`@codemirror/search` — v6.7.1 — Copyright (C) 2018-2021 by Marijn Haverbeke
<marijn@haverbeke.berlin> and others
- `@lezer/common` — v1.5.2, `@lezer/highlight` — v1.2.3, `@lezer/lr` — v1.4.10,
`style-mod` — v4.1.3 — Copyright (C) 2018 by Marijn Haverbeke
<marijn@haverbeke.berlin> and others
- `w3c-keyname` — v2.2.8 — Copyright (C) 2016 by Marijn Haverbeke
<marijn@haverbeke.berlin> and others
- `crelt` — v1.0.7 — Copyright (C) 2020 by Marijn Haverbeke <marijn@haverbeke.berlin>
- `@marijn/find-cluster-break` — v1.0.3 — Copyright (C) 2024 by Marijn Haverbeke
<marijn@haverbeke.berlin>

MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 changes: 21 additions & 0 deletions docs/ADR-0001-reactivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,24 @@ edit, not a new `effect()`. Housing them in `state.js` rather than on `app`
matches every other slice in this file; #88's "last slice" note above refers to
the schema panel specifically, not every remaining plain field in the
codebase — this is a smaller, unrelated follow-up tidying three of those.

## Addendum — the editor is CodeMirror 6 behind the EditorPort seam (#143/#21)

The decision's "imperative adapters behind injected seams for the hard,
third-party, or high-frequency-pointer surfaces" clause is now realized for the
editor: #143 extracted the `EditorPort` interface (mount / getValue /
getSelection / insertAtCursor / replaceDocument / revealOffset / syncFromState /
refreshReference / onDocChange, injected as `env.Editor`), and #21 swapped the
adapter from the hand-rolled textarea to **CodeMirror 6** — the fourth bundled
runtime dependency. Signals still coordinate the state (`hasSelection`,
tab/effect wiring, the app-level `onDocChange` subscriber owns the
`tab.sql`/dirty writes); CM6 owns every keystroke, selection, undo, and
measurement inside the port. Per-tab `EditorState`s give per-tab undo — a
capability the shared-textarea design structurally lacked — and the adapter is
unit-tested against the real CM6 under happy-dom (the coverage gate holds
without a fake-editor seam). The tokenizer stays in `core/sql-highlight.js` for
completion context; the CM6 dialect gets the same server keyword/function sets
via a `Compartment` reconfigure. Nothing about the state model changed — this
addendum records that the editor island now has its intended long-term
implementation, and that #84 (schema-aware autocomplete) plugs into the CM6
completion source rather than growing new overlay machinery.
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@
"vitest": "^2.1.8"
},
"dependencies": {
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.10.4",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/language": "^6.12.4",
"@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.7.0",
"@codemirror/view": "^6.43.4",
"@dagrejs/dagre": "^3.0.0",
"@lezer/highlight": "^1.2.3",
"@preact/signals-core": "^1.14.3",
"chart.js": "^4.5.1"
}
Expand Down
26 changes: 0 additions & 26 deletions src/core/completions.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,29 +239,3 @@ export function wordAt(value, pos) {
if (s === e) return null;
return { word: value.slice(s, e), from: s, to: e };
}

/**
* If `pos` is inside a function call `name(… )`, return {name, argIdx} — the
* enclosing function and which argument the caret is on (commas counted at
* depth 0). Used by signature help (#27). Returns null outside a call; a `;` or
* newline at depth 0 ends the search (don't cross statements/lines).
*/
export function signatureContext(value, pos) {
let depth = 0;
let argIdx = 0;
for (let i = pos - 1; i >= 0; i--) {
const c = value[i];
if (c === ')') depth++;
else if (c === '(') {
if (depth === 0) {
let e = i;
while (e > 0 && /[A-Za-z0-9_]/.test(value[e - 1])) e--;
const name = value.slice(e, i);
return name ? { name, argIdx } : null;
}
depth--;
} else if (c === ',' && depth === 0) argIdx++;
else if ((c === ';' || c === '\n') && depth === 0) return null;
}
return null;
}
Loading