⚠️ Reconciled 2026-06-30 (#88 / #21). With the editor moving to CodeMirror 6 behind an EditorPort seam (#21), this lands on CM6, not the textarea: completion is hosted by a CM6 completion source (@codemirror/lang-sql schemaCompletionSource + a custom source) instead of editor-complete.js + the textarea dropdown. Depends on #21 (sequence: signals → #21 → this; #68 Phase 4). The pure src/core/from-scope.js below (alias / FROM-scope resolution + unqualified-column scoping) is editor-agnostic, feeds the CM6 source, and remains the core of this issue. The textarea-hosting details in Proposed solution / Scope (editor-complete.js, dropdown refresh) are superseded by the CM6 adapter; the FROM-scope/alias/scoping logic and the Acceptance criteria stand.
Part of #68 (Roadmap to 1.0.0) — Editor intelligence track.
Problem
Column-name autocompletion technically exists but almost never fires in practice. The engine already emits column candidates for any loaded table (src/core/completions.js:102), already filters qualified table. to that table's columns (completions.js:195), and the dropdown already renders columns with their type. But columns are only fetched when the user manually expands that table in the schema sidebar (src/ui/schema.js:116 → src/ui/app.js:375 loadColumns) — nobody does that mid-typing, so the candidate pool is almost always empty. On top of that:
- Aliases don't resolve. The qualified filter matches
ctx.parent literally, so events. works but e. from FROM events e does not (parent is "e", no such table).
- Unqualified columns aren't scoped. A bare word competes against columns from every loaded table globally with a flat boost (
completions.js:215) — no notion of which tables are in the current statement's FROM.
Proposed solution
Make columns available while typing, driven by the statement's FROM/JOIN clause, and make completion FROM-aware. Two settled decisions:
- FROM-driven lazy loading. Parse the current statement's FROM/JOIN tables; load their
system.columns on a debounced idle tick — metadata-only, deduped via the existing 'loading' sentinel, cached per connection, and never on the keystroke path (our standing editor rule). Refresh the open dropdown when columns arrive.
- Full FROM awareness. Resolve aliases (
e. → events) and scope/boost unqualified column suggestions to the statement's in-scope tables.
Scope
src/core/from-scope.js (new, 100% covered) — pure: given editor text + caret, return the { db, table, alias }[] for the statement containing the caret (handle db.table, table alias, table AS alias, multiple comma joins and JOINs; reuse the existing SQL tokenizer so strings/comments don't fool it). This single module feeds all three gaps.
src/core/completions.js — qualified path resolves ctx.parent through the alias map before filtering; unqualified path scopes/boosts columns to in-scope tables instead of the global pool. Keep current behavior when no FROM scope is available.
- Editor-driven column loading in
src/ui/app.js + src/ui/editor-complete.js — on a debounced idle tick (not per keystroke), diff the statement's in-scope tables against schema and call the existing ch.loadColumns for any whose columns are still null; mark 'loading' to dedupe; on completion rebuildCompletions() and refresh the open dropdown. No change to src/net/ch-client.js (reuses loadColumns).
- State — no new fields needed; columns continue to live on
tableObj.columns (src/state.js:53 schema). The debounce timer/handle lives with the editor-complete host.
Non-goals (v1)
- CTE / subquery-derived column scopes,
USING/correlated-subquery resolution, SELECT * expansion — keep to real base tables in FROM/JOIN.
- Prefetch-all-on-connect (rejected: large payload on big catalogs); revisit as a hybrid later if needed.
Acceptance
Part of #68 (Roadmap to 1.0.0) — Editor intelligence track.
Problem
Column-name autocompletion technically exists but almost never fires in practice. The engine already emits column candidates for any loaded table (
src/core/completions.js:102), already filters qualifiedtable.to that table's columns (completions.js:195), and the dropdown already renders columns with their type. But columns are only fetched when the user manually expands that table in the schema sidebar (src/ui/schema.js:116→src/ui/app.js:375loadColumns) — nobody does that mid-typing, so the candidate pool is almost always empty. On top of that:ctx.parentliterally, soevents.works bute.fromFROM events edoes not (parentis"e", no such table).completions.js:215) — no notion of which tables are in the current statement's FROM.Proposed solution
Make columns available while typing, driven by the statement's FROM/JOIN clause, and make completion FROM-aware. Two settled decisions:
system.columnson a debounced idle tick — metadata-only, deduped via the existing'loading'sentinel, cached per connection, and never on the keystroke path (our standing editor rule). Refresh the open dropdown when columns arrive.e.→events) and scope/boost unqualified column suggestions to the statement's in-scope tables.Scope
src/core/from-scope.js(new, 100% covered) — pure: given editor text + caret, return the{ db, table, alias }[]for the statement containing the caret (handledb.table,table alias,table AS alias, multiple comma joins andJOINs; reuse the existing SQL tokenizer so strings/comments don't fool it). This single module feeds all three gaps.src/core/completions.js— qualified path resolvesctx.parentthrough the alias map before filtering; unqualified path scopes/boosts columns to in-scope tables instead of the global pool. Keep current behavior when no FROM scope is available.src/ui/app.js+src/ui/editor-complete.js— on a debounced idle tick (not per keystroke), diff the statement's in-scope tables againstschemaand call the existingch.loadColumnsfor any whosecolumnsare stillnull; mark'loading'to dedupe; on completionrebuildCompletions()and refresh the open dropdown. No change tosrc/net/ch-client.js(reusesloadColumns).tableObj.columns(src/state.js:53schema). The debounce timer/handle lives with the editor-complete host.Non-goals (v1)
USING/correlated-subquery resolution,SELECT *expansion — keep to real base tables in FROM/JOIN.Acceptance
FROM db.table(without sidebar-expanding it) offers that table's columns, unqualified, scoped to the statement.e.afterFROM events e(and... events AS e) offersevents' columns.db.table.andtable.qualification still work.npm testgreen at the per-file coverage gate (from-scope.js+completions.jsat 100%).