Repository navigation
feat: Vega crossfilter in notebook cells - #488
Conversation
Signed-off-by: Ilya Boyandin <[email protected]>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds store-backed Vega cross-filtering (brush selections → SQL predicates), a generalized data-source selector ( Changes
Sequence DiagramsequenceDiagram
participant User
participant VegaChart as "Vega Chart"
participant SelectionListener as "Selection Listener"
participant Store as "Redux Store"
participant QueryEngine as "Query Engine"
participant ConfigPanel as "Config Panel"
User->>VegaChart: brush / select
VegaChart->>SelectionListener: emit BRUSH_PARAM_NAME signal
SelectionListener->>SelectionListener: debounce & normalize → CrossFilterSelection
SelectionListener->>Store: setCrossFilterSelection(chartId, sqlId, selection)
Store->>Store: update crossFilterSelections
Store->>Store: getCrossFilterPredicate(chartId, sqlId)
Store-->>VegaChart: predicate (or null)
VegaChart->>QueryEngine: request data with SQL (+ WHERE predicate)
QueryEngine->>VegaChart: return filtered data
User->>ConfigPanel: toggle cross-filter / select brush field
ConfigPanel->>VegaChart: rebuild spec (flat ↔ layered with BRUSH_PARAM_NAME)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/cells/src/components/VegaCellContent.tsx`:
- Around line 82-89: The cleanup returned by the effect currently clears
timerRef and removes the Vega signal listener
(embed.view.removeSignalListener(BRUSH_PARAM_NAME, handler)) but does not clear
the stored selection in crossFilterSelections, so sibling charts keep filtering;
update the cleanup to also remove this chart's entry from crossFilterSelections
for the current sqlId and cellId (e.g., delete
crossFilterSelections[sqlId]?.[cellId] or set it to undefined) after removing
the listener, guarding for existence of sqlId/cellId and ensuring no other side
effects; keep existing timerRef and try/catch behavior around
removeSignalListener.
- Around line 165-172: selectedSqlQuery currently appends `WHERE
${crossFilterPredicate}` directly onto baseSqlQuery which can break raw SQL
(baseSqlQuery) that already contains WHERE/GROUP BY/ORDER BY/LIMIT or a trailing
semicolon; change the selectedSqlQuery logic (the construction that uses
crossFilterPredicate, baseSqlQuery, getCrossFilterPredicate, id, selectedSqlId)
so that when baseSqlQuery is a fallback raw SQL it is wrapped as a subquery
before adding the brush predicate (e.g. use `SELECT * FROM (${baseSqlQuery}) AS
_cells_base WHERE ...`) and only append the raw `WHERE ...` when you can detect
baseSqlQuery is a safe, single SELECT without trailing clauses/semicolon;
implement the wrapper path so crossFilterPredicate is always applied to a valid
SELECT.
- Around line 57-68: The selection payload uses the encoding channel key (e.g.
value.x) not the data field name, so replace the current lookup in
VegaCellContent.tsx that does const fieldValue = value[brushField] with reading
the channel value (e.g. const fieldValue = value.x or value[brushEncoding] if
you have a variable for the channel) and keep using brushField and
brushFieldType for building the SQL predicate and for the CrossFilterSelection
object; ensure the rest of the interval handling (Array.isArray check, length
=== 2, and constructing the CrossFilterSelection with field: brushField,
fieldType: brushFieldType, type: 'interval', value: fieldValue) remains
unchanged.
In `@packages/cells/src/components/VegaConfigPanel.tsx`:
- Around line 76-89: buildCrossFilterSpec currently always applies binning to
xField; change its signature to accept an xFieldType (e.g.,
'numeric'|'temporal'|'string') and only set bin: {maxbins: 20} when xFieldType
=== 'numeric'. Update the internal xEnc creation in buildCrossFilterSpec to
conditionally include bin based on that type. Then update the call site in
rebuild to pass detectFieldType(merged.xField) into buildCrossFilterSpec so the
builder receives the field type for correct bin application.
In `@packages/cells/src/vegaSelectionUtils.ts`:
- Line 23: quotedField currently wraps sel.field in double quotes but doesn't
escape embedded double quotes, causing invalid SQL for identifiers like a"b;
update the construction of quotedField to first escape all double-quote
characters in sel.field (replace each " with "") and then wrap the escaped
string in double quotes so generated aliases/fields are valid; modify the usage
around the quotedField definition in vegaSelectionUtils.ts where quotedField and
sel.field are handled.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e7731898-5103-4442-95c7-afceb578b43b
📒 Files selected for processing (7)
packages/cells/src/cellsSlice.tspackages/cells/src/components/VegaCellContent.tsxpackages/cells/src/components/VegaConfigPanel.tsxpackages/cells/src/defaultCellRegistry.tsxpackages/cells/src/index.tspackages/cells/src/types.tspackages/cells/src/vegaSelectionUtils.ts
| const fieldValue = value[brushField]; | ||
| if ( | ||
| fieldValue != null && | ||
| Array.isArray(fieldValue) && | ||
| fieldValue.length === 2 | ||
| ) { | ||
| const selection: CrossFilterSelection = { | ||
| field: brushField, | ||
| fieldType: brushFieldType, | ||
| type: 'interval', | ||
| value: fieldValue, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Vega-Lite, when an interval selection parameter is defined with select: { type: 'interval', encodings: ['x'] }, what is the shape of the emitted parameter value? Is the selected range stored under x or under the encoded field name?
💡 Result:
The emitted parameter value for an interval selection with select: { type: 'interval', encodings: ['x'] } is an object with a property named x that holds the selected data range as an array [min, max]. The selected range is stored under the encoding channel name x, not the encoded field name.
Citations:
- 1: https://vega.github.io/vega-lite/docs/parameter.html
- 2: https://vega.github.io/vega-lite/examples/interactive_brush.html
- 3: https://vega.github.io/vega-lite-v4/docs/init.html
- 4: https://vega.github.io/vega-lite/docs/selection.html
Use the encoding channel key instead of the field name to access the interval selection range.
The interval selection parameter emits values keyed by the encoding channel name (in this case x), not by the field name. Line 60 reads const fieldValue = value[brushField], but this will always be undefined because the payload has value.x, not value[brushField]. Read from value.x instead while keeping brushField for the SQL predicate construction.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/cells/src/components/VegaCellContent.tsx` around lines 57 - 68, The
selection payload uses the encoding channel key (e.g. value.x) not the data
field name, so replace the current lookup in VegaCellContent.tsx that does const
fieldValue = value[brushField] with reading the channel value (e.g. const
fieldValue = value.x or value[brushEncoding] if you have a variable for the
channel) and keep using brushField and brushFieldType for building the SQL
predicate and for the CrossFilterSelection object; ensure the rest of the
interval handling (Array.isArray check, length === 2, and constructing the
CrossFilterSelection with field: brushField, fieldType: brushFieldType, type:
'interval', value: fieldValue) remains unchanged.
| return () => { | ||
| if (timerRef.current) clearTimeout(timerRef.current); | ||
| try { | ||
| embed.view.removeSignalListener(BRUSH_PARAM_NAME, handler); | ||
| } catch { | ||
| // View may have been finalized | ||
| } | ||
| }; |
There was a problem hiding this comment.
Clear this chart's stored selection when the listener is torn down.
The cleanup only removes the Vega subscription. The last entry stays in crossFilterSelections[sqlId][cellId], so sibling charts keep filtering after this chart disables cross-filter, changes SQL source or brush field, or disappears from the sheet.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/cells/src/components/VegaCellContent.tsx` around lines 82 - 89, The
cleanup returned by the effect currently clears timerRef and removes the Vega
signal listener (embed.view.removeSignalListener(BRUSH_PARAM_NAME, handler)) but
does not clear the stored selection in crossFilterSelections, so sibling charts
keep filtering; update the cleanup to also remove this chart's entry from
crossFilterSelections for the current sqlId and cellId (e.g., delete
crossFilterSelections[sqlId]?.[cellId] or set it to undefined) after removing
the listener, guarding for existence of sqlId/cellId and ensuring no other side
effects; keep existing timerRef and try/catch behavior around
removeSignalListener.
…rossfilter-in-notebook-cells Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/cells/src/sqlHelpers.ts (1)
188-195:⚠️ Potential issue | 🟠 MajorExclude the current cell from AST dependency matching.
This matcher now scans every SQL cell in
cells, including the one being analyzed. If a cell's title/result name matches an external table it reads from, you create a self-edge and also drop that table fromtableNames. The olderfindSqlDependencies()path already skips the target cell; the AST path needs the same guard.Suggested fix
export async function findSqlDependenciesFromAst(opts: { + targetCellId?: string; sql: string; cells: Record<string, Cell>; sqlSelectToJson: (sql: string) => Promise<{ error: boolean; statements?: unknown[]; }>; }): Promise<CellDependencies> { - const {sql, cells, sqlSelectToJson} = opts; + const {sql, cells, sqlSelectToJson, targetCellId} = opts; ... for (const [id, cell] of Object.entries(cells)) { + if (id === targetCellId) continue; if (cell.type === 'sql') { ... } }Also applies to: 208-223
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/sqlHelpers.ts` around lines 188 - 195, The AST-based matcher in findSqlDependenciesFromAst is currently iterating all entries in opts.cells and can match the cell being analyzed, creating self-dependencies and removing its table from tableNames; modify the loop that scans opts.cells (where you compare cell titles/result names against tableNames) to skip the current cell by checking and continuing when a cell's SQL equals opts.sql (or otherwise identifying the same cell) so you never create self-edges or drop the analyzed cell from tableNames; apply the same guard to the similar scanning block later in that function.packages/cells/src/dagUtils.ts (1)
258-266:⚠️ Potential issue | 🟠 MajorForce one graph-cache rebuild before trusting
tableDependencies.This early return still keys off
isGraphCacheComplete(), which only checksdependencies. A sheet loaded with a preexisting cache can skip recomputation here even though its table-level metadata was never derived, so table-triggered invalidation stays empty until some unrelated cache bust occurs. Becausepackages/cells/src/types.tsnow defaultstableDependenciesto{}, a cache version bump or explicit invalidation on hydration is the safer fix.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/dagUtils.ts` around lines 258 - 266, The early return that trusts sheet.graphCache should also verify that table-level metadata was derived: after confirming isGraphCacheComplete(sheet.graphCache, sheet) and obtaining cache (SheetGraphCache), check that cache.tableDependencies exists and is populated (e.g., truthy and has keys) before returning; if tableDependencies is missing or empty (which may happen because types.ts now defaults tableDependencies to {}), do not return early so the graph-cache recomputation runs once to derive tableDependencies. Ensure checks reference isGraphCacheComplete, sheet.graphCache, cache.tableDependencies and preserve existing return shape when tableDependencies is valid.packages/cells/src/cellsSlice.ts (1)
676-679:⚠️ Potential issue | 🟡 MinorPass the
tableNamesparameter toreplaceCellDependenciesInCacheon line 676.The
addEdgefunction callsreplaceCellDependenciesInCachewithout the 4thtableNamesparameter, while all other call sites (lines 310, 455, 734) provide it. This causes table dependencies to not be tracked when edges are added, creating inconsistent behavior.Call site comparison
Line 676 (missing
tableNames):replaceCellDependenciesInCache(sheet, edge.target, [ ...deps, edge.source, ]);Other call sites (with
tableNames):replaceCellDependenciesInCache(sheet, cell.id, deps.cellIds.filter(...), deps.tableNames); replaceCellDependenciesInCache(ownerSheet, id, newDeps.cellIds.filter(...), newDeps.tableNames); replaceCellDependenciesInCache(draftSheet, cellId, deps.cellIds.filter(...), deps.tableNames);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/cellsSlice.ts` around lines 676 - 679, In addEdge, the call to replaceCellDependenciesInCache(sheet, edge.target, [...deps, edge.source]) omits the 4th tableNames parameter; update that call to pass the table names (use deps.tableNames) as the fourth argument so table dependencies are tracked consistently by replaceCellDependenciesInCache for the sheet/edge.target update.
♻️ Duplicate comments (3)
packages/cells/src/components/VegaCellContent.tsx (3)
88-95:⚠️ Potential issue | 🟠 MajorClear this chart's stored selection in the effect cleanup.
The cleanup only removes the Vega listener. When cross-filtering is turned off, the brush field or SQL source changes, or the cell unmounts,
crossFilterSelections[sqlId][cellId]survives and sibling charts keep filtering against stale state.Suggested fix
return () => { if (timerRef.current) clearTimeout(timerRef.current); try { embed.view.removeSignalListener(BRUSH_PARAM_NAME, handler); } catch { // View may have been finalized } + setCrossFilterSelection(cellId, sqlId, null); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/components/VegaCellContent.tsx` around lines 88 - 95, The effect cleanup currently clears timerRef and removes the Vega listener (embed.view.removeSignalListener(BRUSH_PARAM_NAME, handler)) but does not clear the stored cross-filter selection, so stale state remains in crossFilterSelections[sqlId][cellId]; update the returned cleanup function to also remove or reset that entry (e.g., delete crossFilterSelections[sqlId][cellId] or set it to undefined via the same state updater you use elsewhere) using the sqlId and cellId identifiers so sibling charts stop filtering against stale selections when the brush/SQL/source changes or the cell unmounts.
63-74:⚠️ Potential issue | 🟠 MajorVerify the interval payload lookup matches Vega-Lite's channel-based shape.
If this brush is still projected via
encodings: ['x'], Line 63 is reading the wrong property. Vega-Lite interval selections projected over encodings expose channel keys likex/y, and the official rectangular-brush example initializes the selection asvalue: {x: [...], y: [...]}. In that shape,value[brushField]staysundefined, so the store never receives an active selection. (vega.github.io)In Vega-Lite, for an interval selection parameter defined with `select: { type: 'interval', encodings: ['x'] }`, is the emitted signal value keyed by the encoding channel (`x`) or by the underlying field name?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/components/VegaCellContent.tsx` around lines 63 - 74, The code in VegaCellContent reads interval payloads by looking up value[brushField], but Vega-Lite interval selections projected via encodings provide channel-keyed values (e.g., value.x/value.y), so VegaCellContent must check for both channel-keyed and field-keyed shapes; update the logic around fieldValue (currently computed from value[brushField]) to first attempt channel lookup using brushField (e.g., value.x or value.y) and fall back to the existing field lookup, and ensure CrossFilterSelection (constructed in the selection creation code path) receives the interval array whether it came from the channel key or the field key; adjust any variable references to brushField, brushFieldType and the selection creation to use the found interval array.
189-191:⚠️ Potential issue | 🟠 MajorWrap the source query before appending the brush predicate.
Line 190 concatenates
WHERE ...directly ontobaseSqlQuery, and Lines 176-182 allow that string to be the raw SQL text from the source cell. That breaks queries that already containWHERE,GROUP BY,ORDER BY,LIMIT, or a trailing semicolon.Suggested fix
+ const filteredSqlSource = baseSqlQuery.replace(/;+\s*$/, ''); const selectedSqlQuery = crossFilterPredicate - ? `${baseSqlQuery} WHERE ${crossFilterPredicate}` + ? `SELECT * FROM (${filteredSqlSource}) AS crossfilter_source WHERE ${crossFilterPredicate}` : baseSqlQuery;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/components/VegaCellContent.tsx` around lines 189 - 191, selectedSqlQuery currently appends "WHERE ..." directly to baseSqlQuery which can break if baseSqlQuery already contains WHERE/GROUP BY/ORDER BY/LIMIT or a trailing semicolon; update the construction in VegaCellContent.tsx (symbols: baseSqlQuery, crossFilterPredicate, selectedSqlQuery) to sanitize and wrap the source SQL as a subquery (e.g., remove any trailing semicolon, then use `SELECT * FROM ( <baseSqlQuery> ) AS _source WHERE <crossFilterPredicate>`) so the brush predicate is always applied safely regardless of the original query's clauses.
🧹 Nitpick comments (1)
packages/cells/src/cellsSlice.ts (1)
118-145: Consider batching cell invalidations to avoid multiple state updates.The loop calls
invalidateCellStatusseparately for each affected cell, triggering Nset()calls and N re-renders. When many cells depend on changed tables, this could cause performance issues.♻️ Proposed refactor to batch invalidations
- store.subscribe((state, prevState) => { + store.subscribe((state, prevState) => { if (state.db.tables === prevState.db.tables) return; const toName = (t: {table: {schema?: string; table: string}}) => `${t.table.schema ?? 'main'}.${t.table.table}`; const oldNames = new Set(prevState.db.tables.map(toName)); const newNames = new Set(state.db.tables.map(toName)); const changed = new Set<string>(); for (const n of oldNames) { if (!newNames.has(n)) changed.add(n); } for (const n of newNames) { if (!oldNames.has(n)) changed.add(n); } if (!changed.size) return; + const cellsToInvalidate: string[] = []; for (const sheet of Object.values(state.cells.config.sheets)) { const cache = sheet.graphCache; if (!cache?.tableDependencies) continue; for (const [cellId, tables] of Object.entries( cache.tableDependencies, )) { if (tables.some((t) => changed.has(t))) { - state.cells.invalidateCellStatus(cellId); + cellsToInvalidate.push(cellId); } } } + if (cellsToInvalidate.length) { + set((s) => + produce(s, (draft) => { + for (const cellId of cellsToInvalidate) { + const cell = draft.cells.config.data[cellId]; + if (!cell) continue; + const registryItem = cellRegistry[cell.type]; + const status = draft.cells.status[cellId]; + if (status && registryItem?.invalidateStatus) { + draft.cells.status[cellId] = registryItem.invalidateStatus(status); + } + } + }), + ); + } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/cellsSlice.ts` around lines 118 - 145, The current store.subscribe handler loops and calls state.cells.invalidateCellStatus(cellId) for each affected cell, causing N separate state updates; change this to batch invalidations by collecting affected cellIds into a Set and then invoking a single batch method (e.g. add a new cells slice method named invalidateCellsStatus(cellIds: string[])) from the subscription. Specifically, inside the subscribe callback (store.subscribe) compute the affectedIds when table changes (reuse cache.tableDependencies logic), then call state.cells.invalidateCellsStatus(Array.from(affectedIds)) instead of calling invalidateCellStatus repeatedly; implement invalidateCellsStatus in the cells slice to perform one atomic update that marks all provided cellIds stale.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@packages/cells/src/cellsSlice.ts`:
- Around line 676-679: In addEdge, the call to
replaceCellDependenciesInCache(sheet, edge.target, [...deps, edge.source]) omits
the 4th tableNames parameter; update that call to pass the table names (use
deps.tableNames) as the fourth argument so table dependencies are tracked
consistently by replaceCellDependenciesInCache for the sheet/edge.target update.
In `@packages/cells/src/dagUtils.ts`:
- Around line 258-266: The early return that trusts sheet.graphCache should also
verify that table-level metadata was derived: after confirming
isGraphCacheComplete(sheet.graphCache, sheet) and obtaining cache
(SheetGraphCache), check that cache.tableDependencies exists and is populated
(e.g., truthy and has keys) before returning; if tableDependencies is missing or
empty (which may happen because types.ts now defaults tableDependencies to {}),
do not return early so the graph-cache recomputation runs once to derive
tableDependencies. Ensure checks reference isGraphCacheComplete,
sheet.graphCache, cache.tableDependencies and preserve existing return shape
when tableDependencies is valid.
In `@packages/cells/src/sqlHelpers.ts`:
- Around line 188-195: The AST-based matcher in findSqlDependenciesFromAst is
currently iterating all entries in opts.cells and can match the cell being
analyzed, creating self-dependencies and removing its table from tableNames;
modify the loop that scans opts.cells (where you compare cell titles/result
names against tableNames) to skip the current cell by checking and continuing
when a cell's SQL equals opts.sql (or otherwise identifying the same cell) so
you never create self-edges or drop the analyzed cell from tableNames; apply the
same guard to the similar scanning block later in that function.
---
Duplicate comments:
In `@packages/cells/src/components/VegaCellContent.tsx`:
- Around line 88-95: The effect cleanup currently clears timerRef and removes
the Vega listener (embed.view.removeSignalListener(BRUSH_PARAM_NAME, handler))
but does not clear the stored cross-filter selection, so stale state remains in
crossFilterSelections[sqlId][cellId]; update the returned cleanup function to
also remove or reset that entry (e.g., delete
crossFilterSelections[sqlId][cellId] or set it to undefined via the same state
updater you use elsewhere) using the sqlId and cellId identifiers so sibling
charts stop filtering against stale selections when the brush/SQL/source changes
or the cell unmounts.
- Around line 63-74: The code in VegaCellContent reads interval payloads by
looking up value[brushField], but Vega-Lite interval selections projected via
encodings provide channel-keyed values (e.g., value.x/value.y), so
VegaCellContent must check for both channel-keyed and field-keyed shapes; update
the logic around fieldValue (currently computed from value[brushField]) to first
attempt channel lookup using brushField (e.g., value.x or value.y) and fall back
to the existing field lookup, and ensure CrossFilterSelection (constructed in
the selection creation code path) receives the interval array whether it came
from the channel key or the field key; adjust any variable references to
brushField, brushFieldType and the selection creation to use the found interval
array.
- Around line 189-191: selectedSqlQuery currently appends "WHERE ..." directly
to baseSqlQuery which can break if baseSqlQuery already contains WHERE/GROUP
BY/ORDER BY/LIMIT or a trailing semicolon; update the construction in
VegaCellContent.tsx (symbols: baseSqlQuery, crossFilterPredicate,
selectedSqlQuery) to sanitize and wrap the source SQL as a subquery (e.g.,
remove any trailing semicolon, then use `SELECT * FROM ( <baseSqlQuery> ) AS
_source WHERE <crossFilterPredicate>`) so the brush predicate is always applied
safely regardless of the original query's clauses.
---
Nitpick comments:
In `@packages/cells/src/cellsSlice.ts`:
- Around line 118-145: The current store.subscribe handler loops and calls
state.cells.invalidateCellStatus(cellId) for each affected cell, causing N
separate state updates; change this to batch invalidations by collecting
affected cellIds into a Set and then invoking a single batch method (e.g. add a
new cells slice method named invalidateCellsStatus(cellIds: string[])) from the
subscription. Specifically, inside the subscribe callback (store.subscribe)
compute the affectedIds when table changes (reuse cache.tableDependencies
logic), then call state.cells.invalidateCellsStatus(Array.from(affectedIds))
instead of calling invalidateCellStatus repeatedly; implement
invalidateCellsStatus in the cells slice to perform one atomic update that marks
all provided cellIds stale.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 080e7770-4eb3-4baa-bb74-7159bb77c40c
📒 Files selected for processing (10)
packages/cells/__tests__/cellsSlice.localDeps.test.tspackages/cells/__tests__/defaultCellRegistry.integration.test.tsxpackages/cells/src/cellsSlice.tspackages/cells/src/components/VegaCellContent.tsxpackages/cells/src/dagUtils.tspackages/cells/src/defaultCellRegistry.tsxpackages/cells/src/helpers.tspackages/cells/src/index.tspackages/cells/src/sqlHelpers.tspackages/cells/src/types.ts
✅ Files skipped from review due to trivial changes (1)
- packages/cells/src/index.ts
Signed-off-by: Ilya Boyandin <[email protected]>
There was a problem hiding this comment.
♻️ Duplicate comments (3)
packages/cells/src/components/VegaCellContent.tsx (3)
48-63:⚠️ Potential issue | 🟠 MajorUse the encoding channel key (
x) instead of the field name to access the interval selection range.The interval selection parameter emits values keyed by the encoding channel name (e.g.,
x), not by the data field name. Line 48 readsvalue[brushField], but the payload actually hasvalue.x(or whatever encoding channel is configured). This will always beundefinedfor typical x-axis brush selections.🐛 Proposed fix
- const fieldValue = value[brushField]; + // Vega interval selections emit values keyed by encoding channel, not field name. + // For a typical x-axis brush, the value is in value.x + const fieldValue = value.x;If the brush encoding channel is configurable, you may need to store/pass the encoding channel separately from the field name and use that for lookup.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/components/VegaCellContent.tsx` around lines 48 - 63, The code is using the data field name (brushField) to read the interval selection from the Vega event (fieldValue = value[brushField]) but Vega emits interval values keyed by the encoding channel (e.g., value.x); update the logic to read the selection using the encoding channel key instead of the field name: introduce or use an existing variable representing the encoding channel (e.g., brushChannel or brushEncoding) to access value[brushChannel], validate it is an array of length 2, then build the CrossFilterSelection using field: brushField and fieldType: brushFieldType and call setCrossFilterSelection(cellId, sqlId, selection) (or null) as before; if the encoding channel can be configured, ensure it is stored/passed to VegaCellContent so the handler can access it.
73-80:⚠️ Potential issue | 🟠 MajorClear this chart's stored selection when the listener is torn down.
The cleanup only removes the Vega subscription but doesn't clear the entry from
crossFilterSelections[sqlId][cellId]. When this chart disables cross-filter, changes its SQL source, or is removed from the sheet, sibling charts will continue filtering based on the stale selection.🐛 Proposed fix
return () => { if (timerRef.current) clearTimeout(timerRef.current); + setCrossFilterSelection(cellId, sqlId, null); try { embed.view.removeSignalListener(BRUSH_PARAM_NAME, handler); } catch { // View may have been finalized } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/components/VegaCellContent.tsx` around lines 73 - 80, The cleanup currently removes the Vega signal listener but fails to clear the stored cross-filter state; update the teardown inside the returned cleanup function (near timerRef.current and embed.view.removeSignalListener(BRUSH_PARAM_NAME, handler)) to also remove/clear crossFilterSelections[sqlId][cellId] (or delete the cellId key) so that when the view is destroyed, cross-filter state for this chart is removed; ensure you guard for existence of crossFilterSelections and sqlId/cellId before modifying and keep this step inside the same try/catch or a safe conditional to avoid throwing if already missing.
152-154:⚠️ Potential issue | 🟠 MajorWrap the fallback SQL before appending the brush predicate.
When
resultViewis unavailable,baseSqlQueryfalls back to the raw SQL cell text. ConcatenatingWHERE ...directly onto that string breaks any query that already has its ownWHERE,GROUP BY,ORDER BY,LIMIT, or a trailing semicolon.🐛 Proposed fix
+ const wrappedBaseSqlQuery = baseSqlQuery.replace(/;+\s*$/, ''); const selectedSqlQuery = crossFilterPredicate - ? `${baseSqlQuery} WHERE ${crossFilterPredicate}` + ? `SELECT * FROM (${wrappedBaseSqlQuery}) AS _crossfilter_base WHERE ${crossFilterPredicate}` : baseSqlQuery;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/components/VegaCellContent.tsx` around lines 152 - 154, selectedSqlQuery currently appends "WHERE ..." directly to baseSqlQuery which breaks when baseSqlQuery is the raw SQL fallback (no resultView) and already contains WHERE/GROUP/ORDER/LIMIT or trailing semicolons; change the construction so when resultView is falsy (i.e. using the raw SQL fallback) you wrap the fallback SQL into a subquery and select from it before applying the crossFilterPredicate (e.g. build selectedSqlQuery as "SELECT * FROM (<baseSqlQuery-without-trailing-semicolon>) AS _vf WHERE <crossFilterPredicate>"), otherwise keep the existing `${baseSqlQuery} WHERE ${crossFilterPredicate}` behavior; update the logic around selectedSqlQuery, crossFilterPredicate, baseSqlQuery and resultView accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/cells/src/components/VegaCellContent.tsx`:
- Around line 48-63: The code is using the data field name (brushField) to read
the interval selection from the Vega event (fieldValue = value[brushField]) but
Vega emits interval values keyed by the encoding channel (e.g., value.x); update
the logic to read the selection using the encoding channel key instead of the
field name: introduce or use an existing variable representing the encoding
channel (e.g., brushChannel or brushEncoding) to access value[brushChannel],
validate it is an array of length 2, then build the CrossFilterSelection using
field: brushField and fieldType: brushFieldType and call
setCrossFilterSelection(cellId, sqlId, selection) (or null) as before; if the
encoding channel can be configured, ensure it is stored/passed to
VegaCellContent so the handler can access it.
- Around line 73-80: The cleanup currently removes the Vega signal listener but
fails to clear the stored cross-filter state; update the teardown inside the
returned cleanup function (near timerRef.current and
embed.view.removeSignalListener(BRUSH_PARAM_NAME, handler)) to also remove/clear
crossFilterSelections[sqlId][cellId] (or delete the cellId key) so that when the
view is destroyed, cross-filter state for this chart is removed; ensure you
guard for existence of crossFilterSelections and sqlId/cellId before modifying
and keep this step inside the same try/catch or a safe conditional to avoid
throwing if already missing.
- Around line 152-154: selectedSqlQuery currently appends "WHERE ..." directly
to baseSqlQuery which breaks when baseSqlQuery is the raw SQL fallback (no
resultView) and already contains WHERE/GROUP/ORDER/LIMIT or trailing semicolons;
change the construction so when resultView is falsy (i.e. using the raw SQL
fallback) you wrap the fallback SQL into a subquery and select from it before
applying the crossFilterPredicate (e.g. build selectedSqlQuery as "SELECT * FROM
(<baseSqlQuery-without-trailing-semicolon>) AS _vf WHERE
<crossFilterPredicate>"), otherwise keep the existing `${baseSqlQuery} WHERE
${crossFilterPredicate}` behavior; update the logic around selectedSqlQuery,
crossFilterPredicate, baseSqlQuery and resultView accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1ca3ec27-ced8-4367-b315-a6e60b39cb45
📒 Files selected for processing (4)
packages/cells/src/components/CellSourceSelector.tsxpackages/cells/src/components/VegaCellContent.tsxpackages/cells/src/index.tspackages/pivot/src/PivotCellContent.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/cells/src/index.ts
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
packages/cells/src/hooks/useVegaCellQuery.ts (1)
37-39:⚠️ Potential issue | 🟠 MajorTrim trailing semicolons before nesting
baseSqlQuery.
baseSqlQuerycan still be the raw SQL cell text. If that text ends with;, Line 38 producesSELECT * FROM (<sql>; ) AS _cells_base ..., which is invalid as a subquery.Suggested fix
+ const wrappedBaseSqlQuery = baseSqlQuery.replace(/;+\s*$/, ''); const selectedSqlQuery = crossFilterPredicate - ? `SELECT * FROM (${baseSqlQuery}) AS _cells_base WHERE ${crossFilterPredicate}` + ? `SELECT * FROM (${wrappedBaseSqlQuery}) AS _cells_base WHERE ${crossFilterPredicate}` : baseSqlQuery;In DuckDB SQL, is `SELECT * FROM (SELECT 1;) AS t` valid, or must the inner query omit the trailing semicolon when it is used as a subquery?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/hooks/useVegaCellQuery.ts` around lines 37 - 39, The code that builds selectedSqlQuery nests baseSqlQuery directly which can produce an invalid subquery if baseSqlQuery ends with a trailing semicolon; in the logic that builds selectedSqlQuery (useVegaCellQuery -> selectedSqlQuery, crossFilterPredicate, baseSqlQuery) trim any trailing semicolons/whitespace from baseSqlQuery before wrapping it (e.g. remove trailing ";" characters) so the nested form becomes `SELECT * FROM (cleanBaseSqlQuery) AS _cells_base WHERE ...`.packages/cells/src/components/SelectionListener.tsx (1)
46-57:⚠️ Potential issue | 🟠 MajorRead the brush interval from
value.x, notvalue[brushField].The brush is projected over
encodings: ['x'], so the signal payload is keyed by the channel name. Using the data field name here makes normal selections fall through to the clear path and drops the cross-filter.Suggested fix
- const fieldValue = value[brushField]; + const fieldValue = value.x;In Vega-Lite, when an interval selection is defined with `select: { type: 'interval', encodings: ['x'] }`, is the emitted parameter/signal payload keyed by `x` or by the encoded field name?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cells/src/components/SelectionListener.tsx` around lines 46 - 57, The code reads the brush interval from value[brushField], but Vega-Lite emits the interval keyed by the channel name (e.g., 'x') when encodings: ['x'] is used; change the selection extraction to read the interval from value.x (or the brush channel key) instead of value[brushField], then keep the same Array.isArray + length check and call setCrossFilterSelection(cellId, sqlId, selection) or setCrossFilterSelection(cellId, sqlId, null) when absent; update the variable referenced (currently fieldValue) where used and preserve brushFieldType, field, type, and value in the created CrossFilterSelection object.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/cells/src/components/VegaCellContent.tsx`:
- Around line 91-130: The handlers handleCrossFilterToggle,
handleBrushFieldChange and handleBrushFieldTypeChange currently call updateCell
immediately which persists crossFilter outside the same draft/save flow as
draftSpec; change these handlers to mutate only local draft state (e.g.,
draftSpec or a local draftCrossFilter object in component state) instead of
calling updateCell, and then when the existing save routine that writes
draftSpec runs, apply both the vegaSpec and the composed crossFilter in a single
updateCell call so cancelling edit mode leaves no persisted crossFilter changes.
In `@packages/cells/src/helpers.ts`:
- Around line 203-207: toDataSourceTable and fromDataSourceTable currently
flatten QualifiedTableName into an unescaped "schema.table" which breaks
identifiers containing dots; update toDataSourceTable to escape each identifier
segment instead of concatenating raw values (escape any internal double-quotes
by doubling them, then wrap schema and table in double quotes and join with a
dot after DATA_SOURCE_TABLE_PREFIX), and update fromDataSourceTable to reverse
that encoding (strip DATA_SOURCE_TABLE_PREFIX, parse the first dot as the
separator between the quoted schema and the quoted table, unescape doubled
quotes, and return {schema, table}); this preserves identifier boundaries and
avoids misparsing in useVegaCellDataSource.ts.
---
Duplicate comments:
In `@packages/cells/src/components/SelectionListener.tsx`:
- Around line 46-57: The code reads the brush interval from value[brushField],
but Vega-Lite emits the interval keyed by the channel name (e.g., 'x') when
encodings: ['x'] is used; change the selection extraction to read the interval
from value.x (or the brush channel key) instead of value[brushField], then keep
the same Array.isArray + length check and call setCrossFilterSelection(cellId,
sqlId, selection) or setCrossFilterSelection(cellId, sqlId, null) when absent;
update the variable referenced (currently fieldValue) where used and preserve
brushFieldType, field, type, and value in the created CrossFilterSelection
object.
In `@packages/cells/src/hooks/useVegaCellQuery.ts`:
- Around line 37-39: The code that builds selectedSqlQuery nests baseSqlQuery
directly which can produce an invalid subquery if baseSqlQuery ends with a
trailing semicolon; in the logic that builds selectedSqlQuery (useVegaCellQuery
-> selectedSqlQuery, crossFilterPredicate, baseSqlQuery) trim any trailing
semicolons/whitespace from baseSqlQuery before wrapping it (e.g. remove trailing
";" characters) so the nested form becomes `SELECT * FROM (cleanBaseSqlQuery) AS
_cells_base WHERE ...`.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 08289d62-cf62-41fa-9cf2-8655658db60f
📒 Files selected for processing (14)
AGENTS.mdcontributing/typescript.mdpackages/cells/src/components/CellSourceSelector.tsxpackages/cells/src/components/SelectionListener.tsxpackages/cells/src/components/VegaCellContent.tsxpackages/cells/src/components/VegaConfigPanel.tsxpackages/cells/src/helpers.tspackages/cells/src/hooks/useVegaCellDataSource.tspackages/cells/src/hooks/useVegaCellQuery.tspackages/cells/src/index.tspackages/cells/src/types.tspackages/cells/src/vegaSelectionUtils.tspackages/pivot/src/PivotCellContent.tsxpackages/vega/src/index.ts
✅ Files skipped from review due to trivial changes (2)
- AGENTS.md
- packages/vega/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/pivot/src/PivotCellContent.tsx
- packages/cells/src/vegaSelectionUtils.ts
- packages/cells/src/index.ts
- packages/cells/src/components/CellSourceSelector.tsx
Kapture.2026-03-20.at.18.16.29.mp4
Summary by CodeRabbit
New Features
Behavior Changes
Tests