@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
@@ -0,0 +1,74 @@
1
+ function toRelationSqlName(relationName) {
2
+ return relationName.toString();
3
+ }
4
+ /**
5
+ * Chooses whether a SQL cell result should be stored as a logical view or a
6
+ * materialized table.
7
+ *
8
+ * The policy is sticky: once a relation type has been selected for a cell,
9
+ * later executions preserve that choice. On first execution, downstream usage
10
+ * is used as a proxy for reuse.
11
+ */
12
+ export function chooseAdaptiveRelationType(args) {
13
+ const { previousRelationType, hasDownstream } = args;
14
+ if (previousRelationType)
15
+ return previousRelationType;
16
+ return hasDownstream ? 'table' : 'view';
17
+ }
18
+ /**
19
+ * Creates or replaces the relation that backs a SQL cell result.
20
+ *
21
+ * - `table`: materializes data for stable repeated reads.
22
+ * - `view`: stores query definition for lightweight freshness.
23
+ */
24
+ export async function createOrReplaceResultRelation(args) {
25
+ const { connector, relationName, relationType, sql, signal } = args;
26
+ const relationSqlName = toRelationSqlName(relationName);
27
+ if (relationType === 'table') {
28
+ await connector.query(`CREATE OR REPLACE TABLE ${relationSqlName} AS ${sql}`, {
29
+ signal,
30
+ });
31
+ return;
32
+ }
33
+ await connector.query(`CREATE OR REPLACE VIEW ${relationSqlName} AS ${sql}`, {
34
+ signal,
35
+ });
36
+ }
37
+ /**
38
+ * Drops both view and table variants for a relation name.
39
+ *
40
+ * This is intentionally defensive because different runs may have used
41
+ * different relation modes.
42
+ */
43
+ export async function dropResultRelation(args) {
44
+ const { connector, relationName } = args;
45
+ if (!relationName)
46
+ return;
47
+ const relationSqlName = toRelationSqlName(relationName);
48
+ await connector.query(`DROP VIEW IF EXISTS ${relationSqlName}`);
49
+ await connector.query(`DROP TABLE IF EXISTS ${relationSqlName}`);
50
+ }
51
+ /**
52
+ * Renames a saved SQL cell result relation while preserving its mode.
53
+ *
54
+ * - `table`: copies current relation data into the new name.
55
+ * - `view`: recreates the view definition under the new name.
56
+ *
57
+ * Any existing destination relation is removed first, and the old source name
58
+ * is cleaned up after the new relation is created.
59
+ */
60
+ export async function renameResultRelation(args) {
61
+ const { connector, oldRelationName, newRelationName, relationType, viewSql } = args;
62
+ const oldRelationSqlName = toRelationSqlName(oldRelationName);
63
+ const newRelationSqlName = toRelationSqlName(newRelationName);
64
+ // Clear destination first in case a stale object exists.
65
+ await dropResultRelation({ connector, relationName: newRelationSqlName });
66
+ if (relationType === 'table') {
67
+ await connector.query(`CREATE TABLE ${newRelationSqlName} AS SELECT * FROM ${oldRelationSqlName}`);
68
+ }
69
+ else {
70
+ await connector.query(`CREATE VIEW ${newRelationSqlName} AS ${viewSql}`);
71
+ }
72
+ await dropResultRelation({ connector, relationName: oldRelationSqlName });
73
+ }
74
+ //# sourceMappingURL=resultRelationPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resultRelationPolicy.js","sourceRoot":"","sources":["../src/resultRelationPolicy.ts"],"names":[],"mappings":"AAyBA,SAAS,iBAAiB,CAAC,YAAgC;IACzD,OAAO,YAAY,CAAC,QAAQ,EAAE,CAAC;AACjC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,0BAA0B,CAAC,IAG1C;IACC,MAAM,EAAC,oBAAoB,EAAE,aAAa,EAAC,GAAG,IAAI,CAAC;IACnD,IAAI,oBAAoB;QAAE,OAAO,oBAAoB,CAAC;IACtD,OAAO,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAAC,IAMnD;IACC,MAAM,EAAC,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAC,GAAG,IAAI,CAAC;IAClE,MAAM,eAAe,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;IACxD,IAAI,YAAY,KAAK,OAAO,EAAE,CAAC;QAC7B,MAAM,SAAS,CAAC,KAAK,CACnB,2BAA2B,eAAe,OAAO,GAAG,EAAE,EACtD;YACE,MAAM;SACP,CACF,CAAC;QACF,OAAO;IACT,CAAC;IACD,MAAM,SAAS,CAAC,KAAK,CAAC,0BAA0B,eAAe,OAAO,GAAG,EAAE,EAAE;QAC3E,MAAM;KACP,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAGxC;IACC,MAAM,EAAC,SAAS,EAAE,YAAY,EAAC,GAAG,IAAI,CAAC;IACvC,IAAI,CAAC,YAAY;QAAE,OAAO;IAC1B,MAAM,eAAe,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;IACxD,MAAM,SAAS,CAAC,KAAK,CAAC,uBAAuB,eAAe,EAAE,CAAC,CAAC;IAChE,MAAM,SAAS,CAAC,KAAK,CAAC,wBAAwB,eAAe,EAAE,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,IAM1C;IACC,MAAM,EAAC,SAAS,EAAE,eAAe,EAAE,eAAe,EAAE,YAAY,EAAE,OAAO,EAAC,GACxE,IAAI,CAAC;IACP,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,eAAe,CAAC,CAAC;IAC9D,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,eAAe,CAAC,CAAC;IAE9D,yDAAyD;IACzD,MAAM,kBAAkB,CAAC,EAAC,SAAS,EAAE,YAAY,EAAE,kBAAkB,EAAC,CAAC,CAAC;IAExE,IAAI,YAAY,KAAK,OAAO,EAAE,CAAC;QAC7B,MAAM,SAAS,CAAC,KAAK,CACnB,gBAAgB,kBAAkB,qBAAqB,kBAAkB,EAAE,CAC5E,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,MAAM,SAAS,CAAC,KAAK,CAAC,eAAe,kBAAkB,OAAO,OAAO,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,MAAM,kBAAkB,CAAC,EAAC,SAAS,EAAE,YAAY,EAAE,kBAAkB,EAAC,CAAC,CAAC;AAC1E,CAAC","sourcesContent":["import type {QualifiedTableName} from '@sqlrooms/duckdb';\n\n/**\n * Centralized policy for persisting SQL cell query results.\n *\n * This module implements an adaptive strategy:\n * - first run chooses `table` when downstream dependencies exist, otherwise\n * `view`;\n * - subsequent runs preserve the previously chosen relation type.\n *\n * The policy is shared by execution, rename, and cleanup paths so behavior\n * stays consistent across cross-cutting lifecycle events. Cleanup intentionally\n * drops both relation variants (`VIEW` and `TABLE`) because historical runs or\n * migrations may have created either form for the same logical result name.\n */\nexport type ResultRelationType = 'view' | 'table';\nexport type ResultRelationName = string | QualifiedTableName;\n\ntype SqlConnectorLike = {\n query: (\n sql: string,\n options?: {signal?: AbortSignal},\n ) => PromiseLike<unknown>;\n};\n\nfunction toRelationSqlName(relationName: ResultRelationName): string {\n return relationName.toString();\n}\n\n/**\n * Chooses whether a SQL cell result should be stored as a logical view or a\n * materialized table.\n *\n * The policy is sticky: once a relation type has been selected for a cell,\n * later executions preserve that choice. On first execution, downstream usage\n * is used as a proxy for reuse.\n */\nexport function chooseAdaptiveRelationType(args: {\n previousRelationType?: ResultRelationType;\n hasDownstream: boolean;\n}): ResultRelationType {\n const {previousRelationType, hasDownstream} = args;\n if (previousRelationType) return previousRelationType;\n return hasDownstream ? 'table' : 'view';\n}\n\n/**\n * Creates or replaces the relation that backs a SQL cell result.\n *\n * - `table`: materializes data for stable repeated reads.\n * - `view`: stores query definition for lightweight freshness.\n */\nexport async function createOrReplaceResultRelation(args: {\n connector: SqlConnectorLike;\n relationName: ResultRelationName;\n relationType: ResultRelationType;\n sql: string;\n signal?: AbortSignal;\n}) {\n const {connector, relationName, relationType, sql, signal} = args;\n const relationSqlName = toRelationSqlName(relationName);\n if (relationType === 'table') {\n await connector.query(\n `CREATE OR REPLACE TABLE ${relationSqlName} AS ${sql}`,\n {\n signal,\n },\n );\n return;\n }\n await connector.query(`CREATE OR REPLACE VIEW ${relationSqlName} AS ${sql}`, {\n signal,\n });\n}\n\n/**\n * Drops both view and table variants for a relation name.\n *\n * This is intentionally defensive because different runs may have used\n * different relation modes.\n */\nexport async function dropResultRelation(args: {\n connector: SqlConnectorLike;\n relationName?: ResultRelationName;\n}) {\n const {connector, relationName} = args;\n if (!relationName) return;\n const relationSqlName = toRelationSqlName(relationName);\n await connector.query(`DROP VIEW IF EXISTS ${relationSqlName}`);\n await connector.query(`DROP TABLE IF EXISTS ${relationSqlName}`);\n}\n\n/**\n * Renames a saved SQL cell result relation while preserving its mode.\n *\n * - `table`: copies current relation data into the new name.\n * - `view`: recreates the view definition under the new name.\n *\n * Any existing destination relation is removed first, and the old source name\n * is cleaned up after the new relation is created.\n */\nexport async function renameResultRelation(args: {\n connector: SqlConnectorLike;\n oldRelationName: ResultRelationName;\n newRelationName: ResultRelationName;\n relationType: ResultRelationType;\n viewSql: string;\n}) {\n const {connector, oldRelationName, newRelationName, relationType, viewSql} =\n args;\n const oldRelationSqlName = toRelationSqlName(oldRelationName);\n const newRelationSqlName = toRelationSqlName(newRelationName);\n\n // Clear destination first in case a stale object exists.\n await dropResultRelation({connector, relationName: newRelationSqlName});\n\n if (relationType === 'table') {\n await connector.query(\n `CREATE TABLE ${newRelationSqlName} AS SELECT * FROM ${oldRelationSqlName}`,\n );\n } else {\n await connector.query(`CREATE VIEW ${newRelationSqlName} AS ${viewSql}`);\n }\n\n await dropResultRelation({connector, relationName: oldRelationSqlName});\n}\n"]}
@@ -0,0 +1,41 @@
1
+ import type { Cell, Edge, SqlDependencyOptions, SqlRenderInput, SqlRunCallbacks, SqlRunResult } from './types';
2
+ export declare function deriveEdgesFromSql(cellId: string, sql: string, allCells: Record<string, Cell>): Edge[];
3
+ export declare function renderSqlWithInputs(rawSql: string, inputs: SqlRenderInput[]): string;
4
+ export declare function findSqlDependencies<TCell extends {
5
+ id: string;
6
+ type: string;
7
+ }>(opts: {
8
+ targetCell: TCell;
9
+ cells: Record<string, TCell>;
10
+ getSqlText: (cell: TCell) => string | undefined;
11
+ getInputVarName: (cell: TCell) => string | undefined;
12
+ getSqlResultName: (cellId: string) => string | undefined;
13
+ options?: SqlDependencyOptions;
14
+ }): string[];
15
+ export declare function runSqlWithCallbacks(run: () => Promise<SqlRunResult>, callbacks?: SqlRunCallbacks): Promise<void>;
16
+ /**
17
+ * Find SQL cell dependencies using AST-based parsing.
18
+ * This provides more accurate detection than text-based matching.
19
+ */
20
+ export declare function findSqlDependenciesFromAst(opts: {
21
+ sql: string;
22
+ cells: Record<string, Cell>;
23
+ sqlSelectToJson: (sql: string) => Promise<{
24
+ error: boolean;
25
+ statements?: unknown[];
26
+ }>;
27
+ }): Promise<string[]>;
28
+ /**
29
+ * Rewrites unqualified references to known sheet-local result names as
30
+ * schema-qualified references. This keeps SQL ergonomic without mutable
31
+ * connector-level current schema state.
32
+ */
33
+ export declare function qualifySheetLocalResultNames(opts: {
34
+ sql: string;
35
+ sheetSchema: string;
36
+ sheetDatabase?: string;
37
+ sheetCellIds: string[];
38
+ cells: Record<string, Cell>;
39
+ getSqlResultName: (cellId: string) => string | undefined;
40
+ }): string;
41
+ //# sourceMappingURL=sqlHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlHelpers.d.ts","sourceRoot":"","sources":["../src/sqlHelpers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,IAAI,EACJ,IAAI,EAEJ,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACb,MAAM,SAAS,CAAC;AAIjB,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAC7B,IAAI,EAAE,CA2BR;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,EAAE,GACvB,MAAM,CAcR;AAED,wBAAgB,mBAAmB,CACjC,KAAK,SAAS;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,EACxC,IAAI,EAAE;IACN,UAAU,EAAE,KAAK,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC7B,UAAU,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC;IAChD,eAAe,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC;IACrD,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACzD,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC,GAAG,MAAM,EAAE,CAyCX;AAeD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,EAChC,SAAS,GAAE,eAAoB,iBAYhC;AAsCD;;;GAGG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC5B,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;QACxC,KAAK,EAAE,OAAO,CAAC;QACf,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC;KACxB,CAAC,CAAC;CACJ,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAkCpB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC5B,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAC1D,GAAG,MAAM,CAqCT"}
@@ -0,0 +1,190 @@
1
+ import { convertToValidColumnOrTableName } from '@sqlrooms/utils';
2
+ import { isInputCell, isSqlCell } from './types';
3
+ import { getEffectiveResultName } from './utils';
4
+ export function deriveEdgesFromSql(cellId, sql, allCells) {
5
+ const targetCell = allCells[cellId];
6
+ if (!targetCell)
7
+ return [];
8
+ const deps = findSqlDependencies({
9
+ targetCell,
10
+ cells: allCells,
11
+ getSqlText: () => sql,
12
+ getInputVarName: (cell) => isInputCell(cell) ? cell.data.input.varName : undefined,
13
+ getSqlResultName: (cid) => {
14
+ const cell = allCells[cid];
15
+ if (cell && isSqlCell(cell)) {
16
+ return getEffectiveResultName(cell.data, convertToValidColumnOrTableName);
17
+ }
18
+ return undefined;
19
+ },
20
+ });
21
+ return deps.map((sourceId) => ({
22
+ id: `${sourceId}-${cellId}`,
23
+ source: sourceId,
24
+ target: cellId,
25
+ }));
26
+ }
27
+ export function renderSqlWithInputs(rawSql, inputs) {
28
+ const varMap = {};
29
+ for (const { varName, value } of inputs) {
30
+ varMap[varName] = value;
31
+ }
32
+ return (rawSql || '').replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, (_m, v) => {
33
+ const val = varMap[v];
34
+ return typeof val === 'number'
35
+ ? String(val)
36
+ : `'${String(val ?? '')?.replace(/'/g, "''")}'`;
37
+ });
38
+ }
39
+ export function findSqlDependencies(opts) {
40
+ const { targetCell, cells, getSqlText, getInputVarName, getSqlResultName, options, } = opts;
41
+ const sql = getSqlText(targetCell) || '';
42
+ const sqlLower = sql.toLowerCase();
43
+ const deps = [];
44
+ const inputTypes = options?.inputTypes ?? ['input'];
45
+ const sqlTypes = options?.sqlTypes ?? ['sql'];
46
+ for (const otherId in cells) {
47
+ if (otherId === targetCell.id)
48
+ continue;
49
+ const other = cells[otherId];
50
+ if (!other)
51
+ continue;
52
+ if (inputTypes.includes(other.type)) {
53
+ const vn = getInputVarName(other);
54
+ if (!vn)
55
+ continue;
56
+ if (sql.includes(`{{${vn}}}`) || sql.includes(`:${vn}`)) {
57
+ deps.push(other.id);
58
+ }
59
+ }
60
+ else if (sqlTypes.includes(other.type)) {
61
+ const resultName = getSqlResultName(other.id);
62
+ const title = typeof other.data?.title === 'string'
63
+ ? other.data?.title
64
+ : undefined;
65
+ // Match on title (case-insensitive) with identifier boundaries
66
+ const titleMatch = title && containsIdentifier(sqlLower, title.toLowerCase());
67
+ // Match on effective result name (case-insensitive)
68
+ const resultMatch = resultName && containsIdentifier(sqlLower, resultName.toLowerCase());
69
+ if (titleMatch || resultMatch)
70
+ deps.push(other.id);
71
+ }
72
+ }
73
+ return Array.from(new Set(deps));
74
+ }
75
+ function escapeRegExp(input) {
76
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
77
+ }
78
+ function containsIdentifier(sqlLower, candidateLower) {
79
+ if (!candidateLower.trim())
80
+ return false;
81
+ const pattern = new RegExp(`(?<![a-zA-Z0-9_.])${escapeRegExp(candidateLower)}(?![a-zA-Z0-9_])`, 'i');
82
+ return pattern.test(sqlLower);
83
+ }
84
+ export async function runSqlWithCallbacks(run, callbacks = {}) {
85
+ callbacks.onStart?.();
86
+ try {
87
+ const res = await run();
88
+ callbacks.onSuccess?.(res);
89
+ }
90
+ catch (e) {
91
+ const message = e instanceof Error ? e.message : String(e);
92
+ callbacks.onError?.(message);
93
+ }
94
+ finally {
95
+ callbacks.onFinally?.();
96
+ }
97
+ }
98
+ /**
99
+ * Recursively extract table names from a parsed SQL AST.
100
+ */
101
+ function extractTablesFromAst(statements) {
102
+ const tables = new Set();
103
+ const walk = (node) => {
104
+ if (!node || typeof node !== 'object')
105
+ return;
106
+ const obj = node;
107
+ // DuckDB AST uses table_name for table references
108
+ if (typeof obj.table_name === 'string') {
109
+ tables.add(obj.table_name.toLowerCase());
110
+ }
111
+ for (const value of Object.values(obj)) {
112
+ if (Array.isArray(value)) {
113
+ value.forEach(walk);
114
+ }
115
+ else if (value && typeof value === 'object') {
116
+ walk(value);
117
+ }
118
+ }
119
+ };
120
+ for (const stmt of statements) {
121
+ const stmtObj = stmt;
122
+ if (stmtObj.node) {
123
+ walk(stmtObj.node);
124
+ }
125
+ else {
126
+ walk(stmt);
127
+ }
128
+ }
129
+ return tables;
130
+ }
131
+ /**
132
+ * Find SQL cell dependencies using AST-based parsing.
133
+ * This provides more accurate detection than text-based matching.
134
+ */
135
+ export async function findSqlDependenciesFromAst(opts) {
136
+ const { sql, cells, sqlSelectToJson } = opts;
137
+ try {
138
+ const parsed = await sqlSelectToJson(sql);
139
+ if (parsed.error || !parsed.statements) {
140
+ return []; // Fall back to empty deps on parse error
141
+ }
142
+ const referencedTables = extractTablesFromAst(parsed.statements);
143
+ const deps = [];
144
+ for (const [id, cell] of Object.entries(cells)) {
145
+ if (cell.type === 'sql') {
146
+ const cellData = cell.data;
147
+ const title = cellData.title?.toLowerCase();
148
+ const effectiveName = getEffectiveResultName(cellData, convertToValidColumnOrTableName).toLowerCase();
149
+ // Match on either title or effective result name
150
+ if ((title && referencedTables.has(title)) ||
151
+ referencedTables.has(effectiveName)) {
152
+ deps.push(id);
153
+ }
154
+ }
155
+ }
156
+ return deps;
157
+ }
158
+ catch {
159
+ return [];
160
+ }
161
+ }
162
+ /**
163
+ * Rewrites unqualified references to known sheet-local result names as
164
+ * schema-qualified references. This keeps SQL ergonomic without mutable
165
+ * connector-level current schema state.
166
+ */
167
+ export function qualifySheetLocalResultNames(opts) {
168
+ const { sql, sheetSchema, sheetDatabase, sheetCellIds, cells, getSqlResultName, } = opts;
169
+ let rewritten = sql;
170
+ const names = Array.from(new Set(sheetCellIds
171
+ .map((cellId) => {
172
+ const cell = cells[cellId];
173
+ if (!cell || cell.type !== 'sql')
174
+ return undefined;
175
+ const resultName = getSqlResultName(cellId);
176
+ if (!resultName || resultName.includes('.'))
177
+ return undefined;
178
+ return resultName;
179
+ })
180
+ .filter((v) => Boolean(v)))).sort((a, b) => b.length - a.length);
181
+ for (const name of names) {
182
+ const qualified = sheetDatabase
183
+ ? `${sheetDatabase}.${sheetSchema}.${name}`
184
+ : `${sheetSchema}.${name}`;
185
+ const pattern = new RegExp(`(?<![a-zA-Z0-9_.])${escapeRegExp(name)}(?![a-zA-Z0-9_])`, 'g');
186
+ rewritten = rewritten.replace(pattern, qualified);
187
+ }
188
+ return rewritten;
189
+ }
190
+ //# sourceMappingURL=sqlHelpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlHelpers.js","sourceRoot":"","sources":["../src/sqlHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,+BAA+B,EAAC,MAAM,iBAAiB,CAAC;AAUhE,OAAO,EAAC,WAAW,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAC,sBAAsB,EAAC,MAAM,SAAS,CAAC;AAE/C,MAAM,UAAU,kBAAkB,CAChC,MAAc,EACd,GAAW,EACX,QAA8B;IAE9B,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC;IAE3B,MAAM,IAAI,GAAG,mBAAmB,CAAO;QACrC,UAAU;QACV,KAAK,EAAE,QAAQ;QACf,UAAU,EAAE,GAAG,EAAE,CAAC,GAAG;QACrB,eAAe,EAAE,CAAC,IAAI,EAAE,EAAE,CACxB,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;QACzD,gBAAgB,EAAE,CAAC,GAAG,EAAE,EAAE;YACxB,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,IAAI,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC5B,OAAO,sBAAsB,CAC3B,IAAI,CAAC,IAAmB,EACxB,+BAA+B,CAChC,CAAC;YACJ,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC,CAAC;IAEH,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC7B,EAAE,EAAE,GAAG,QAAQ,IAAI,MAAM,EAAE;QAC3B,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,MAAM;KACf,CAAC,CAAC,CAAC;AACN,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,MAAc,EACd,MAAwB;IAExB,MAAM,MAAM,GAAoC,EAAE,CAAC;IACnD,KAAK,MAAM,EAAC,OAAO,EAAE,KAAK,EAAC,IAAI,MAAM,EAAE,CAAC;QACtC,MAAM,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,OAAO,CAC3B,yCAAyC,EACzC,CAAC,EAAU,EAAE,CAAS,EAAE,EAAE;QACxB,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACtB,OAAO,OAAO,GAAG,KAAK,QAAQ;YAC5B,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;YACb,CAAC,CAAC,IAAI,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;IACpD,CAAC,CACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB,CAEjC,IAOD;IACC,MAAM,EACJ,UAAU,EACV,KAAK,EACL,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,OAAO,GACR,GAAG,IAAI,CAAC;IACT,MAAM,GAAG,GAAG,UAAU,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;IACzC,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IACnC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,CAAC,OAAO,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,CAAC,KAAK,CAAC,CAAC;IAE9C,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;QAC5B,IAAI,OAAO,KAAK,UAAU,CAAC,EAAE;YAAE,SAAS;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACpC,MAAM,EAAE,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;YAClC,IAAI,CAAC,EAAE;gBAAE,SAAS;YAClB,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;gBACxD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;aAAM,IAAI,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACzC,MAAM,UAAU,GAAG,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC9C,MAAM,KAAK,GACT,OAAQ,KAAoC,CAAC,IAAI,EAAE,KAAK,KAAK,QAAQ;gBACnE,CAAC,CAAG,KAAmC,CAAC,IAAI,EAAE,KAAgB;gBAC9D,CAAC,CAAC,SAAS,CAAC;YAChB,+DAA+D;YAC/D,MAAM,UAAU,GACd,KAAK,IAAI,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;YAC7D,oDAAoD;YACpD,MAAM,WAAW,GACf,UAAU,IAAI,kBAAkB,CAAC,QAAQ,EAAE,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;YACvE,IAAI,UAAU,IAAI,WAAW;gBAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,kBAAkB,CAAC,QAAgB,EAAE,cAAsB;IAClE,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE;QAAE,OAAO,KAAK,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,MAAM,CACxB,qBAAqB,YAAY,CAAC,cAAc,CAAC,kBAAkB,EACnE,GAAG,CACJ,CAAC;IACF,OAAO,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,GAAgC,EAChC,YAA6B,EAAE;IAE/B,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC;IACtB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,GAAG,EAAE,CAAC;QACxB,SAAS,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC3D,SAAS,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;YAAS,CAAC;QACT,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC;IAC1B,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,oBAAoB,CAAC,UAAqB;IACjD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IAEjC,MAAM,IAAI,GAAG,CAAC,IAAa,EAAQ,EAAE;QACnC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO;QAC9C,MAAM,GAAG,GAAG,IAA+B,CAAC;QAE5C,kDAAkD;QAClD,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;QAC3C,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YACvC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;iBAAM,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC9C,IAAI,CAAC,KAAK,CAAC,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,IAA+B,CAAC;QAChD,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,IAOhD;IACC,MAAM,EAAC,GAAG,EAAE,KAAK,EAAE,eAAe,EAAC,GAAG,IAAI,CAAC;IAE3C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAI,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACvC,OAAO,EAAE,CAAC,CAAC,yCAAyC;QACtD,CAAC;QAED,MAAM,gBAAgB,GAAG,oBAAoB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACjE,MAAM,IAAI,GAAa,EAAE,CAAC;QAE1B,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/C,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;gBACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAmB,CAAC;gBAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC;gBAC5C,MAAM,aAAa,GAAG,sBAAsB,CAC1C,QAAQ,EACR,+BAA+B,CAChC,CAAC,WAAW,EAAE,CAAC;gBAChB,iDAAiD;gBACjD,IACE,CAAC,KAAK,IAAI,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;oBACtC,gBAAgB,CAAC,GAAG,CAAC,aAAa,CAAC,EACnC,CAAC;oBACD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAChB,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAAC,IAO5C;IACC,MAAM,EACJ,GAAG,EACH,WAAW,EACX,aAAa,EACb,YAAY,EACZ,KAAK,EACL,gBAAgB,GACjB,GAAG,IAAI,CAAC;IACT,IAAI,SAAS,GAAG,GAAG,CAAC;IAEpB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CACtB,IAAI,GAAG,CACL,YAAY;SACT,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QACd,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;YAAE,OAAO,SAAS,CAAC;QACnD,MAAM,UAAU,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;QAC9D,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAC1C,CACF,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IAEtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,aAAa;YAC7B,CAAC,CAAC,GAAG,aAAa,IAAI,WAAW,IAAI,IAAI,EAAE;YAC3C,CAAC,CAAC,GAAG,WAAW,IAAI,IAAI,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,IAAI,MAAM,CACxB,qBAAqB,YAAY,CAAC,IAAI,CAAC,kBAAkB,EACzD,GAAG,CACJ,CAAC;QACF,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACpD,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC","sourcesContent":["import {convertToValidColumnOrTableName} from '@sqlrooms/utils';\nimport type {\n Cell,\n Edge,\n SqlCellData,\n SqlDependencyOptions,\n SqlRenderInput,\n SqlRunCallbacks,\n SqlRunResult,\n} from './types';\nimport {isInputCell, isSqlCell} from './types';\nimport {getEffectiveResultName} from './utils';\n\nexport function deriveEdgesFromSql(\n cellId: string,\n sql: string,\n allCells: Record<string, Cell>,\n): Edge[] {\n const targetCell = allCells[cellId];\n if (!targetCell) return [];\n\n const deps = findSqlDependencies<Cell>({\n targetCell,\n cells: allCells,\n getSqlText: () => sql,\n getInputVarName: (cell) =>\n isInputCell(cell) ? cell.data.input.varName : undefined,\n getSqlResultName: (cid) => {\n const cell = allCells[cid];\n if (cell && isSqlCell(cell)) {\n return getEffectiveResultName(\n cell.data as SqlCellData,\n convertToValidColumnOrTableName,\n );\n }\n return undefined;\n },\n });\n\n return deps.map((sourceId) => ({\n id: `${sourceId}-${cellId}`,\n source: sourceId,\n target: cellId,\n }));\n}\n\nexport function renderSqlWithInputs(\n rawSql: string,\n inputs: SqlRenderInput[],\n): string {\n const varMap: Record<string, string | number> = {};\n for (const {varName, value} of inputs) {\n varMap[varName] = value;\n }\n return (rawSql || '').replace(\n /\\{\\{\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\}\\}/g,\n (_m: string, v: string) => {\n const val = varMap[v];\n return typeof val === 'number'\n ? String(val)\n : `'${String(val ?? '')?.replace(/'/g, \"''\")}'`;\n },\n );\n}\n\nexport function findSqlDependencies<\n TCell extends {id: string; type: string},\n>(opts: {\n targetCell: TCell;\n cells: Record<string, TCell>;\n getSqlText: (cell: TCell) => string | undefined;\n getInputVarName: (cell: TCell) => string | undefined;\n getSqlResultName: (cellId: string) => string | undefined;\n options?: SqlDependencyOptions;\n}): string[] {\n const {\n targetCell,\n cells,\n getSqlText,\n getInputVarName,\n getSqlResultName,\n options,\n } = opts;\n const sql = getSqlText(targetCell) || '';\n const sqlLower = sql.toLowerCase();\n const deps: string[] = [];\n const inputTypes = options?.inputTypes ?? ['input'];\n const sqlTypes = options?.sqlTypes ?? ['sql'];\n\n for (const otherId in cells) {\n if (otherId === targetCell.id) continue;\n const other = cells[otherId];\n if (!other) continue;\n if (inputTypes.includes(other.type)) {\n const vn = getInputVarName(other);\n if (!vn) continue;\n if (sql.includes(`{{${vn}}}`) || sql.includes(`:${vn}`)) {\n deps.push(other.id);\n }\n } else if (sqlTypes.includes(other.type)) {\n const resultName = getSqlResultName(other.id);\n const title =\n typeof (other as {data?: {title?: unknown}}).data?.title === 'string'\n ? ((other as {data?: {title?: string}}).data?.title as string)\n : undefined;\n // Match on title (case-insensitive) with identifier boundaries\n const titleMatch =\n title && containsIdentifier(sqlLower, title.toLowerCase());\n // Match on effective result name (case-insensitive)\n const resultMatch =\n resultName && containsIdentifier(sqlLower, resultName.toLowerCase());\n if (titleMatch || resultMatch) deps.push(other.id);\n }\n }\n return Array.from(new Set(deps));\n}\n\nfunction escapeRegExp(input: string): string {\n return input.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction containsIdentifier(sqlLower: string, candidateLower: string): boolean {\n if (!candidateLower.trim()) return false;\n const pattern = new RegExp(\n `(?<![a-zA-Z0-9_.])${escapeRegExp(candidateLower)}(?![a-zA-Z0-9_])`,\n 'i',\n );\n return pattern.test(sqlLower);\n}\n\nexport async function runSqlWithCallbacks(\n run: () => Promise<SqlRunResult>,\n callbacks: SqlRunCallbacks = {},\n) {\n callbacks.onStart?.();\n try {\n const res = await run();\n callbacks.onSuccess?.(res);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n callbacks.onError?.(message);\n } finally {\n callbacks.onFinally?.();\n }\n}\n\n/**\n * Recursively extract table names from a parsed SQL AST.\n */\nfunction extractTablesFromAst(statements: unknown[]): Set<string> {\n const tables = new Set<string>();\n\n const walk = (node: unknown): void => {\n if (!node || typeof node !== 'object') return;\n const obj = node as Record<string, unknown>;\n\n // DuckDB AST uses table_name for table references\n if (typeof obj.table_name === 'string') {\n tables.add(obj.table_name.toLowerCase());\n }\n\n for (const value of Object.values(obj)) {\n if (Array.isArray(value)) {\n value.forEach(walk);\n } else if (value && typeof value === 'object') {\n walk(value);\n }\n }\n };\n\n for (const stmt of statements) {\n const stmtObj = stmt as Record<string, unknown>;\n if (stmtObj.node) {\n walk(stmtObj.node);\n } else {\n walk(stmt);\n }\n }\n\n return tables;\n}\n\n/**\n * Find SQL cell dependencies using AST-based parsing.\n * This provides more accurate detection than text-based matching.\n */\nexport async function findSqlDependenciesFromAst(opts: {\n sql: string;\n cells: Record<string, Cell>;\n sqlSelectToJson: (sql: string) => Promise<{\n error: boolean;\n statements?: unknown[];\n }>;\n}): Promise<string[]> {\n const {sql, cells, sqlSelectToJson} = opts;\n\n try {\n const parsed = await sqlSelectToJson(sql);\n if (parsed.error || !parsed.statements) {\n return []; // Fall back to empty deps on parse error\n }\n\n const referencedTables = extractTablesFromAst(parsed.statements);\n const deps: string[] = [];\n\n for (const [id, cell] of Object.entries(cells)) {\n if (cell.type === 'sql') {\n const cellData = cell.data as SqlCellData;\n const title = cellData.title?.toLowerCase();\n const effectiveName = getEffectiveResultName(\n cellData,\n convertToValidColumnOrTableName,\n ).toLowerCase();\n // Match on either title or effective result name\n if (\n (title && referencedTables.has(title)) ||\n referencedTables.has(effectiveName)\n ) {\n deps.push(id);\n }\n }\n }\n\n return deps;\n } catch {\n return [];\n }\n}\n\n/**\n * Rewrites unqualified references to known sheet-local result names as\n * schema-qualified references. This keeps SQL ergonomic without mutable\n * connector-level current schema state.\n */\nexport function qualifySheetLocalResultNames(opts: {\n sql: string;\n sheetSchema: string;\n sheetDatabase?: string;\n sheetCellIds: string[];\n cells: Record<string, Cell>;\n getSqlResultName: (cellId: string) => string | undefined;\n}): string {\n const {\n sql,\n sheetSchema,\n sheetDatabase,\n sheetCellIds,\n cells,\n getSqlResultName,\n } = opts;\n let rewritten = sql;\n\n const names = Array.from(\n new Set(\n sheetCellIds\n .map((cellId) => {\n const cell = cells[cellId];\n if (!cell || cell.type !== 'sql') return undefined;\n const resultName = getSqlResultName(cellId);\n if (!resultName || resultName.includes('.')) return undefined;\n return resultName;\n })\n .filter((v): v is string => Boolean(v)),\n ),\n ).sort((a, b) => b.length - a.length);\n\n for (const name of names) {\n const qualified = sheetDatabase\n ? `${sheetDatabase}.${sheetSchema}.${name}`\n : `${sheetSchema}.${name}`;\n const pattern = new RegExp(\n `(?<![a-zA-Z0-9_.])${escapeRegExp(name)}(?![a-zA-Z0-9_])`,\n 'g',\n );\n rewritten = rewritten.replace(pattern, qualified);\n }\n\n return rewritten;\n}\n"]}