@sqlrooms/cells 0.29.0-rc.1

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 (99) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +32 -0
  3. package/dist/cellsSlice.d.ts +3 -0
  4. package/dist/cellsSlice.d.ts.map +1 -0
  5. package/dist/cellsSlice.js +602 -0
  6. package/dist/cellsSlice.js.map +1 -0
  7. package/dist/components/DeleteSheetModal.d.ts +9 -0
  8. package/dist/components/DeleteSheetModal.d.ts.map +1 -0
  9. package/dist/components/DeleteSheetModal.js +6 -0
  10. package/dist/components/DeleteSheetModal.js.map +1 -0
  11. package/dist/components/Input/ConfigPanel/DropdownConfig.d.ts +2 -0
  12. package/dist/components/Input/ConfigPanel/DropdownConfig.d.ts.map +1 -0
  13. package/dist/components/Input/ConfigPanel/DropdownConfig.js +44 -0
  14. package/dist/components/Input/ConfigPanel/DropdownConfig.js.map +1 -0
  15. package/dist/components/Input/ConfigPanel/InputConfigPanel.d.ts +11 -0
  16. package/dist/components/Input/ConfigPanel/InputConfigPanel.d.ts.map +1 -0
  17. package/dist/components/Input/ConfigPanel/InputConfigPanel.js +65 -0
  18. package/dist/components/Input/ConfigPanel/InputConfigPanel.js.map +1 -0
  19. package/dist/components/Input/ConfigPanel/SliderConfig.d.ts +2 -0
  20. package/dist/components/Input/ConfigPanel/SliderConfig.d.ts.map +1 -0
  21. package/dist/components/Input/ConfigPanel/SliderConfig.js +19 -0
  22. package/dist/components/Input/ConfigPanel/SliderConfig.js.map +1 -0
  23. package/dist/components/Input/ConfigPanel/TextConfig.d.ts +2 -0
  24. package/dist/components/Input/ConfigPanel/TextConfig.d.ts.map +1 -0
  25. package/dist/components/Input/ConfigPanel/TextConfig.js +8 -0
  26. package/dist/components/Input/ConfigPanel/TextConfig.js.map +1 -0
  27. package/dist/components/Input/helpers.d.ts +3 -0
  28. package/dist/components/Input/helpers.d.ts.map +1 -0
  29. package/dist/components/Input/helpers.js +28 -0
  30. package/dist/components/Input/helpers.js.map +1 -0
  31. package/dist/components/InputCellContent.d.ts +9 -0
  32. package/dist/components/InputCellContent.d.ts.map +1 -0
  33. package/dist/components/InputCellContent.js +38 -0
  34. package/dist/components/InputCellContent.js.map +1 -0
  35. package/dist/components/SheetsTabBar.d.ts +6 -0
  36. package/dist/components/SheetsTabBar.d.ts.map +1 -0
  37. package/dist/components/SheetsTabBar.js +57 -0
  38. package/dist/components/SheetsTabBar.js.map +1 -0
  39. package/dist/components/SqlCellContent.d.ts +9 -0
  40. package/dist/components/SqlCellContent.d.ts.map +1 -0
  41. package/dist/components/SqlCellContent.js +222 -0
  42. package/dist/components/SqlCellContent.js.map +1 -0
  43. package/dist/components/SqlCellRunButton.d.ts +17 -0
  44. package/dist/components/SqlCellRunButton.d.ts.map +1 -0
  45. package/dist/components/SqlCellRunButton.js +11 -0
  46. package/dist/components/SqlCellRunButton.js.map +1 -0
  47. package/dist/components/TextCellContent.d.ts +9 -0
  48. package/dist/components/TextCellContent.d.ts.map +1 -0
  49. package/dist/components/TextCellContent.js +27 -0
  50. package/dist/components/TextCellContent.js.map +1 -0
  51. package/dist/components/VegaCellContent.d.ts +9 -0
  52. package/dist/components/VegaCellContent.d.ts.map +1 -0
  53. package/dist/components/VegaCellContent.js +66 -0
  54. package/dist/components/VegaCellContent.js.map +1 -0
  55. package/dist/components/VegaConfigPanel.d.ts +8 -0
  56. package/dist/components/VegaConfigPanel.d.ts.map +1 -0
  57. package/dist/components/VegaConfigPanel.js +72 -0
  58. package/dist/components/VegaConfigPanel.js.map +1 -0
  59. package/dist/dagUtils.d.ts +20 -0
  60. package/dist/dagUtils.d.ts.map +1 -0
  61. package/dist/dagUtils.js +233 -0
  62. package/dist/dagUtils.js.map +1 -0
  63. package/dist/defaultCellRegistry.d.ts +3 -0
  64. package/dist/defaultCellRegistry.d.ts.map +1 -0
  65. package/dist/defaultCellRegistry.js +177 -0
  66. package/dist/defaultCellRegistry.js.map +1 -0
  67. package/dist/execution.d.ts +9 -0
  68. package/dist/execution.d.ts.map +1 -0
  69. package/dist/execution.js +195 -0
  70. package/dist/execution.js.map +1 -0
  71. package/dist/helpers.d.ts +45 -0
  72. package/dist/helpers.d.ts.map +1 -0
  73. package/dist/helpers.js +124 -0
  74. package/dist/helpers.js.map +1 -0
  75. package/dist/hooks.d.ts +11 -0
  76. package/dist/hooks.d.ts.map +1 -0
  77. package/dist/hooks.js +9 -0
  78. package/dist/hooks.js.map +1 -0
  79. package/dist/index.d.ts +27 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +26 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/resultRelationPolicy.d.ts +74 -0
  84. package/dist/resultRelationPolicy.d.ts.map +1 -0
  85. package/dist/resultRelationPolicy.js +74 -0
  86. package/dist/resultRelationPolicy.js.map +1 -0
  87. package/dist/sqlHelpers.d.ts +41 -0
  88. package/dist/sqlHelpers.d.ts.map +1 -0
  89. package/dist/sqlHelpers.js +190 -0
  90. package/dist/sqlHelpers.js.map +1 -0
  91. package/dist/types.d.ts +501 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +144 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/utils.d.ts +17 -0
  96. package/dist/utils.d.ts.map +1 -0
  97. package/dist/utils.js +35 -0
  98. package/dist/utils.js.map +1 -0
  99. package/package.json +57 -0
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright 2025 SQLRooms Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @sqlrooms/cells
2
+
3
+ Shared cells model and UI primitives used by notebook and canvas views.
4
+
5
+ The package owns:
6
+
7
+ - canonical cell records (`cells.config.data`)
8
+ - sheet grouping and ordering (`cells.config.sheets`, `sheetOrder`, `currentSheetId`)
9
+ - in-sheet dependency edges and cascade execution
10
+ - SQL/result execution helpers and status tracking
11
+
12
+ ## Dependency and schema model
13
+
14
+ - Dependencies and cascades are **sheet-local** by default.
15
+ - Cross-sheet references are only supported via explicit fully qualified SQL names.
16
+ - SQL execution resolves to a stable sheet schema namespace; it does not rely on global `USE schema`.
17
+ - Unqualified result-name references are resolved to the current sheet namespace during execution.
18
+
19
+ ## Stable public API
20
+
21
+ Import from package root for stable APIs:
22
+
23
+ - slice: `createCellsSlice`
24
+ - hooks: `useCellsStore`
25
+ - helpers: `findSheetIdForCell`, `getSheetsByType`
26
+ - SQL helpers: `renderSqlWithInputs`, `findSqlDependencies`, `findSqlDependenciesFromAst`
27
+ - UI: `SqlCellContent`, `SqlCellRunButton`, `TextCellContent`, `InputCellContent`, `VegaCellContent`, `SheetsTabBar`
28
+ - types/schemas: exports from `types.ts`
29
+
30
+ ## Internal APIs
31
+
32
+ Avoid importing internal implementation modules directly (for example `dagUtils`, `execution`, or component subpaths). These are not guaranteed to be semver-stable.
@@ -0,0 +1,3 @@
1
+ import type { CellsSliceOptions, CellsSliceState } from './types';
2
+ export declare function createCellsSlice(props: CellsSliceOptions): import("zustand").StateCreator<CellsSliceState>;
3
+ //# sourceMappingURL=cellsSlice.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,602 @@
1
+ import { createId } from '@paralleldrive/cuid2';
2
+ import { makePagedQuery } from '@sqlrooms/data-table';
3
+ import { sanitizeQuery } from '@sqlrooms/duckdb';
4
+ import { createSlice } from '@sqlrooms/room-store';
5
+ import { generateUniqueName } from '@sqlrooms/utils';
6
+ import { produce } from 'immer';
7
+ import { buildGraphCacheFromEdges, buildDependencyGraph, buildDependencyGraphAsync, collectReachable, ensureGraphCache, removeCellFromCache, replaceCellDependenciesInCache, topologicalOrder, } from './dagUtils';
8
+ import { findSheetIdForCell, getRequiredSqlSelectToJson, normalizeCellsConfigStructure, resolveDependencies, } from './helpers';
9
+ import { dropResultRelation } from './resultRelationPolicy';
10
+ import { isInputCell, isSqlCell } from './types';
11
+ import { getSheetSchemaName, isDefined } from './utils';
12
+ function createDefaultCellsConfig(config) {
13
+ const sheetId = createId();
14
+ const defaultConfig = {
15
+ data: {},
16
+ sheets: {
17
+ [sheetId]: {
18
+ id: sheetId,
19
+ type: 'notebook',
20
+ title: 'Sheet',
21
+ schemaName: getSheetSchemaName(sheetId),
22
+ cellIds: [],
23
+ edges: [],
24
+ graphCache: {
25
+ dependencies: {},
26
+ dependents: {},
27
+ contentHashByCell: {},
28
+ },
29
+ },
30
+ },
31
+ sheetOrder: [sheetId],
32
+ currentSheetId: sheetId,
33
+ };
34
+ if (!config) {
35
+ return defaultConfig;
36
+ }
37
+ const { sheets, sheetOrder, currentSheetId } = normalizeCellsConfigStructure(config, defaultConfig);
38
+ return {
39
+ ...defaultConfig,
40
+ ...config,
41
+ sheets,
42
+ sheetOrder,
43
+ currentSheetId,
44
+ };
45
+ }
46
+ // --- Slice Implementation ---
47
+ export function createCellsSlice(props) {
48
+ const { cellRegistry, supportedSheetTypes = ['notebook', 'canvas'] } = props;
49
+ const initialConfig = createDefaultCellsConfig(props?.config);
50
+ // Keep result data outside Immer drafts, but scoped per slice instance.
51
+ const cellResultCache = new Map();
52
+ return createSlice((set, get, store) => {
53
+ const dropSqlResultRelation = async (resultName) => {
54
+ try {
55
+ const connector = await get().db.getConnector();
56
+ await dropResultRelation({ connector, relationName: resultName });
57
+ }
58
+ catch {
59
+ // Best-effort cleanup: relation might already be gone or connector
60
+ // might be unavailable during teardown.
61
+ }
62
+ };
63
+ return {
64
+ cells: {
65
+ cellRegistry,
66
+ supportedSheetTypes,
67
+ config: initialConfig,
68
+ status: {},
69
+ activeAbortControllers: {},
70
+ resultVersion: {},
71
+ pageVersion: {},
72
+ addCell: async (sheetId, cell, index) => {
73
+ // Pre-compute dependencies outside produce() to support async
74
+ const sqlSelectToJson = getRequiredSqlSelectToJson(get());
75
+ const targetSheet = get().cells.config.sheets[sheetId];
76
+ const scopedCells = Object.fromEntries((targetSheet?.cellIds ?? [])
77
+ .map((id) => get().cells.config.data[id])
78
+ .filter(isDefined)
79
+ .map((candidate) => [candidate.id, candidate]));
80
+ scopedCells[cell.id] = cell;
81
+ const deps = await resolveDependencies(cell, scopedCells, sheetId, cellRegistry, sqlSelectToJson);
82
+ set((state) => produce(state, (draft) => {
83
+ 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
+ }
94
+ // Single-owner invariant: a cell can belong to one sheet only.
95
+ for (const [existingSheetId, existingSheet] of Object.entries(draft.cells.config.sheets)) {
96
+ if (existingSheetId === sheetId)
97
+ continue;
98
+ existingSheet.cellIds = existingSheet.cellIds.filter((cid) => cid !== cell.id);
99
+ existingSheet.edges = existingSheet.edges.filter((e) => e.source !== cell.id && e.target !== cell.id);
100
+ removeCellFromCache(existingSheet, cell.id);
101
+ }
102
+ let sheet = draft.cells.config.sheets[sheetId];
103
+ if (!sheet) {
104
+ sheet = {
105
+ id: sheetId,
106
+ type: 'notebook',
107
+ title: 'Sheet',
108
+ schemaName: getSheetSchemaName(sheetId),
109
+ cellIds: [],
110
+ edges: [],
111
+ graphCache: {
112
+ dependencies: {},
113
+ dependents: {},
114
+ contentHashByCell: {},
115
+ },
116
+ };
117
+ draft.cells.config.sheets[sheetId] = sheet;
118
+ if (!draft.cells.config.sheetOrder.includes(sheetId)) {
119
+ draft.cells.config.sheetOrder.push(sheetId);
120
+ }
121
+ if (!draft.cells.config.currentSheetId) {
122
+ draft.cells.config.currentSheetId = sheetId;
123
+ }
124
+ }
125
+ const newIndex = index !== undefined ? index : sheet.cellIds.length;
126
+ sheet.cellIds = sheet.cellIds.filter((cid) => cid !== cell.id);
127
+ sheet.cellIds.splice(newIndex, 0, cell.id);
128
+ // Add edges from pre-computed dependencies
129
+ sheet.edges = sheet.edges.filter((edge) => edge.target !== cell.id &&
130
+ edge.id !== `${edge.source}-${cell.id}`);
131
+ const localCellIds = new Set(sheet.cellIds);
132
+ for (const depId of deps) {
133
+ if (!localCellIds.has(depId) || depId === cell.id)
134
+ continue;
135
+ sheet.edges.push({
136
+ id: `${depId}-${cell.id}`,
137
+ source: depId,
138
+ target: cell.id,
139
+ });
140
+ }
141
+ replaceCellDependenciesInCache(sheet, cell.id, deps.filter((depId) => localCellIds.has(depId)));
142
+ }));
143
+ },
144
+ removeCell: (id) => {
145
+ const previousStatus = get().cells.status[id];
146
+ cellResultCache.delete(id);
147
+ set((state) => produce(state, (draft) => {
148
+ delete draft.cells.config.data[id];
149
+ delete draft.cells.status[id];
150
+ delete draft.cells.resultVersion[id];
151
+ delete draft.cells.pageVersion[id];
152
+ const controller = draft.cells.activeAbortControllers[id];
153
+ if (controller) {
154
+ controller.abort();
155
+ delete draft.cells.activeAbortControllers[id];
156
+ }
157
+ // Remove from all sheets
158
+ for (const sheet of Object.values(draft.cells.config.sheets)) {
159
+ sheet.cellIds = sheet.cellIds.filter((cid) => cid !== id);
160
+ sheet.edges = sheet.edges.filter((e) => e.source !== id && e.target !== id);
161
+ removeCellFromCache(sheet, id);
162
+ }
163
+ }));
164
+ if (previousStatus?.type === 'sql') {
165
+ void dropSqlResultRelation(previousStatus.resultView);
166
+ }
167
+ },
168
+ updateCell: async (id, updater, opts) => {
169
+ const cell = get().cells.config.data[id];
170
+ if (!cell)
171
+ return;
172
+ const ownerSheetId = findSheetIdForCell(get(), id);
173
+ const scopedCells = Object.fromEntries(((ownerSheetId &&
174
+ get().cells.config.sheets[ownerSheetId]?.cellIds) ||
175
+ [])
176
+ .map((cellId) => get().cells.config.data[cellId])
177
+ .filter(isDefined)
178
+ .map((candidate) => [candidate.id, candidate]));
179
+ const updatedCell = updater(cell);
180
+ 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;
185
+ const existingStatus = get().cells.status[id];
186
+ const hasExistingView = resultNameChanged &&
187
+ existingStatus?.type === 'sql' &&
188
+ existingStatus.status === 'success' &&
189
+ existingStatus.resultView;
190
+ const semanticInputChanged = isInputCell(cell) &&
191
+ isInputCell(updatedCell) &&
192
+ (cell.data.input.varName !== updatedCell.data.input.varName ||
193
+ cell.data.input.value !== updatedCell.data.input.value);
194
+ const semanticSqlChanged = isSqlCell(cell) &&
195
+ isSqlCell(updatedCell) &&
196
+ cell.data.sql !== updatedCell.data.sql;
197
+ // Apply cell data to the store immediately so that other
198
+ // actions (e.g. runCell triggered via Cmd+Enter) always see
199
+ // the latest value. Edge/dependency updates follow
200
+ // asynchronously below.
201
+ set((state) => produce(state, (draft) => {
202
+ draft.cells.config.data[id] = updatedCell;
203
+ }));
204
+ // Avoid expensive dependency graph recomputation on each SQL keystroke.
205
+ // We recompute immediately for non-SQL edits, or when callers explicitly
206
+ // request cascading behavior.
207
+ const shouldRecomputeDependencies = !semanticSqlChanged || opts?.cascade === true;
208
+ if (shouldRecomputeDependencies) {
209
+ // Pre-compute dependencies outside produce() to support async
210
+ const sqlSelectToJson = getRequiredSqlSelectToJson(get());
211
+ const newDeps = await resolveDependencies(updatedCell, scopedCells, ownerSheetId || '', cellRegistry, sqlSelectToJson);
212
+ set((state) => produce(state, (draft) => {
213
+ const ownerSheet = Object.values(draft.cells.config.sheets).find((sheet) => sheet.cellIds.includes(id));
214
+ if (ownerSheet) {
215
+ const localCellIds = new Set(ownerSheet.cellIds);
216
+ ownerSheet.edges = ownerSheet.edges.filter((e) => e.target !== id);
217
+ for (const depId of newDeps) {
218
+ if (!localCellIds.has(depId) || depId === id)
219
+ continue;
220
+ ownerSheet.edges.push({
221
+ id: `${depId}-${id}`,
222
+ source: depId,
223
+ target: id,
224
+ });
225
+ }
226
+ replaceCellDependenciesInCache(ownerSheet, id, newDeps.filter((depId) => localCellIds.has(depId)));
227
+ }
228
+ }));
229
+ }
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
+ }
241
+ }
242
+ // After update, trigger cascade only if explicitly requested
243
+ // or if semantic execution inputs changed.
244
+ const shouldCascade = opts?.cascade || resultNameChanged || semanticInputChanged;
245
+ if (shouldCascade) {
246
+ if (ownerSheetId) {
247
+ void get().cells.runDownstreamCascade(ownerSheetId, id);
248
+ }
249
+ }
250
+ },
251
+ addSheet: (title, type = 'notebook') => {
252
+ const id = createId();
253
+ set((state) => produce(state, (draft) => {
254
+ const typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
255
+ const existingTitles = Object.values(draft.cells.config.sheets).map((s) => s.title);
256
+ const defaultTitle = generateUniqueName(`${typeLabel} 1`, existingTitles, ' ');
257
+ draft.cells.config.sheets[id] = {
258
+ id,
259
+ type,
260
+ title: title || defaultTitle,
261
+ schemaName: getSheetSchemaName(id),
262
+ cellIds: [],
263
+ edges: [],
264
+ graphCache: {
265
+ dependencies: {},
266
+ dependents: {},
267
+ contentHashByCell: {},
268
+ },
269
+ };
270
+ draft.cells.config.sheetOrder.push(id);
271
+ draft.cells.config.currentSheetId = id;
272
+ }));
273
+ return id;
274
+ },
275
+ removeSheet: (sheetId) => {
276
+ const sheet = get().cells.config.sheets[sheetId];
277
+ if (!sheet)
278
+ return;
279
+ const ownedCellIds = [...sheet.cellIds];
280
+ const relationNamesToDrop = ownedCellIds.flatMap((cellId) => {
281
+ const status = get().cells.status[cellId];
282
+ return status?.type === 'sql' && status.resultView
283
+ ? [status.resultView]
284
+ : [];
285
+ });
286
+ set((state) => produce(state, (draft) => {
287
+ for (const cellId of ownedCellIds) {
288
+ cellResultCache.delete(cellId);
289
+ delete draft.cells.config.data[cellId];
290
+ delete draft.cells.status[cellId];
291
+ delete draft.cells.resultVersion[cellId];
292
+ delete draft.cells.pageVersion[cellId];
293
+ const controller = draft.cells.activeAbortControllers[cellId];
294
+ if (controller) {
295
+ controller.abort();
296
+ }
297
+ delete draft.cells.activeAbortControllers[cellId];
298
+ }
299
+ for (const existingSheet of Object.values(draft.cells.config.sheets)) {
300
+ existingSheet.cellIds = existingSheet.cellIds.filter((cid) => !ownedCellIds.includes(cid));
301
+ existingSheet.edges = existingSheet.edges.filter((e) => !ownedCellIds.includes(e.source) &&
302
+ !ownedCellIds.includes(e.target));
303
+ for (const cellId of ownedCellIds) {
304
+ removeCellFromCache(existingSheet, cellId);
305
+ }
306
+ }
307
+ delete draft.cells.config.sheets[sheetId];
308
+ draft.cells.config.sheetOrder =
309
+ draft.cells.config.sheetOrder.filter((id) => id !== sheetId);
310
+ if (draft.cells.config.currentSheetId === sheetId) {
311
+ draft.cells.config.currentSheetId =
312
+ draft.cells.config.sheetOrder[0];
313
+ }
314
+ }));
315
+ for (const relationName of relationNamesToDrop) {
316
+ void dropSqlResultRelation(relationName);
317
+ }
318
+ },
319
+ closeSheet: (sheetId) => {
320
+ set((state) => produce(state, (draft) => {
321
+ draft.cells.config.sheetOrder =
322
+ draft.cells.config.sheetOrder.filter((id) => id !== sheetId);
323
+ if (draft.cells.config.currentSheetId === sheetId) {
324
+ draft.cells.config.currentSheetId =
325
+ draft.cells.config.sheetOrder[0];
326
+ }
327
+ }));
328
+ },
329
+ openSheet: (sheetId) => {
330
+ set((state) => produce(state, (draft) => {
331
+ if (!draft.cells.config.sheetOrder.includes(sheetId)) {
332
+ draft.cells.config.sheetOrder.push(sheetId);
333
+ }
334
+ draft.cells.config.currentSheetId = sheetId;
335
+ }));
336
+ },
337
+ setSheetOrder: (sheetOrder) => {
338
+ set((state) => produce(state, (draft) => {
339
+ draft.cells.config.sheetOrder = sheetOrder;
340
+ }));
341
+ },
342
+ renameSheet: (sheetId, title) => {
343
+ set((state) => produce(state, (draft) => {
344
+ const sheet = draft.cells.config.sheets[sheetId];
345
+ if (sheet) {
346
+ // We could add validation here, but usually UI handles it.
347
+ // For now, we'll just allow it as the primary logic is in view slices.
348
+ sheet.title = title;
349
+ }
350
+ }));
351
+ },
352
+ setCurrentSheet: (sheetId) => {
353
+ set((state) => produce(state, (draft) => {
354
+ draft.cells.config.currentSheetId = sheetId;
355
+ }));
356
+ },
357
+ addEdge: (sheetId, edge) => {
358
+ set((state) => produce(state, (draft) => {
359
+ let sheet = draft.cells.config.sheets[sheetId];
360
+ if (!sheet) {
361
+ sheet = {
362
+ id: sheetId,
363
+ type: 'notebook',
364
+ title: 'Sheet',
365
+ schemaName: getSheetSchemaName(sheetId),
366
+ cellIds: [],
367
+ edges: [],
368
+ graphCache: {
369
+ dependencies: {},
370
+ dependents: {},
371
+ contentHashByCell: {},
372
+ },
373
+ };
374
+ draft.cells.config.sheets[sheetId] = sheet;
375
+ if (!draft.cells.config.sheetOrder.includes(sheetId)) {
376
+ draft.cells.config.sheetOrder.push(sheetId);
377
+ }
378
+ if (!draft.cells.config.currentSheetId) {
379
+ draft.cells.config.currentSheetId = sheetId;
380
+ }
381
+ }
382
+ if (!sheet.cellIds.includes(edge.source) ||
383
+ !sheet.cellIds.includes(edge.target)) {
384
+ return;
385
+ }
386
+ const id = `${edge.source}-${edge.target}`;
387
+ if (!sheet.edges.find((e) => e.id === id)) {
388
+ sheet.edges.push({ ...edge, id });
389
+ ensureGraphCache(sheet);
390
+ const deps = sheet.graphCache?.dependencies[edge.target] || [];
391
+ replaceCellDependenciesInCache(sheet, edge.target, [
392
+ ...deps,
393
+ edge.source,
394
+ ]);
395
+ }
396
+ }));
397
+ },
398
+ removeEdge: (sheetId, edgeId) => {
399
+ set((state) => produce(state, (draft) => {
400
+ const sheet = draft.cells.config.sheets[sheetId];
401
+ if (sheet) {
402
+ sheet.edges = sheet.edges.filter((e) => e.id !== edgeId);
403
+ delete sheet.graphCache;
404
+ }
405
+ }));
406
+ },
407
+ updateEdgesFromSql: async (sheetId, cellId) => {
408
+ const cell = get().cells.config.data[cellId];
409
+ const sheet = get().cells.config.sheets[sheetId];
410
+ if (!cell || !sheet)
411
+ return;
412
+ // Pre-compute dependencies outside produce() to support async
413
+ const sqlSelectToJson = getRequiredSqlSelectToJson(get());
414
+ const scopedCells = Object.fromEntries(sheet.cellIds
415
+ .map((id) => get().cells.config.data[id])
416
+ .filter(isDefined)
417
+ .map((candidate) => [candidate.id, candidate]));
418
+ const deps = await resolveDependencies(cell, scopedCells, sheetId, cellRegistry, sqlSelectToJson);
419
+ set((state) => produce(state, (draft) => {
420
+ const draftSheet = draft.cells.config.sheets[sheetId];
421
+ if (draftSheet) {
422
+ draftSheet.edges = draftSheet.edges.filter((e) => e.target !== cellId);
423
+ const localCellIds = new Set(draftSheet.cellIds);
424
+ for (const depId of deps) {
425
+ if (!localCellIds.has(depId) || depId === cellId)
426
+ continue;
427
+ draftSheet.edges.push({
428
+ id: `${depId}-${cellId}`,
429
+ source: depId,
430
+ target: cellId,
431
+ });
432
+ }
433
+ replaceCellDependenciesInCache(draftSheet, cellId, deps.filter((depId) => localCellIds.has(depId)));
434
+ }
435
+ }));
436
+ },
437
+ runCell: async (id, opts) => {
438
+ const state = get();
439
+ const cell = state.cells.config.data[id];
440
+ if (!cell)
441
+ return;
442
+ const registryItem = cellRegistry[cell.type];
443
+ if (!registryItem)
444
+ return;
445
+ if (registryItem.runCell) {
446
+ // Pass get/set to registry's runCell
447
+ await registryItem.runCell({ id, opts, get, set });
448
+ return;
449
+ }
450
+ // No default behavior - each cell type must define runCell if needed
451
+ },
452
+ cancelCell: (id) => {
453
+ const state = get();
454
+ const controller = state.cells.activeAbortControllers[id];
455
+ if (controller) {
456
+ controller.abort();
457
+ }
458
+ },
459
+ invalidateCellStatus: (id) => {
460
+ set((state) => produce(state, (draft) => {
461
+ 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
+ };
469
+ }
470
+ }));
471
+ },
472
+ // Cell result cache actions
473
+ setCellResult: (id, data) => {
474
+ cellResultCache.set(id, data);
475
+ set((state) => produce(state, (draft) => {
476
+ draft.cells.resultVersion[id] =
477
+ (draft.cells.resultVersion[id] ?? 0) + 1;
478
+ }));
479
+ },
480
+ setCellResultPage: (id, data) => {
481
+ cellResultCache.set(id, data);
482
+ set((state) => produce(state, (draft) => {
483
+ draft.cells.pageVersion[id] =
484
+ (draft.cells.pageVersion[id] ?? 0) + 1;
485
+ }));
486
+ },
487
+ getCellResult: (id) => {
488
+ return cellResultCache.get(id);
489
+ },
490
+ clearCellResult: (id) => {
491
+ cellResultCache.delete(id);
492
+ set((state) => produce(state, (draft) => {
493
+ delete draft.cells.resultVersion[id];
494
+ delete draft.cells.pageVersion[id];
495
+ }));
496
+ },
497
+ fetchCellResultPage: async (id, pagination, sorting) => {
498
+ const state = get();
499
+ const status = state.cells.status[id];
500
+ if (status?.type !== 'sql' || !status.resultView)
501
+ return;
502
+ const connector = await state.db.getConnector();
503
+ const baseQuery = sanitizeQuery(`SELECT * FROM ${status.resultView}`);
504
+ const pagedQuery = makePagedQuery(baseQuery, sorting ?? [], pagination);
505
+ const arrowTable = await connector.query(pagedQuery);
506
+ const countResult = await connector.query(`SELECT COUNT(*)::int AS count FROM ${status.resultView}`);
507
+ const totalRows = countResult.toArray()[0]?.count ?? 0;
508
+ get().cells.setCellResultPage(id, { arrowTable, totalRows });
509
+ },
510
+ // DAG methods (sync versions for UI usage)
511
+ getRootCells: (sheetId) => {
512
+ const { dependencies } = buildDependencyGraph(sheetId, get());
513
+ const ids = Object.keys(dependencies);
514
+ return ids.filter((id) => (dependencies[id]?.length ?? 0) === 0);
515
+ },
516
+ getDownstream: (sheetId, sourceCellId) => {
517
+ const { dependencies, dependents } = buildDependencyGraph(sheetId, get());
518
+ const reachable = collectReachable(sourceCellId, dependents);
519
+ if (!reachable.size)
520
+ return [];
521
+ reachable.delete(sourceCellId);
522
+ const rootsWithinScope = Array.from(reachable).filter((id) => {
523
+ const deps = dependencies[id] || [];
524
+ return deps.filter((d) => reachable.has(d)).length === 0;
525
+ });
526
+ return topologicalOrder(rootsWithinScope, dependencies, dependents, reachable);
527
+ },
528
+ // Async cascade execution using AST-based dependency resolution
529
+ runAllCellsCascade: async (sheetId) => {
530
+ const cached = buildDependencyGraph(sheetId, get());
531
+ const hasCachedNodes = Object.keys(cached.dependencies).length > 0;
532
+ if (!hasCachedNodes) {
533
+ set((state) => produce(state, (draft) => {
534
+ const sheet = draft.cells.config.sheets[sheetId];
535
+ if (sheet) {
536
+ sheet.graphCache = buildGraphCacheFromEdges(sheet);
537
+ }
538
+ }));
539
+ }
540
+ const sqlSelectToJson = getRequiredSqlSelectToJson(get());
541
+ const { dependencies, dependents } = await buildDependencyGraphAsync(sheetId, get(), sqlSelectToJson);
542
+ const roots = Object.keys(dependencies).filter((id) => (dependencies[id]?.length ?? 0) === 0);
543
+ const order = topologicalOrder(roots, dependencies, dependents);
544
+ for (const cellId of order) {
545
+ try {
546
+ await get().cells.runCell(cellId, { cascade: false });
547
+ }
548
+ catch (error) {
549
+ 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
+ }));
557
+ }
558
+ }
559
+ },
560
+ runDownstreamCascade: async (sheetId, sourceCellId) => {
561
+ const cached = buildDependencyGraph(sheetId, get());
562
+ const hasCachedNodes = Object.keys(cached.dependencies).length > 0;
563
+ if (!hasCachedNodes) {
564
+ set((state) => produce(state, (draft) => {
565
+ const sheet = draft.cells.config.sheets[sheetId];
566
+ if (sheet) {
567
+ sheet.graphCache = buildGraphCacheFromEdges(sheet);
568
+ }
569
+ }));
570
+ }
571
+ const sqlSelectToJson = getRequiredSqlSelectToJson(get());
572
+ const { dependencies, dependents } = await buildDependencyGraphAsync(sheetId, get(), sqlSelectToJson);
573
+ const reachable = collectReachable(sourceCellId, dependents);
574
+ if (!reachable.size)
575
+ return;
576
+ reachable.delete(sourceCellId);
577
+ const rootsWithinScope = Array.from(reachable).filter((id) => {
578
+ const deps = dependencies[id] || [];
579
+ return deps.filter((d) => reachable.has(d)).length === 0;
580
+ });
581
+ const order = topologicalOrder(rootsWithinScope, dependencies, dependents, reachable);
582
+ for (const cellId of order) {
583
+ try {
584
+ await get().cells.runCell(cellId, { cascade: false });
585
+ }
586
+ catch (error) {
587
+ 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
+ }));
595
+ }
596
+ }
597
+ },
598
+ },
599
+ };
600
+ });
601
+ }
602
+ //# sourceMappingURL=cellsSlice.js.map