@sqlrooms/cells 0.29.0-rc.1 → 0.29.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/dist/cellsSlice.d.ts.map +1 -1
  2. package/dist/cellsSlice.js +191 -73
  3. package/dist/cellsSlice.js.map +1 -1
  4. package/dist/components/AggregationSelector.d.ts +8 -0
  5. package/dist/components/AggregationSelector.d.ts.map +1 -0
  6. package/dist/components/AggregationSelector.js +13 -0
  7. package/dist/components/AggregationSelector.js.map +1 -0
  8. package/dist/components/CellSourceSelector.d.ts +11 -0
  9. package/dist/components/CellSourceSelector.d.ts.map +1 -0
  10. package/dist/components/CellSourceSelector.js +35 -0
  11. package/dist/components/CellSourceSelector.js.map +1 -0
  12. package/dist/components/ChartTypeSelector.d.ts +8 -0
  13. package/dist/components/ChartTypeSelector.d.ts.map +1 -0
  14. package/dist/components/ChartTypeSelector.js +13 -0
  15. package/dist/components/ChartTypeSelector.js.map +1 -0
  16. package/dist/components/CollapsibleSectionButton.d.ts +10 -0
  17. package/dist/components/CollapsibleSectionButton.d.ts.map +1 -0
  18. package/dist/components/CollapsibleSectionButton.js +10 -0
  19. package/dist/components/CollapsibleSectionButton.js.map +1 -0
  20. package/dist/components/ColorSelector.d.ts +24 -0
  21. package/dist/components/ColorSelector.d.ts.map +1 -0
  22. package/dist/components/ColorSelector.js +17 -0
  23. package/dist/components/ColorSelector.js.map +1 -0
  24. package/dist/components/FieldSelector.d.ts +11 -0
  25. package/dist/components/FieldSelector.d.ts.map +1 -0
  26. package/dist/components/FieldSelector.js +11 -0
  27. package/dist/components/FieldSelector.js.map +1 -0
  28. package/dist/components/FieldTypeIcon.d.ts +8 -0
  29. package/dist/components/FieldTypeIcon.d.ts.map +1 -0
  30. package/dist/components/FieldTypeIcon.js +27 -0
  31. package/dist/components/FieldTypeIcon.js.map +1 -0
  32. package/dist/components/InputCellContent.d.ts.map +1 -1
  33. package/dist/components/InputCellContent.js +3 -3
  34. package/dist/components/InputCellContent.js.map +1 -1
  35. package/dist/components/SelectionListener.d.ts +15 -0
  36. package/dist/components/SelectionListener.d.ts.map +1 -0
  37. package/dist/components/SelectionListener.js +74 -0
  38. package/dist/components/SelectionListener.js.map +1 -0
  39. package/dist/components/SheetsTabBar.d.ts.map +1 -1
  40. package/dist/components/SheetsTabBar.js +2 -1
  41. package/dist/components/SheetsTabBar.js.map +1 -1
  42. package/dist/components/SqlCellBody.d.ts +6 -0
  43. package/dist/components/SqlCellBody.d.ts.map +1 -0
  44. package/dist/components/SqlCellBody.js +12 -0
  45. package/dist/components/SqlCellBody.js.map +1 -0
  46. package/dist/components/SqlCellConnectionSelector.d.ts +9 -0
  47. package/dist/components/SqlCellConnectionSelector.d.ts.map +1 -0
  48. package/dist/components/SqlCellConnectionSelector.js +21 -0
  49. package/dist/components/SqlCellConnectionSelector.js.map +1 -0
  50. package/dist/components/SqlCellContent.d.ts.map +1 -1
  51. package/dist/components/SqlCellContent.js +29 -154
  52. package/dist/components/SqlCellContent.js.map +1 -1
  53. package/dist/components/SqlCellDependentsMenu.d.ts +11 -0
  54. package/dist/components/SqlCellDependentsMenu.d.ts.map +1 -0
  55. package/dist/components/SqlCellDependentsMenu.js +62 -0
  56. package/dist/components/SqlCellDependentsMenu.js.map +1 -0
  57. package/dist/components/SqlCellEditor.d.ts +14 -0
  58. package/dist/components/SqlCellEditor.d.ts.map +1 -0
  59. package/dist/components/SqlCellEditor.js +17 -0
  60. package/dist/components/SqlCellEditor.js.map +1 -0
  61. package/dist/components/SqlCellResultNameEditor.d.ts +8 -0
  62. package/dist/components/SqlCellResultNameEditor.d.ts.map +1 -0
  63. package/dist/components/SqlCellResultNameEditor.js +38 -0
  64. package/dist/components/SqlCellResultNameEditor.js.map +1 -0
  65. package/dist/components/SqlCellResults.d.ts +17 -0
  66. package/dist/components/SqlCellResults.d.ts.map +1 -0
  67. package/dist/components/SqlCellResults.js +45 -0
  68. package/dist/components/SqlCellResults.js.map +1 -0
  69. package/dist/components/SqlCellRunButton.d.ts +5 -6
  70. package/dist/components/SqlCellRunButton.d.ts.map +1 -1
  71. package/dist/components/SqlCellRunButton.js +11 -7
  72. package/dist/components/SqlCellRunButton.js.map +1 -1
  73. package/dist/components/SqlQueryPreview.d.ts +9 -0
  74. package/dist/components/SqlQueryPreview.d.ts.map +1 -0
  75. package/dist/components/SqlQueryPreview.js +14 -0
  76. package/dist/components/SqlQueryPreview.js.map +1 -0
  77. package/dist/components/TextCellContent.d.ts.map +1 -1
  78. package/dist/components/TextCellContent.js +24 -7
  79. package/dist/components/TextCellContent.js.map +1 -1
  80. package/dist/components/VegaCellContent.d.ts +0 -1
  81. package/dist/components/VegaCellContent.d.ts.map +1 -1
  82. package/dist/components/VegaCellContent.js +61 -47
  83. package/dist/components/VegaCellContent.js.map +1 -1
  84. package/dist/components/VegaCellHeader.d.ts +11 -0
  85. package/dist/components/VegaCellHeader.d.ts.map +1 -0
  86. package/dist/components/VegaCellHeader.js +31 -0
  87. package/dist/components/VegaCellHeader.js.map +1 -0
  88. package/dist/components/VegaConfigPanel.d.ts +5 -0
  89. package/dist/components/VegaConfigPanel.d.ts.map +1 -1
  90. package/dist/components/VegaConfigPanel.js +53 -63
  91. package/dist/components/VegaConfigPanel.js.map +1 -1
  92. package/dist/crdt.d.ts +61 -0
  93. package/dist/crdt.d.ts.map +1 -0
  94. package/dist/crdt.js +82 -0
  95. package/dist/crdt.js.map +1 -0
  96. package/dist/dagSlice.d.ts +3 -0
  97. package/dist/dagSlice.d.ts.map +1 -0
  98. package/dist/dagSlice.js +51 -0
  99. package/dist/dagSlice.js.map +1 -0
  100. package/dist/dagUtils.d.ts +2 -1
  101. package/dist/dagUtils.d.ts.map +1 -1
  102. package/dist/dagUtils.js +35 -11
  103. package/dist/dagUtils.js.map +1 -1
  104. package/dist/defaultCellRegistry.d.ts.map +1 -1
  105. package/dist/defaultCellRegistry.js +63 -24
  106. package/dist/defaultCellRegistry.js.map +1 -1
  107. package/dist/execution.d.ts.map +1 -1
  108. package/dist/execution.js +2 -1
  109. package/dist/execution.js.map +1 -1
  110. package/dist/helpers.d.ts +14 -2
  111. package/dist/helpers.d.ts.map +1 -1
  112. package/dist/helpers.js +56 -2
  113. package/dist/helpers.js.map +1 -1
  114. package/dist/hooks/useVegaCellDataSource.d.ts +18 -0
  115. package/dist/hooks/useVegaCellDataSource.d.ts.map +1 -0
  116. package/dist/hooks/useVegaCellDataSource.js +40 -0
  117. package/dist/hooks/useVegaCellDataSource.js.map +1 -0
  118. package/dist/hooks/useVegaCellQuery.d.ts +16 -0
  119. package/dist/hooks/useVegaCellQuery.d.ts.map +1 -0
  120. package/dist/hooks/useVegaCellQuery.js +25 -0
  121. package/dist/hooks/useVegaCellQuery.js.map +1 -0
  122. package/dist/index.d.ts +3 -2
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +3 -2
  125. package/dist/index.js.map +1 -1
  126. package/dist/sqlHelpers.d.ts +3 -3
  127. package/dist/sqlHelpers.d.ts.map +1 -1
  128. package/dist/sqlHelpers.js +9 -5
  129. package/dist/sqlHelpers.js.map +1 -1
  130. package/dist/typeGuards.d.ts +2 -0
  131. package/dist/typeGuards.d.ts.map +1 -0
  132. package/dist/typeGuards.js +4 -0
  133. package/dist/typeGuards.js.map +1 -0
  134. package/dist/types.d.ts +97 -26
  135. package/dist/types.d.ts.map +1 -1
  136. package/dist/types.js +17 -2
  137. package/dist/types.js.map +1 -1
  138. package/dist/useValidateResultName.d.ts +34 -0
  139. package/dist/useValidateResultName.d.ts.map +1 -0
  140. package/dist/useValidateResultName.js +111 -0
  141. package/dist/useValidateResultName.js.map +1 -0
  142. package/dist/utils.d.ts +8 -0
  143. package/dist/utils.d.ts.map +1 -1
  144. package/dist/utils.js +16 -0
  145. package/dist/utils.js.map +1 -1
  146. package/dist/validation.d.ts +16 -0
  147. package/dist/validation.d.ts.map +1 -0
  148. package/dist/validation.js +40 -0
  149. package/dist/validation.js.map +1 -0
  150. package/dist/vegaSelectionUtils.d.ts +14 -0
  151. package/dist/vegaSelectionUtils.d.ts.map +1 -0
  152. package/dist/vegaSelectionUtils.js +97 -0
  153. package/dist/vegaSelectionUtils.js.map +1 -0
  154. package/dist/vegaSpecBuilder.d.ts +34 -0
  155. package/dist/vegaSpecBuilder.d.ts.map +1 -0
  156. package/dist/vegaSpecBuilder.js +90 -0
  157. package/dist/vegaSpecBuilder.js.map +1 -0
  158. package/package.json +12 -13
@@ -1 +1 @@
1
- {"version":3,"file":"cellsSlice.d.ts","sourceRoot":"","sources":["../src/cellsSlice.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAKV,iBAAiB,EACjB,eAAe,EAGhB,MAAM,SAAS,CAAC;AAgDjB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,mDA4uBxD"}
1
+ {"version":3,"file":"cellsSlice.d.ts","sourceRoot":"","sources":["../src/cellsSlice.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAKV,iBAAiB,EACjB,eAAe,EAMhB,MAAM,SAAS,CAAC;AAoDjB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,mDA83BxD"}
@@ -2,13 +2,14 @@ import { createId } from '@paralleldrive/cuid2';
2
2
  import { makePagedQuery } from '@sqlrooms/data-table';
3
3
  import { sanitizeQuery } from '@sqlrooms/duckdb';
4
4
  import { createSlice } from '@sqlrooms/room-store';
5
- import { generateUniqueName } from '@sqlrooms/utils';
5
+ import { convertToValidColumnOrTableName, generateUniqueName, } from '@sqlrooms/utils';
6
6
  import { produce } from 'immer';
7
- import { buildGraphCacheFromEdges, buildDependencyGraph, buildDependencyGraphAsync, collectReachable, ensureGraphCache, removeCellFromCache, replaceCellDependenciesInCache, topologicalOrder, } from './dagUtils';
7
+ import { buildDependencyGraph, buildDependencyGraphAsync, buildGraphCacheFromEdges, collectReachable, ensureGraphCache, removeCellFromCache, replaceCellDependenciesInCache, topologicalOrder, } from './dagUtils';
8
8
  import { findSheetIdForCell, getRequiredSqlSelectToJson, normalizeCellsConfigStructure, resolveDependencies, } from './helpers';
9
9
  import { dropResultRelation } from './resultRelationPolicy';
10
10
  import { isInputCell, isSqlCell } from './types';
11
- import { getSheetSchemaName, isDefined } from './utils';
11
+ import { getEffectiveResultName, getSheetSchemaName, isDefined } from './utils';
12
+ import { buildCrossFilterPredicate } from './vegaSelectionUtils';
12
13
  function createDefaultCellsConfig(config) {
13
14
  const sheetId = createId();
14
15
  const defaultConfig = {
@@ -25,11 +26,13 @@ function createDefaultCellsConfig(config) {
25
26
  dependencies: {},
26
27
  dependents: {},
27
28
  contentHashByCell: {},
29
+ tableDependencies: {},
28
30
  },
29
31
  },
30
32
  },
31
33
  sheetOrder: [sheetId],
32
34
  currentSheetId: sheetId,
35
+ tableDepSchemas: ['main'],
33
36
  };
34
37
  if (!config) {
35
38
  return defaultConfig;
@@ -50,16 +53,53 @@ export function createCellsSlice(props) {
50
53
  // Keep result data outside Immer drafts, but scoped per slice instance.
51
54
  const cellResultCache = new Map();
52
55
  return createSlice((set, get, store) => {
53
- const dropSqlResultRelation = async (resultName) => {
56
+ const dropRelationBestEffort = async (relationName) => {
54
57
  try {
55
58
  const connector = await get().db.getConnector();
56
- await dropResultRelation({ connector, relationName: resultName });
59
+ await dropResultRelation({ connector, relationName });
57
60
  }
58
61
  catch {
59
- // Best-effort cleanup: relation might already be gone or connector
60
- // might be unavailable during teardown.
62
+ // Best-effort cleanup
61
63
  }
62
64
  };
65
+ const dropRelationsForStatus = async (cellType, status) => {
66
+ if (!status)
67
+ return;
68
+ const registryItem = cellRegistry[cellType];
69
+ const names = registryItem?.getRelationsToDrop?.(status) ?? [];
70
+ for (const name of names) {
71
+ await dropRelationBestEffort(name);
72
+ }
73
+ };
74
+ // Mark cells stale when their dependent database tables change (schema-level).
75
+ store.subscribe((state, prevState) => {
76
+ if (state.db.tables === prevState.db.tables)
77
+ return;
78
+ const toName = (t) => `${t.table.schema ?? 'main'}.${t.table.table}`;
79
+ const oldNames = new Set(prevState.db.tables.map(toName));
80
+ const newNames = new Set(state.db.tables.map(toName));
81
+ const changed = new Set();
82
+ for (const n of oldNames) {
83
+ if (!newNames.has(n))
84
+ changed.add(n);
85
+ }
86
+ for (const n of newNames) {
87
+ if (!oldNames.has(n))
88
+ changed.add(n);
89
+ }
90
+ if (!changed.size)
91
+ return;
92
+ for (const sheet of Object.values(state.cells.config.sheets)) {
93
+ const cache = sheet.graphCache;
94
+ if (!cache?.tableDependencies)
95
+ continue;
96
+ for (const [cellId, tables] of Object.entries(cache.tableDependencies)) {
97
+ if (tables.some((t) => changed.has(t))) {
98
+ state.cells.invalidateCellStatus(cellId);
99
+ }
100
+ }
101
+ }
102
+ });
63
103
  return {
64
104
  cells: {
65
105
  cellRegistry,
@@ -69,6 +109,42 @@ export function createCellsSlice(props) {
69
109
  activeAbortControllers: {},
70
110
  resultVersion: {},
71
111
  pageVersion: {},
112
+ async initialize() {
113
+ for (const [cellId, cell] of Object.entries(get().cells.config.data)) {
114
+ const registryItem = cellRegistry[cell.type];
115
+ if (registryItem?.onInitialize) {
116
+ registryItem.onInitialize({
117
+ id: cellId,
118
+ status: get().cells.status[cellId],
119
+ get,
120
+ set,
121
+ });
122
+ }
123
+ }
124
+ },
125
+ crossFilterSelections: {},
126
+ setCrossFilterSelection: (chartCellId, sqlId, selection) => {
127
+ set((state) => produce(state, (draft) => {
128
+ if (!draft.cells.crossFilterSelections[sqlId]) {
129
+ draft.cells.crossFilterSelections[sqlId] = {};
130
+ }
131
+ draft.cells.crossFilterSelections[sqlId][chartCellId] = selection;
132
+ }));
133
+ },
134
+ getCrossFilterPredicate: (chartCellId, sqlId) => {
135
+ const group = get().cells.crossFilterSelections[sqlId];
136
+ if (!group)
137
+ return null;
138
+ const siblingSelections = Object.entries(group)
139
+ .filter(([id]) => id !== chartCellId)
140
+ .map(([, sel]) => sel);
141
+ return buildCrossFilterPredicate(siblingSelections);
142
+ },
143
+ clearCrossFilterGroup: (sqlId) => {
144
+ set((state) => produce(state, (draft) => {
145
+ delete draft.cells.crossFilterSelections[sqlId];
146
+ }));
147
+ },
72
148
  addCell: async (sheetId, cell, index) => {
73
149
  // Pre-compute dependencies outside produce() to support async
74
150
  const sqlSelectToJson = getRequiredSqlSelectToJson(get());
@@ -78,19 +154,20 @@ export function createCellsSlice(props) {
78
154
  .filter(isDefined)
79
155
  .map((candidate) => [candidate.id, candidate]));
80
156
  scopedCells[cell.id] = cell;
157
+ // Generate a unique result name for SQL cells if not set
158
+ if (cell.type === 'sql' && !cell.data.resultName) {
159
+ const existingNames = Object.values(scopedCells)
160
+ .filter((c) => c.type === 'sql' && c.id !== cell.id)
161
+ .map((c) => getEffectiveResultName(c.data, convertToValidColumnOrTableName));
162
+ const baseName = getEffectiveResultName(cell.data, convertToValidColumnOrTableName);
163
+ const uniqueName = generateUniqueName(baseName, existingNames);
164
+ cell.data.resultName = uniqueName;
165
+ }
81
166
  const deps = await resolveDependencies(cell, scopedCells, sheetId, cellRegistry, sqlSelectToJson);
82
167
  set((state) => produce(state, (draft) => {
83
168
  draft.cells.config.data[cell.id] = cell;
84
- if (cell.type === 'sql') {
85
- draft.cells.status[cell.id] = {
86
- type: 'sql',
87
- status: 'idle',
88
- referencedTables: [],
89
- };
90
- }
91
- else {
92
- draft.cells.status[cell.id] = { type: 'other' };
93
- }
169
+ const registryItem = cellRegistry[cell.type];
170
+ draft.cells.status[cell.id] = registryItem?.createStatus?.(cell.id) ?? { type: 'other' };
94
171
  // Single-owner invariant: a cell can belong to one sheet only.
95
172
  for (const [existingSheetId, existingSheet] of Object.entries(draft.cells.config.sheets)) {
96
173
  if (existingSheetId === sheetId)
@@ -112,6 +189,7 @@ export function createCellsSlice(props) {
112
189
  dependencies: {},
113
190
  dependents: {},
114
191
  contentHashByCell: {},
192
+ tableDependencies: {},
115
193
  },
116
194
  };
117
195
  draft.cells.config.sheets[sheetId] = sheet;
@@ -129,7 +207,7 @@ export function createCellsSlice(props) {
129
207
  sheet.edges = sheet.edges.filter((edge) => edge.target !== cell.id &&
130
208
  edge.id !== `${edge.source}-${cell.id}`);
131
209
  const localCellIds = new Set(sheet.cellIds);
132
- for (const depId of deps) {
210
+ for (const depId of deps.cellIds) {
133
211
  if (!localCellIds.has(depId) || depId === cell.id)
134
212
  continue;
135
213
  sheet.edges.push({
@@ -138,10 +216,11 @@ export function createCellsSlice(props) {
138
216
  target: cell.id,
139
217
  });
140
218
  }
141
- replaceCellDependenciesInCache(sheet, cell.id, deps.filter((depId) => localCellIds.has(depId)));
219
+ replaceCellDependenciesInCache(sheet, cell.id, deps.cellIds.filter((depId) => localCellIds.has(depId)), deps.tableNames);
142
220
  }));
143
221
  },
144
222
  removeCell: (id) => {
223
+ const cell = get().cells.config.data[id];
145
224
  const previousStatus = get().cells.status[id];
146
225
  cellResultCache.delete(id);
147
226
  set((state) => produce(state, (draft) => {
@@ -161,8 +240,19 @@ export function createCellsSlice(props) {
161
240
  removeCellFromCache(sheet, id);
162
241
  }
163
242
  }));
164
- if (previousStatus?.type === 'sql') {
165
- void dropSqlResultRelation(previousStatus.resultView);
243
+ if (cell) {
244
+ const registryItem = cellRegistry[cell.type];
245
+ if (registryItem?.onRemove) {
246
+ void registryItem.onRemove({
247
+ id,
248
+ status: previousStatus,
249
+ get,
250
+ set,
251
+ });
252
+ }
253
+ else {
254
+ void dropRelationsForStatus(cell.type, previousStatus);
255
+ }
166
256
  }
167
257
  },
168
258
  updateCell: async (id, updater, opts) => {
@@ -178,15 +268,11 @@ export function createCellsSlice(props) {
178
268
  .map((candidate) => [candidate.id, candidate]));
179
269
  const updatedCell = updater(cell);
180
270
  scopedCells[id] = updatedCell;
181
- // Check if resultName changed for SQL cells with successful execution
182
- const resultNameChanged = isSqlCell(cell) &&
183
- isSqlCell(updatedCell) &&
184
- cell.data.resultName !== updatedCell.data.resultName;
271
+ const registryItem = cellRegistry[cell.type];
185
272
  const existingStatus = get().cells.status[id];
186
- const hasExistingView = resultNameChanged &&
187
- existingStatus?.type === 'sql' &&
188
- existingStatus.status === 'success' &&
189
- existingStatus.resultView;
273
+ const oldResultRelation = existingStatus && registryItem?.getResultRelation
274
+ ? registryItem.getResultRelation(existingStatus)
275
+ : undefined;
190
276
  const semanticInputChanged = isInputCell(cell) &&
191
277
  isInputCell(updatedCell) &&
192
278
  (cell.data.input.varName !== updatedCell.data.input.varName ||
@@ -194,12 +280,20 @@ export function createCellsSlice(props) {
194
280
  const semanticSqlChanged = isSqlCell(cell) &&
195
281
  isSqlCell(updatedCell) &&
196
282
  cell.data.sql !== updatedCell.data.sql;
283
+ const semanticChanged = registryItem?.hasSemanticChange?.(cell, updatedCell) ?? false;
197
284
  // Apply cell data to the store immediately so that other
198
285
  // actions (e.g. runCell triggered via Cmd+Enter) always see
199
286
  // the latest value. Edge/dependency updates follow
200
287
  // asynchronously below.
201
288
  set((state) => produce(state, (draft) => {
202
289
  draft.cells.config.data[id] = updatedCell;
290
+ if (semanticChanged) {
291
+ const status = draft.cells.status[id];
292
+ if (status && registryItem?.invalidateStatus) {
293
+ draft.cells.status[id] =
294
+ registryItem.invalidateStatus(status);
295
+ }
296
+ }
203
297
  }));
204
298
  // Avoid expensive dependency graph recomputation on each SQL keystroke.
205
299
  // We recompute immediately for non-SQL edits, or when callers explicitly
@@ -214,7 +308,7 @@ export function createCellsSlice(props) {
214
308
  if (ownerSheet) {
215
309
  const localCellIds = new Set(ownerSheet.cellIds);
216
310
  ownerSheet.edges = ownerSheet.edges.filter((e) => e.target !== id);
217
- for (const depId of newDeps) {
311
+ for (const depId of newDeps.cellIds) {
218
312
  if (!localCellIds.has(depId) || depId === id)
219
313
  continue;
220
314
  ownerSheet.edges.push({
@@ -223,25 +317,27 @@ export function createCellsSlice(props) {
223
317
  target: id,
224
318
  });
225
319
  }
226
- replaceCellDependenciesInCache(ownerSheet, id, newDeps.filter((depId) => localCellIds.has(depId)));
320
+ replaceCellDependenciesInCache(ownerSheet, id, newDeps.cellIds.filter((depId) => localCellIds.has(depId)), newDeps.tableNames);
227
321
  }
228
322
  }));
229
323
  }
230
- // If we have an existing view, rename it instead of invalidating
231
- if (hasExistingView && existingStatus?.resultView) {
232
- const registryItem = cellRegistry[cell.type];
233
- if (registryItem?.renameResult) {
234
- void registryItem.renameResult({
235
- id,
236
- oldResultView: existingStatus.resultView,
237
- get,
238
- set,
239
- });
240
- }
324
+ // If the result relation changed name (e.g. SQL resultName changed), rename it
325
+ const newStatus = get().cells.status[id];
326
+ const newResultRelation = newStatus && registryItem?.getResultRelation
327
+ ? registryItem.getResultRelation(newStatus)
328
+ : undefined;
329
+ const resultRelationChanged = oldResultRelation && newResultRelation !== oldResultRelation;
330
+ if (resultRelationChanged && registryItem?.renameResult) {
331
+ void registryItem.renameResult({
332
+ id,
333
+ oldResultView: oldResultRelation,
334
+ get,
335
+ set,
336
+ });
241
337
  }
242
338
  // After update, trigger cascade only if explicitly requested
243
339
  // or if semantic execution inputs changed.
244
- const shouldCascade = opts?.cascade || resultNameChanged || semanticInputChanged;
340
+ const shouldCascade = opts?.cascade || resultRelationChanged || semanticInputChanged;
245
341
  if (shouldCascade) {
246
342
  if (ownerSheetId) {
247
343
  void get().cells.runDownstreamCascade(ownerSheetId, id);
@@ -265,6 +361,7 @@ export function createCellsSlice(props) {
265
361
  dependencies: {},
266
362
  dependents: {},
267
363
  contentHashByCell: {},
364
+ tableDependencies: {},
268
365
  },
269
366
  };
270
367
  draft.cells.config.sheetOrder.push(id);
@@ -278,10 +375,12 @@ export function createCellsSlice(props) {
278
375
  return;
279
376
  const ownedCellIds = [...sheet.cellIds];
280
377
  const relationNamesToDrop = ownedCellIds.flatMap((cellId) => {
378
+ const cell = get().cells.config.data[cellId];
281
379
  const status = get().cells.status[cellId];
282
- return status?.type === 'sql' && status.resultView
283
- ? [status.resultView]
284
- : [];
380
+ if (!cell || !status)
381
+ return [];
382
+ const registryItem = cellRegistry[cell.type];
383
+ return registryItem?.getRelationsToDrop?.(status) ?? [];
285
384
  });
286
385
  set((state) => produce(state, (draft) => {
287
386
  for (const cellId of ownedCellIds) {
@@ -313,7 +412,7 @@ export function createCellsSlice(props) {
313
412
  }
314
413
  }));
315
414
  for (const relationName of relationNamesToDrop) {
316
- void dropSqlResultRelation(relationName);
415
+ void dropRelationBestEffort(relationName);
317
416
  }
318
417
  },
319
418
  closeSheet: (sheetId) => {
@@ -369,6 +468,7 @@ export function createCellsSlice(props) {
369
468
  dependencies: {},
370
469
  dependents: {},
371
470
  contentHashByCell: {},
471
+ tableDependencies: {},
372
472
  },
373
473
  };
374
474
  draft.cells.config.sheets[sheetId] = sheet;
@@ -421,7 +521,7 @@ export function createCellsSlice(props) {
421
521
  if (draftSheet) {
422
522
  draftSheet.edges = draftSheet.edges.filter((e) => e.target !== cellId);
423
523
  const localCellIds = new Set(draftSheet.cellIds);
424
- for (const depId of deps) {
524
+ for (const depId of deps.cellIds) {
425
525
  if (!localCellIds.has(depId) || depId === cellId)
426
526
  continue;
427
527
  draftSheet.edges.push({
@@ -430,7 +530,7 @@ export function createCellsSlice(props) {
430
530
  target: cellId,
431
531
  });
432
532
  }
433
- replaceCellDependenciesInCache(draftSheet, cellId, deps.filter((depId) => localCellIds.has(depId)));
533
+ replaceCellDependenciesInCache(draftSheet, cellId, deps.cellIds.filter((depId) => localCellIds.has(depId)), deps.tableNames);
434
534
  }
435
535
  }));
436
536
  },
@@ -457,15 +557,14 @@ export function createCellsSlice(props) {
457
557
  }
458
558
  },
459
559
  invalidateCellStatus: (id) => {
560
+ const cell = get().cells.config.data[id];
561
+ if (!cell)
562
+ return;
563
+ const registryItem = cellRegistry[cell.type];
460
564
  set((state) => produce(state, (draft) => {
461
565
  const status = draft.cells.status[id];
462
- if (status?.type === 'sql') {
463
- // Reset to idle, clearing the result so user knows to re-run
464
- draft.cells.status[id] = {
465
- type: 'sql',
466
- status: 'idle',
467
- referencedTables: status.referencedTables || [],
468
- };
566
+ if (status && registryItem?.invalidateStatus) {
567
+ draft.cells.status[id] = registryItem.invalidateStatus(status);
469
568
  }
470
569
  }));
471
570
  },
@@ -496,14 +595,21 @@ export function createCellsSlice(props) {
496
595
  },
497
596
  fetchCellResultPage: async (id, pagination, sorting) => {
498
597
  const state = get();
598
+ const cell = state.cells.config.data[id];
599
+ if (!cell)
600
+ return;
601
+ const registryItem = cellRegistry[cell.type];
499
602
  const status = state.cells.status[id];
500
- if (status?.type !== 'sql' || !status.resultView)
603
+ const resultRelation = status && registryItem?.getResultRelation
604
+ ? registryItem.getResultRelation(status)
605
+ : undefined;
606
+ if (!resultRelation)
501
607
  return;
502
608
  const connector = await state.db.getConnector();
503
- const baseQuery = sanitizeQuery(`SELECT * FROM ${status.resultView}`);
609
+ const baseQuery = sanitizeQuery(`SELECT * FROM ${resultRelation}`);
504
610
  const pagedQuery = makePagedQuery(baseQuery, sorting ?? [], pagination);
505
611
  const arrowTable = await connector.query(pagedQuery);
506
- const countResult = await connector.query(`SELECT COUNT(*)::int AS count FROM ${status.resultView}`);
612
+ const countResult = await connector.query(`SELECT COUNT(*)::int AS count FROM ${resultRelation}`);
507
613
  const totalRows = countResult.toArray()[0]?.count ?? 0;
508
614
  get().cells.setCellResultPage(id, { arrowTable, totalRows });
509
615
  },
@@ -547,13 +653,19 @@ export function createCellsSlice(props) {
547
653
  }
548
654
  catch (error) {
549
655
  const message = error instanceof Error ? error.message : String(error);
550
- set((state) => produce(state, (draft) => {
551
- const status = draft.cells.status[cellId];
552
- if (status?.type === 'sql') {
553
- status.status = 'error';
554
- status.lastError = message;
555
- }
556
- }));
656
+ const failedCell = get().cells.config.data[cellId];
657
+ const failedRegistryItem = failedCell
658
+ ? cellRegistry[failedCell.type]
659
+ : undefined;
660
+ if (failedRegistryItem?.recordError) {
661
+ set((state) => produce(state, (draft) => {
662
+ const status = draft.cells.status[cellId];
663
+ if (status) {
664
+ draft.cells.status[cellId] =
665
+ failedRegistryItem.recordError(status, message);
666
+ }
667
+ }));
668
+ }
557
669
  }
558
670
  }
559
671
  },
@@ -585,13 +697,19 @@ export function createCellsSlice(props) {
585
697
  }
586
698
  catch (error) {
587
699
  const message = error instanceof Error ? error.message : String(error);
588
- set((state) => produce(state, (draft) => {
589
- const status = draft.cells.status[cellId];
590
- if (status?.type === 'sql') {
591
- status.status = 'error';
592
- status.lastError = message;
593
- }
594
- }));
700
+ const failedCell = get().cells.config.data[cellId];
701
+ const failedRegistryItem = failedCell
702
+ ? cellRegistry[failedCell.type]
703
+ : undefined;
704
+ if (failedRegistryItem?.recordError) {
705
+ set((state) => produce(state, (draft) => {
706
+ const status = draft.cells.status[cellId];
707
+ if (status) {
708
+ draft.cells.status[cellId] =
709
+ failedRegistryItem.recordError(status, message);
710
+ }
711
+ }));
712
+ }
595
713
  }
596
714
  }
597
715
  },