Skip to content

feat: Vega crossfilter in notebook cells - #488

Merged
dmitriy-kostianetskiy merged 6 commits into
mainfrom
feat--Vega-crossfilter-in-notebook-cells
Mar 26, 2026
Merged

dmitriy-kostianetskiy merged 6 commits into
mainfrom
feat--Vega-crossfilter-in-notebook-cells

Conversation

@ilyabo

@ilyabo ilyabo commented Mar 21, 2026 •

Copy link
Copy Markdown
Collaborator
Kapture.2026-03-20.at.18.16.29.mp4

Summary by CodeRabbit

  • New Features

    • Cross-filtering: interactive brush selections that filter sibling charts and update queries; charts enable cross-filtering by default.
    • Data-source selector: choose cell-backed or table-backed sources in chart and pivot editors.
    • Chart editor: toggle cross-filter, pick brush field and type; header shows active filter or configured brush.
  • Behavior Changes

    • Table-reference changes now invalidate and refresh affected charts.
    • Cross-filter predicates are applied to SQL queries for filtered views.
  • Tests

    • Added tests covering table-dependency recording and dependency extraction.

@coderabbitai

coderabbitai Bot commented Mar 21, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds store-backed Vega cross-filtering (brush selections → SQL predicates), a generalized data-source selector (cell:/table:), table-level dependency reporting and graph-cache tableDependencies, and store-driven invalidation when database tables change.

Changes

Cohort / File(s) Summary
State, Types & Slice
packages/cells/src/cellsSlice.ts, packages/cells/src/types.ts
New cross-filter state cells.crossFilterSelections and actions (setCrossFilterSelection, getCrossFilterPredicate, clearCrossFilterGroup); added BrushFieldType, CrossFilterConfig, CrossFilterSelection, CellDependencies; added tableDepSchemas config and tableDependencies in sheet graph cache.
Vega UI, Hooks & Listeners
packages/cells/src/components/VegaCellContent.tsx, packages/cells/src/components/VegaConfigPanel.tsx, packages/cells/src/components/SelectionListener.tsx, packages/cells/src/hooks/useVegaCellDataSource.ts, packages/cells/src/hooks/useVegaCellQuery.ts
Replaced SQL-only selector with CellSourceSelector; introduced cross-filter UI/handlers and brush field typing; added SelectionListener to capture BRUSH_PARAM_NAME signals and write selections to store; new hooks compute base/selected SQL and cross-filter predicate.
Cell Source & Registry
packages/cells/src/components/CellSourceSelector.tsx, packages/cells/src/defaultCellRegistry.tsx
Added CellSourceSelector component; default Vega cell data now includes crossFilter.enabled = true; findDependencies returns {cellIds, tableNames} (tableNames from tableRef when present).
Dependency resolution & DAG
packages/cells/src/helpers.ts, packages/cells/src/dagUtils.ts, packages/cells/src/sqlHelpers.ts, packages/cells/src/sqlHelpers.ts
Introduced normalizeCellDependencies; resolveDependencies / findSqlDependenciesFromAst return CellDependencies; graph building and cache now track tableDependencies; replaceCellDependenciesInCache accepts tableNames, removeCellFromCache clears tableDeps.
Vega selection utils & exports
packages/cells/src/vegaSelectionUtils.ts, packages/cells/src/index.ts
Added BRUSH_PARAM_NAME and buildCrossFilterPredicate (interval/point handling, quoting, temporal conversion); re-exported CellSourceSelector, normalize/helpers, and cross-filter utilities from package index.
Pivot integration
packages/pivot/src/PivotCellContent.tsx, packages/pivot/src/pivotExecution.ts
Pivot source selection switched to CellSourceSelector with cell:<id> encoding; minor Array.from simplification in pivotExecution.
Tests
packages/cells/__tests__/cellsSlice.localDeps.test.ts, packages/cells/__tests__/defaultCellRegistry.integration.test.tsx
Added/updated tests to expect {cellIds, tableNames} shape and to assert tableDependencies recording for unmatched table refs.
Docs & Styles
contributing/typescript.md, AGENTS.md
New TypeScript/React guidelines doc and small README update.

Sequence Diagram

sequenceDiagram
    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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • bdjulbic
  • mihajlo-stojkovic

Poem

🐇
I hop through fields and specs tonight,
A brush, a hop, a filtered light,
Selections stitched in rows and strings,
Predicates hum beneath my springs,
Charts leap bright — carrot-coded delight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Vega crossfilter in notebook cells' clearly and concisely describes the main feature addition: implementing cross-filter functionality for Vega charts in notebook cells.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat--Vega-crossfilter-in-notebook-cells

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f9fc960 and 1de52da.

📒 Files selected for processing (7)
  • packages/cells/src/cellsSlice.ts
  • packages/cells/src/components/VegaCellContent.tsx
  • packages/cells/src/components/VegaConfigPanel.tsx
  • packages/cells/src/defaultCellRegistry.tsx
  • packages/cells/src/index.ts
  • packages/cells/src/types.ts
  • packages/cells/src/vegaSelectionUtils.ts

Comment on lines +57 to +68
const fieldValue = value[brushField];
if (
fieldValue != null &&
Array.isArray(fieldValue) &&
fieldValue.length === 2
) {
const selection: CrossFilterSelection = {
field: brushField,
fieldType: brushFieldType,
type: 'interval',
value: fieldValue,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


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.

Comment on lines +82 to +89
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
try {
embed.view.removeSignalListener(BRUSH_PARAM_NAME, handler);
} catch {
// View may have been finalized
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread packages/cells/src/components/VegaCellContent.tsx Outdated
Comment thread packages/cells/src/components/VegaConfigPanel.tsx
Comment thread packages/cells/src/vegaSelectionUtils.ts Outdated
ilyabo added 2 commits March 24, 2026 20:52
…rossfilter-in-notebook-cells

Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Exclude 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 from tableNames. The older findSqlDependencies() 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 | 🟠 Major

Force one graph-cache rebuild before trusting tableDependencies.

This early return still keys off isGraphCacheComplete(), which only checks dependencies. 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. Because packages/cells/src/types.ts now defaults tableDependencies to {}, 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 | 🟡 Minor

Pass the tableNames parameter to replaceCellDependenciesInCache on line 676.

The addEdge function calls replaceCellDependenciesInCache without the 4th tableNames parameter, 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 | 🟠 Major

Clear 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 | 🟠 Major

Verify 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 like x/y, and the official rectangular-brush example initializes the selection as value: {x: [...], y: [...]}. In that shape, value[brushField] stays undefined, 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 | 🟠 Major

Wrap the source query before appending the brush predicate.

Line 190 concatenates WHERE ... directly onto baseSqlQuery, and Lines 176-182 allow that string to be the raw SQL text from the source cell. That breaks queries that already contain WHERE, 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 invalidateCellStatus separately for each affected cell, triggering N set() 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

📥 Commits

Reviewing files that changed from the base of the PR and between ed3ff89 and d6d7bc2.

📒 Files selected for processing (10)
  • packages/cells/__tests__/cellsSlice.localDeps.test.ts
  • packages/cells/__tests__/defaultCellRegistry.integration.test.tsx
  • packages/cells/src/cellsSlice.ts
  • packages/cells/src/components/VegaCellContent.tsx
  • packages/cells/src/dagUtils.ts
  • packages/cells/src/defaultCellRegistry.tsx
  • packages/cells/src/helpers.ts
  • packages/cells/src/index.ts
  • packages/cells/src/sqlHelpers.ts
  • packages/cells/src/types.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/cells/src/index.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
packages/cells/src/components/VegaCellContent.tsx (3)

48-63: ⚠️ Potential issue | 🟠 Major

Use 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 reads value[brushField], but the payload actually has value.x (or whatever encoding channel is configured). This will always be undefined for 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 | 🟠 Major

Clear 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 | 🟠 Major

Wrap the fallback SQL before appending the brush predicate.

When resultView is unavailable, baseSqlQuery falls back to the raw SQL cell text. Concatenating WHERE ... directly onto that string breaks any query that already has its own WHERE, 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

📥 Commits

Reviewing files that changed from the base of the PR and between d6d7bc2 and d237e01.

📒 Files selected for processing (4)
  • packages/cells/src/components/CellSourceSelector.tsx
  • packages/cells/src/components/VegaCellContent.tsx
  • packages/cells/src/index.ts
  • packages/pivot/src/PivotCellContent.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cells/src/index.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/cells/src/hooks/useVegaCellQuery.ts (1)

37-39: ⚠️ Potential issue | 🟠 Major

Trim trailing semicolons before nesting baseSqlQuery.

baseSqlQuery can still be the raw SQL cell text. If that text ends with ;, Line 38 produces SELECT * 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 | 🟠 Major

Read the brush interval from value.x, not value[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

📥 Commits

Reviewing files that changed from the base of the PR and between d237e01 and ae6befa.

📒 Files selected for processing (14)
  • AGENTS.md
  • contributing/typescript.md
  • packages/cells/src/components/CellSourceSelector.tsx
  • packages/cells/src/components/SelectionListener.tsx
  • packages/cells/src/components/VegaCellContent.tsx
  • packages/cells/src/components/VegaConfigPanel.tsx
  • packages/cells/src/helpers.ts
  • packages/cells/src/hooks/useVegaCellDataSource.ts
  • packages/cells/src/hooks/useVegaCellQuery.ts
  • packages/cells/src/index.ts
  • packages/cells/src/types.ts
  • packages/cells/src/vegaSelectionUtils.ts
  • packages/pivot/src/PivotCellContent.tsx
  • packages/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

Comment thread packages/cells/src/components/VegaCellContent.tsx
Comment thread packages/cells/src/helpers.ts
@dmitriy-kostianetskiy
dmitriy-kostianetskiy merged commit fdd3a40 into main Mar 26, 2026
3 checks passed
@dmitriy-kostianetskiy
dmitriy-kostianetskiy deleted the feat--Vega-crossfilter-in-notebook-cells branch March 26, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants