Repository navigation
Restore the searchable multi-select for Array(scalar T) Dashboard variables with option SQL #468
Description
Reverses one #447 owner decision, for a narrow case. Restores the control from #189 / PR #364.
Context
Array(T) is the type a multi-select exists for. But #447 phase 1 removed the curated filter model wholesale — its Non-goals list "Multiselect or Array-valued variable controls", and its phase-1 acceptance gate required that nothing refer to "arrays, or multiselect". Phase 2 then classified every container type as having no inferred control.
The result, hit in the running app: a user : Array(String) variable on the ClickHouse Operations dashboard, given a perfectly good option query
SELECT DISTINCT initial_user, initial_user
FROM merge(system, '^query_log')
WHERE event_time >= now() - INTERVAL 7 DAY AND initial_user != ''stored the SQL correctly and ran it fine in its own Variable: user tab — 13 rows — but the Dashboard rendered a free-text box, never ran the query, and showed "Array(String) is a container type — type a literal value".
The value pipeline never lost the ability to serve this: param-serialize.ts has always turned a JS string[] into a ClickHouse array literal, and dashboard/model/dashboard-filter-store.ts survived #447 with its string | string[] round-trip intact.
Goal
An Array of a scalar with option SQL renders the searchable multi-select. Every other container is unchanged.
Decisions
| Value model | Real string[] end to end, not a stringified literal |
| UX | Full #189 control: search, tri-state Select visible, Clear/Cancel/Apply, draft-until-Apply, focus return, refresh reconciliation |
| Surfaces | Dashboard only. The Workbench {name:Type} strip is untouched and state.varValues stays Record<string, string> |
| Eligibility | Array(scalar T) only. Tuple/Map/Nested/Array(Array(T)) keep today's text field + marker |
Design
- One predicate,
multiSelectElementType(core/param-type.ts), is read by BOTHcore/variable-options.ts's batch filter andfieldControlKind's control choice — so a type whose option SQL ran can never be one the bar refuses to render a select for. fieldControlKindgains'multi', classifying the type;filter-bar.tspairs that withspec.options !== null, because only the bar can see whether option SQL was configured.- Arrays travel viewer session →
VariableFieldSpec.selection→ control →onCommitVariableSelection. They never enterFilterBarApp.state.varValues. Widening that type would not have been a safeguard — TS property assignability is covariant even for mutable properties — so keeping it narrow is the enforcement. - An empty selection reduces to
UNSET_VALUE, not[]:emptyValue()treats a present[]as a real value, so binding one would run panels as… IN []— nothing returned, but looking filtered. This narrows Dashboard query-backed filters: searchable multiselect with Apply #189, which could express an "active empty array". - Reconciliation returns names;
refreshruns one coalesced wave after both the option request and the tile pool settle.
Latent bug fixed
dashboard-viewer-session.ts marked every Array(T) variable with valid option SQL status: 'error' through a branch commented "Unreachable via optionBatchVariables' own rule". It was reachable, and wrong; it was invisible only because the select it applied to never rendered. Admitting Array(scalar T) into the batch makes the comment true of the remaining containers, and the message now says what is actually wrong.
Supersedes
- Replace curated Dashboard filters with inferred Variables and batched option queries #447 Non-goals → "Multiselect or Array-valued variable controls" no longer holds for
Array(scalar T)with option SQL. - Replace curated Dashboard filters with inferred Variables and batched option queries #447 Dashboard controls and binding → "unsupported compound types such as
Array,Tuple,Map, orNested-> local unsupported-type diagnostic" gains that carve-out. - Replace curated Dashboard filters with inferred Variables and batched option queries #447 phase-1 acceptance gate → the "arrays, or multiselect" clause was a phase-1 removal gate; it stands for the curated model, not for this control.
Recovery base for the deleted code: ac96b4a^ (= a87c45a).
Acceptance
-
Array(scalar T)+ option SQL → searchable multi-select; the option batch runs for it -
Tuple/Map/Nested/Array(Array(T))unchanged (no batch, no select, same marker) -
Array(scalar T)with no option SQL keeps its text field, with a marker naming the fix - Selections bind as a real array literal via the shared typed serializer
- Empty selection = unset; panels wait
- Refresh reconciles by bound value; at most one wave; no auto-select of new options
- Selections persist across reload
-
state.varValuesstaysRecord<string, string>; the Workbench strip is untouched - 100% coverage on every touched gated file; e2e across Chromium/Firefox/WebKit
Activity
fix(#468): close three selection-destroying paths found in review
docs(#468): record the corrected selection-safety semantics
fix(#468): Apply must also preserve off-list values from a truncated …
Merge pull request #469 from Altinity/feat/restore-array-multiselect
BorisTyshkevich commentedon Jul 27, 2026
Back-reference (2026-07-27), for traceability only — nothing to do here.
249f7a0in this issue's PR #469 (fix(#468): close three selection-destroying paths found in review) also fixed #461's second reported defect as collateral, withouteither issue recording it.
That commit switched the option-batch truncation flag from the KEPT count to the RAW
row count in
readVariableOptionBatch(src/core/variable-options.ts:432-441). Underthis issue's lens that was selection preservation — don't prune a committed selection
against a list that is silently partial. But it is also exactly what #461 asked for:
a non-
DISTINCToption query returning 1,001 rows that collapse to a handful ofdistinct values is now reported as truncated instead of presented as complete.
The only trace was the
(#461)reference inside the code comment, so #461 read asunimplemented from the outside for a day. #461's body now carries the provenance.
One consequence worth noting: because the fix arrived under this issue's lens rather
than #461's, it was tested for selection preservation and not for #461's collapse
case. The remaining work — a regression test proving a raw-count-driven flag, which
the current distinct-value test cannot distinguish from kept-count logic — stays on
#461.
feat(Altinity#468): restore the searchable multi-select for Array(sca…
refactor(Altinity#459): fix review findings — a wrong rename, stale p…