@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.
- package/LICENSE.md +9 -0
- package/README.md +32 -0
- package/dist/cellsSlice.d.ts +3 -0
- package/dist/cellsSlice.d.ts.map +1 -0
- package/dist/cellsSlice.js +602 -0
- package/dist/cellsSlice.js.map +1 -0
- package/dist/components/DeleteSheetModal.d.ts +9 -0
- package/dist/components/DeleteSheetModal.d.ts.map +1 -0
- package/dist/components/DeleteSheetModal.js +6 -0
- package/dist/components/DeleteSheetModal.js.map +1 -0
- package/dist/components/Input/ConfigPanel/DropdownConfig.d.ts +2 -0
- package/dist/components/Input/ConfigPanel/DropdownConfig.d.ts.map +1 -0
- package/dist/components/Input/ConfigPanel/DropdownConfig.js +44 -0
- package/dist/components/Input/ConfigPanel/DropdownConfig.js.map +1 -0
- package/dist/components/Input/ConfigPanel/InputConfigPanel.d.ts +11 -0
- package/dist/components/Input/ConfigPanel/InputConfigPanel.d.ts.map +1 -0
- package/dist/components/Input/ConfigPanel/InputConfigPanel.js +65 -0
- package/dist/components/Input/ConfigPanel/InputConfigPanel.js.map +1 -0
- package/dist/components/Input/ConfigPanel/SliderConfig.d.ts +2 -0
- package/dist/components/Input/ConfigPanel/SliderConfig.d.ts.map +1 -0
- package/dist/components/Input/ConfigPanel/SliderConfig.js +19 -0
- package/dist/components/Input/ConfigPanel/SliderConfig.js.map +1 -0
- package/dist/components/Input/ConfigPanel/TextConfig.d.ts +2 -0
- package/dist/components/Input/ConfigPanel/TextConfig.d.ts.map +1 -0
- package/dist/components/Input/ConfigPanel/TextConfig.js +8 -0
- package/dist/components/Input/ConfigPanel/TextConfig.js.map +1 -0
- package/dist/components/Input/helpers.d.ts +3 -0
- package/dist/components/Input/helpers.d.ts.map +1 -0
- package/dist/components/Input/helpers.js +28 -0
- package/dist/components/Input/helpers.js.map +1 -0
- package/dist/components/InputCellContent.d.ts +9 -0
- package/dist/components/InputCellContent.d.ts.map +1 -0
- package/dist/components/InputCellContent.js +38 -0
- package/dist/components/InputCellContent.js.map +1 -0
- package/dist/components/SheetsTabBar.d.ts +6 -0
- package/dist/components/SheetsTabBar.d.ts.map +1 -0
- package/dist/components/SheetsTabBar.js +57 -0
- package/dist/components/SheetsTabBar.js.map +1 -0
- package/dist/components/SqlCellContent.d.ts +9 -0
- package/dist/components/SqlCellContent.d.ts.map +1 -0
- package/dist/components/SqlCellContent.js +222 -0
- package/dist/components/SqlCellContent.js.map +1 -0
- package/dist/components/SqlCellRunButton.d.ts +17 -0
- package/dist/components/SqlCellRunButton.d.ts.map +1 -0
- package/dist/components/SqlCellRunButton.js +11 -0
- package/dist/components/SqlCellRunButton.js.map +1 -0
- package/dist/components/TextCellContent.d.ts +9 -0
- package/dist/components/TextCellContent.d.ts.map +1 -0
- package/dist/components/TextCellContent.js +27 -0
- package/dist/components/TextCellContent.js.map +1 -0
- package/dist/components/VegaCellContent.d.ts +9 -0
- package/dist/components/VegaCellContent.d.ts.map +1 -0
- package/dist/components/VegaCellContent.js +66 -0
- package/dist/components/VegaCellContent.js.map +1 -0
- package/dist/components/VegaConfigPanel.d.ts +8 -0
- package/dist/components/VegaConfigPanel.d.ts.map +1 -0
- package/dist/components/VegaConfigPanel.js +72 -0
- package/dist/components/VegaConfigPanel.js.map +1 -0
- package/dist/dagUtils.d.ts +20 -0
- package/dist/dagUtils.d.ts.map +1 -0
- package/dist/dagUtils.js +233 -0
- package/dist/dagUtils.js.map +1 -0
- package/dist/defaultCellRegistry.d.ts +3 -0
- package/dist/defaultCellRegistry.d.ts.map +1 -0
- package/dist/defaultCellRegistry.js +177 -0
- package/dist/defaultCellRegistry.js.map +1 -0
- package/dist/execution.d.ts +9 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +195 -0
- package/dist/execution.js.map +1 -0
- package/dist/helpers.d.ts +45 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +124 -0
- package/dist/helpers.js.map +1 -0
- package/dist/hooks.d.ts +11 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +9 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/resultRelationPolicy.d.ts +74 -0
- package/dist/resultRelationPolicy.d.ts.map +1 -0
- package/dist/resultRelationPolicy.js +74 -0
- package/dist/resultRelationPolicy.js.map +1 -0
- package/dist/sqlHelpers.d.ts +41 -0
- package/dist/sqlHelpers.d.ts.map +1 -0
- package/dist/sqlHelpers.js +190 -0
- package/dist/sqlHelpers.js.map +1 -0
- package/dist/types.d.ts +501 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +144 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +35 -0
- package/dist/utils.js.map +1 -0
- 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 @@
|
|
|
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
|