Found while manually testing #427. #428 will hit this, so it needs deciding before copy-on-drag ships.
The shape
mergeDashboardFilterHelpers (src/core/dashboard-filters.ts) attributes helper columns to parameters globally by name: it collects every provider's helpers, groups by column name, and reports filter-duplicate-provider when one name has more than one provider. That was correct when a source was shared — #359 made N filters on one source resolve to ONE provider, so each name had exactly one.
#427 changed the topology. A filter source is now cloned per Dashboard, and #428 will clone one per drop. As soon as a workspace holds two copies of one option source in the same Dashboard, every column they both emit has two providers.
#427 added a partial mitigation: content-identical providers collapse to one instead of erroring (helperKey). That covers a stable option list, but not a volatile one. Reproduced live against examples/clickhouse-operations.json after an intermediate build wrote one copy per filter:
Multiple Filter queries provide "query_hash": Grafana port filters.
Six copies of gco-filter each ran separately against system.query_log; five columns matched, but query_hash genuinely differed between executions, so it is not deduplicable. Every such column takes its filter out of service.
The robust fix
Attribute per filter, not globally. A curated filter declares its own parameter and owns its own sourceQueryId, so the option list for parameter P should come from the source of the filter that declares P — never from "whichever provider happened to emit a column named P". Under #427's ownership that mapping is unambiguous by construction.
Sketch: pass the filter→source relation into the merge (the viewer session already has it — dashboard-viewer-session.ts builds FilterRuntime with both def.parameter and state.sourceId), and resolve fields[P] from that filter's own provider. filter-duplicate-provider then only fires for a genuinely unattributable column, and the helperKey dedup becomes belt-and-braces.
Deliberately NOT done in #427: it is a change to the #189/#359/#360 filter machinery, the most intricate code in the app, and #427 does not need it — a correctly migrated workspace has one source per Dashboard and therefore one provider per column.
Also worth deciding here
Should #428 reuse a Dashboard's existing copy when the user drops the same Library filter query onto a second filter of that Dashboard, rather than minting a second copy? That matches what #427's migration does and would avoid creating this state at all. The ownership invariant already allows it (ownersAreValid accepts several curated-filter owners within one Dashboard).
Found while manually testing #427. #428 will hit this, so it needs deciding before copy-on-drag ships.
The shape
mergeDashboardFilterHelpers(src/core/dashboard-filters.ts) attributes helper columns to parameters globally by name: it collects every provider's helpers, groups by column name, and reportsfilter-duplicate-providerwhen one name has more than one provider. That was correct when a source was shared — #359 made N filters on one source resolve to ONE provider, so each name had exactly one.#427 changed the topology. A filter source is now cloned per Dashboard, and #428 will clone one per drop. As soon as a workspace holds two copies of one option source in the same Dashboard, every column they both emit has two providers.
#427 added a partial mitigation: content-identical providers collapse to one instead of erroring (
helperKey). That covers a stable option list, but not a volatile one. Reproduced live againstexamples/clickhouse-operations.jsonafter an intermediate build wrote one copy per filter:Six copies of
gco-filtereach ran separately againstsystem.query_log; five columns matched, butquery_hashgenuinely differed between executions, so it is not deduplicable. Every such column takes its filter out of service.The robust fix
Attribute per filter, not globally. A curated filter declares its own
parameterand owns its ownsourceQueryId, so the option list for parameter P should come from the source of the filter that declares P — never from "whichever provider happened to emit a column named P". Under #427's ownership that mapping is unambiguous by construction.Sketch: pass the filter→source relation into the merge (the viewer session already has it —
dashboard-viewer-session.tsbuildsFilterRuntimewith bothdef.parameterandstate.sourceId), and resolvefields[P]from that filter's own provider.filter-duplicate-providerthen only fires for a genuinely unattributable column, and thehelperKeydedup becomes belt-and-braces.Deliberately NOT done in #427: it is a change to the #189/#359/#360 filter machinery, the most intricate code in the app, and #427 does not need it — a correctly migrated workspace has one source per Dashboard and therefore one provider per column.
Also worth deciding here
Should #428 reuse a Dashboard's existing copy when the user drops the same Library filter query onto a second filter of that Dashboard, rather than minting a second copy? That matches what #427's migration does and would avoid creating this state at all. The ownership invariant already allows it (
ownersAreValidaccepts several curated-filter owners within one Dashboard).