@deepagents/text2sql 6.1.0 → 7.0.0
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/README.md +16 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +2 -2
- package/dist/lib/adapters/adapter.d.ts +1 -0
- package/dist/lib/adapters/adapter.d.ts.map +1 -1
- package/dist/lib/adapters/bigquery/index.js +91 -8
- package/dist/lib/adapters/bigquery/index.js.map +3 -3
- package/dist/lib/adapters/clickhouse/index.js +5 -2
- package/dist/lib/adapters/clickhouse/index.js.map +2 -2
- package/dist/lib/adapters/duckdb/duckdb-identifiers.d.ts +8 -0
- package/dist/lib/adapters/duckdb/duckdb-identifiers.d.ts.map +1 -0
- package/dist/lib/adapters/duckdb/duckdb.d.ts +40 -0
- package/dist/lib/adapters/duckdb/duckdb.d.ts.map +1 -0
- package/dist/lib/adapters/duckdb/duckdb.groundings.d.ts +58 -0
- package/dist/lib/adapters/duckdb/duckdb.groundings.d.ts.map +1 -0
- package/dist/lib/adapters/duckdb/duckdb.sql-policy.d.ts +8 -0
- package/dist/lib/adapters/duckdb/duckdb.sql-policy.d.ts.map +1 -0
- package/dist/lib/adapters/duckdb/index.d.ts +34 -0
- package/dist/lib/adapters/duckdb/index.d.ts.map +1 -0
- package/dist/lib/adapters/duckdb/index.js +1933 -0
- package/dist/lib/adapters/duckdb/index.js.map +7 -0
- package/dist/lib/adapters/mysql/index.js +91 -8
- package/dist/lib/adapters/mysql/index.js.map +3 -3
- package/dist/lib/adapters/parser-sql-policy.d.ts.map +1 -1
- package/dist/lib/adapters/postgres/index.js +91 -8
- package/dist/lib/adapters/postgres/index.js.map +3 -3
- package/dist/lib/adapters/postgres/postgres-parser-workaround.d.ts +8 -0
- package/dist/lib/adapters/postgres/postgres-parser-workaround.d.ts.map +1 -0
- package/dist/lib/adapters/posthog/index.js +18 -4
- package/dist/lib/adapters/posthog/index.js.map +2 -2
- package/dist/lib/adapters/posthog/posthog.d.ts +2 -1
- package/dist/lib/adapters/posthog/posthog.d.ts.map +1 -1
- package/dist/lib/adapters/posthog/types.d.ts +2 -0
- package/dist/lib/adapters/posthog/types.d.ts.map +1 -1
- package/dist/lib/adapters/spreadsheet/index.js +91 -8
- package/dist/lib/adapters/spreadsheet/index.js.map +3 -3
- package/dist/lib/adapters/sqlite/index.js +91 -8
- package/dist/lib/adapters/sqlite/index.js.map +3 -3
- package/dist/lib/adapters/sqlserver/index.js +91 -8
- package/dist/lib/adapters/sqlserver/index.js.map +3 -3
- package/dist/lib/agents/question.agent.d.ts.map +1 -1
- package/dist/lib/synthesis/extractors/base-contextual-extractor.d.ts.map +1 -1
- package/dist/lib/synthesis/index.js.map +2 -2
- package/package.json +18 -19
|
@@ -0,0 +1,1933 @@
|
|
|
1
|
+
// packages/text2sql/src/lib/adapters/groundings/abstract.grounding.ts
|
|
2
|
+
function filterColumns(columns, filter) {
|
|
3
|
+
if (Array.isArray(filter)) {
|
|
4
|
+
return columns.filter((col) => filter.includes(col.name));
|
|
5
|
+
}
|
|
6
|
+
if (filter instanceof RegExp) {
|
|
7
|
+
return columns.filter((col) => filter.test(col.name));
|
|
8
|
+
}
|
|
9
|
+
return columns.filter((col) => filter(col.name));
|
|
10
|
+
}
|
|
11
|
+
function applyColumnFilter(entity, columnsConfig) {
|
|
12
|
+
if (!columnsConfig) return entity;
|
|
13
|
+
const filter = columnsConfig[entity.name];
|
|
14
|
+
if (!filter) return entity;
|
|
15
|
+
return { ...entity, columns: filterColumns(entity.columns, filter) };
|
|
16
|
+
}
|
|
17
|
+
var AbstractGrounding = class {
|
|
18
|
+
/**
|
|
19
|
+
* Grounding identifier for debugging/logging.
|
|
20
|
+
*/
|
|
21
|
+
name;
|
|
22
|
+
phase;
|
|
23
|
+
constructor(name, phase) {
|
|
24
|
+
this.name = name;
|
|
25
|
+
this.phase = phase;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Populate ctx with the tables/views this grounding makes available for scope
|
|
29
|
+
* resolution. Annotators decorate entities other groundings produced and add
|
|
30
|
+
* no membership, so the default is a no-op; entity-producing groundings
|
|
31
|
+
* override it. This lets scope resolution skip the annotators' metadata scans.
|
|
32
|
+
*
|
|
33
|
+
* @param _ctx - Shared context for accumulating entity membership
|
|
34
|
+
*/
|
|
35
|
+
async contributeEntities(_ctx) {
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// packages/text2sql/src/lib/adapters/groundings/column-stats.grounding.ts
|
|
40
|
+
var ColumnStatsGrounding = class extends AbstractGrounding {
|
|
41
|
+
constructor(config = {}) {
|
|
42
|
+
super("columnStats", "column_stats");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Execute the grounding process.
|
|
46
|
+
* Annotates columns in ctx.tables and ctx.views with statistics.
|
|
47
|
+
*/
|
|
48
|
+
async execute(ctx) {
|
|
49
|
+
const allContainers = [...ctx.tables, ...ctx.views];
|
|
50
|
+
const total = allContainers.length;
|
|
51
|
+
for (let i = 0; i < allContainers.length; i++) {
|
|
52
|
+
const container = allContainers[i];
|
|
53
|
+
ctx.onProgress({
|
|
54
|
+
type: "phase:progress",
|
|
55
|
+
phase: "column_stats",
|
|
56
|
+
table: container.name,
|
|
57
|
+
message: `Collecting stats for ${container.name}...`,
|
|
58
|
+
current: i + 1,
|
|
59
|
+
total
|
|
60
|
+
});
|
|
61
|
+
for (const column2 of container.columns) {
|
|
62
|
+
try {
|
|
63
|
+
const stats = await this.collectStats(container.name, column2);
|
|
64
|
+
if (stats) {
|
|
65
|
+
column2.stats = stats;
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.warn(
|
|
69
|
+
"Error collecting stats for",
|
|
70
|
+
container.name,
|
|
71
|
+
column2.name,
|
|
72
|
+
error
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// packages/text2sql/src/lib/adapters/groundings/column-values.grounding.ts
|
|
81
|
+
var ColumnValuesGrounding = class extends AbstractGrounding {
|
|
82
|
+
lowCardinalityLimit;
|
|
83
|
+
maxValueLength;
|
|
84
|
+
constructor(config = {}) {
|
|
85
|
+
super("columnValues", "column_values");
|
|
86
|
+
this.lowCardinalityLimit = config.lowCardinalityLimit ?? 20;
|
|
87
|
+
this.maxValueLength = config.maxValueLength ?? 100;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get values for native ENUM type columns.
|
|
91
|
+
* Return undefined if column is not an ENUM type.
|
|
92
|
+
* Default implementation returns undefined (no native ENUM support).
|
|
93
|
+
*/
|
|
94
|
+
async collectEnumValues(_tableName, _column) {
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse CHECK constraint for enum-like IN clause.
|
|
99
|
+
* Extracts values from patterns like:
|
|
100
|
+
* - CHECK (status IN ('active', 'inactive'))
|
|
101
|
+
* - CHECK ((status)::text = ANY (ARRAY['a'::text, 'b'::text]))
|
|
102
|
+
* - CHECK (status = 'active' OR status = 'inactive')
|
|
103
|
+
*/
|
|
104
|
+
parseCheckConstraint(constraint2, columnName) {
|
|
105
|
+
if (constraint2.type !== "CHECK" || !constraint2.definition) {
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
if (constraint2.columns && !constraint2.columns.includes(columnName)) {
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
const def = constraint2.definition;
|
|
112
|
+
const escapedCol = this.escapeRegex(columnName);
|
|
113
|
+
const colPattern = `(?:\\(?\\(?${escapedCol}\\)?(?:::(?:text|varchar|character varying))?\\)?)`;
|
|
114
|
+
const inMatch = def.match(
|
|
115
|
+
new RegExp(`${colPattern}\\s+IN\\s*\\(([^)]+)\\)`, "i")
|
|
116
|
+
);
|
|
117
|
+
if (inMatch) {
|
|
118
|
+
return this.extractStringValues(inMatch[1]);
|
|
119
|
+
}
|
|
120
|
+
const anyMatch = def.match(
|
|
121
|
+
new RegExp(
|
|
122
|
+
`${colPattern}\\s*=\\s*ANY\\s*\\(\\s*(?:ARRAY)?\\s*\\[([^\\]]+)\\]`,
|
|
123
|
+
"i"
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
if (anyMatch) {
|
|
127
|
+
return this.extractStringValues(anyMatch[1]);
|
|
128
|
+
}
|
|
129
|
+
const orPattern = new RegExp(
|
|
130
|
+
`\\b${this.escapeRegex(columnName)}\\b\\s*=\\s*'([^']*)'`,
|
|
131
|
+
"gi"
|
|
132
|
+
);
|
|
133
|
+
const orMatches = [...def.matchAll(orPattern)];
|
|
134
|
+
if (orMatches.length >= 2) {
|
|
135
|
+
return orMatches.map((m) => m[1]);
|
|
136
|
+
}
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Extract string values from a comma-separated list.
|
|
141
|
+
*/
|
|
142
|
+
extractStringValues(input) {
|
|
143
|
+
const values = [];
|
|
144
|
+
const matches = input.matchAll(/'([^']*)'/g);
|
|
145
|
+
for (const match of matches) {
|
|
146
|
+
values.push(match[1]);
|
|
147
|
+
}
|
|
148
|
+
return values.length > 0 ? values : void 0;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Escape special regex characters in a string.
|
|
152
|
+
*/
|
|
153
|
+
escapeRegex(str) {
|
|
154
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get the table from context by name.
|
|
158
|
+
*/
|
|
159
|
+
getTable(ctx, name) {
|
|
160
|
+
return ctx.tables.find((t) => t.name === name);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Execute the grounding process.
|
|
164
|
+
* Annotates columns in ctx.tables and ctx.views with values.
|
|
165
|
+
*/
|
|
166
|
+
async execute(ctx) {
|
|
167
|
+
const allContainers = [...ctx.tables, ...ctx.views];
|
|
168
|
+
const total = allContainers.length;
|
|
169
|
+
for (let i = 0; i < allContainers.length; i++) {
|
|
170
|
+
const container = allContainers[i];
|
|
171
|
+
ctx.onProgress({
|
|
172
|
+
type: "phase:progress",
|
|
173
|
+
phase: "column_values",
|
|
174
|
+
table: container.name,
|
|
175
|
+
message: `Collecting column values for ${container.name}...`,
|
|
176
|
+
current: i + 1,
|
|
177
|
+
total
|
|
178
|
+
});
|
|
179
|
+
const table2 = this.getTable(ctx, container.name);
|
|
180
|
+
for (const column2 of container.columns) {
|
|
181
|
+
try {
|
|
182
|
+
const result = await this.resolveColumnValues(
|
|
183
|
+
container.name,
|
|
184
|
+
column2,
|
|
185
|
+
table2?.constraints
|
|
186
|
+
);
|
|
187
|
+
if (result) {
|
|
188
|
+
column2.kind = result.kind;
|
|
189
|
+
column2.values = result.values;
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.warn(
|
|
193
|
+
"Error collecting column values for",
|
|
194
|
+
container.name,
|
|
195
|
+
column2.name,
|
|
196
|
+
error
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
exceedsMaxValueLength(values) {
|
|
203
|
+
return values.some((v) => v.length > this.maxValueLength);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Resolve column values from all sources in priority order.
|
|
207
|
+
*/
|
|
208
|
+
async resolveColumnValues(tableName, column2, constraints2) {
|
|
209
|
+
const enumValues = await this.collectEnumValues(tableName, column2);
|
|
210
|
+
if (enumValues?.length && !this.exceedsMaxValueLength(enumValues)) {
|
|
211
|
+
return { kind: "Enum", values: enumValues };
|
|
212
|
+
}
|
|
213
|
+
if (constraints2) {
|
|
214
|
+
for (const constraint2 of constraints2) {
|
|
215
|
+
const checkValues = this.parseCheckConstraint(constraint2, column2.name);
|
|
216
|
+
if (checkValues?.length && !this.exceedsMaxValueLength(checkValues)) {
|
|
217
|
+
return { kind: "Enum", values: checkValues };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const lowCardValues = await this.collectLowCardinality(tableName, column2);
|
|
222
|
+
if (lowCardValues?.length && !this.exceedsMaxValueLength(lowCardValues)) {
|
|
223
|
+
return { kind: "LowCardinality", values: lowCardValues };
|
|
224
|
+
}
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// packages/text2sql/src/lib/adapters/groundings/constraint.grounding.ts
|
|
230
|
+
var ConstraintGrounding = class extends AbstractGrounding {
|
|
231
|
+
constructor(config = {}) {
|
|
232
|
+
super("constraint", "constraints");
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Execute the grounding process.
|
|
236
|
+
* Annotates tables in ctx.tables with their constraints.
|
|
237
|
+
*/
|
|
238
|
+
async execute(ctx) {
|
|
239
|
+
const total = ctx.tables.length;
|
|
240
|
+
for (let i = 0; i < ctx.tables.length; i++) {
|
|
241
|
+
const table2 = ctx.tables[i];
|
|
242
|
+
ctx.onProgress({
|
|
243
|
+
type: "phase:progress",
|
|
244
|
+
phase: "constraints",
|
|
245
|
+
table: table2.name,
|
|
246
|
+
message: `Loading constraints for ${table2.name}...`,
|
|
247
|
+
current: i + 1,
|
|
248
|
+
total
|
|
249
|
+
});
|
|
250
|
+
try {
|
|
251
|
+
table2.constraints = await this.getConstraints(table2.name);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.warn("Error collecting constraints for", table2.name, error);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// packages/text2sql/src/lib/adapters/groundings/indexes.grounding.ts
|
|
260
|
+
var IndexesGrounding = class extends AbstractGrounding {
|
|
261
|
+
constructor(config = {}) {
|
|
262
|
+
super("index", "indexes");
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Execute the grounding process.
|
|
266
|
+
* Annotates tables in ctx.tables with their indexes and marks indexed columns.
|
|
267
|
+
*/
|
|
268
|
+
async execute(ctx) {
|
|
269
|
+
const total = ctx.tables.length;
|
|
270
|
+
for (let i = 0; i < ctx.tables.length; i++) {
|
|
271
|
+
const table2 = ctx.tables[i];
|
|
272
|
+
ctx.onProgress({
|
|
273
|
+
type: "phase:progress",
|
|
274
|
+
phase: "indexes",
|
|
275
|
+
table: table2.name,
|
|
276
|
+
message: `Loading indexes for ${table2.name}...`,
|
|
277
|
+
current: i + 1,
|
|
278
|
+
total
|
|
279
|
+
});
|
|
280
|
+
table2.indexes = await this.getIndexes(table2.name);
|
|
281
|
+
for (const index2 of table2.indexes ?? []) {
|
|
282
|
+
for (const colName of index2.columns) {
|
|
283
|
+
const column2 = table2.columns.find((c) => c.name === colName);
|
|
284
|
+
if (column2) {
|
|
285
|
+
column2.isIndexed = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// packages/text2sql/src/lib/adapters/groundings/info.grounding.ts
|
|
294
|
+
var InfoGrounding = class extends AbstractGrounding {
|
|
295
|
+
constructor(config = {}) {
|
|
296
|
+
super("dialectInfo", "info");
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Execute the grounding process.
|
|
300
|
+
* Writes database info to ctx.info.
|
|
301
|
+
*/
|
|
302
|
+
async execute(ctx) {
|
|
303
|
+
ctx.info = await this.collectInfo();
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// packages/text2sql/src/lib/adapters/groundings/row-count.grounding.ts
|
|
308
|
+
var RowCountGrounding = class extends AbstractGrounding {
|
|
309
|
+
constructor(config = {}) {
|
|
310
|
+
super("rowCount", "row_counts");
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Execute the grounding process.
|
|
314
|
+
* Annotates tables in ctx.tables with row counts and size hints.
|
|
315
|
+
*/
|
|
316
|
+
async execute(ctx) {
|
|
317
|
+
const total = ctx.tables.length;
|
|
318
|
+
for (let i = 0; i < ctx.tables.length; i++) {
|
|
319
|
+
const table2 = ctx.tables[i];
|
|
320
|
+
ctx.onProgress({
|
|
321
|
+
type: "phase:progress",
|
|
322
|
+
phase: "row_counts",
|
|
323
|
+
table: table2.name,
|
|
324
|
+
message: `Counting rows in ${table2.name}...`,
|
|
325
|
+
current: i + 1,
|
|
326
|
+
total
|
|
327
|
+
});
|
|
328
|
+
const count = await this.getRowCount(table2.name);
|
|
329
|
+
if (count != null) {
|
|
330
|
+
table2.rowCount = count;
|
|
331
|
+
table2.sizeHint = this.#classifyRowCount(count);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Classify row count into a size hint category.
|
|
337
|
+
*/
|
|
338
|
+
#classifyRowCount(count) {
|
|
339
|
+
if (count < 100) return "tiny";
|
|
340
|
+
if (count < 1e3) return "small";
|
|
341
|
+
if (count < 1e4) return "medium";
|
|
342
|
+
if (count < 1e5) return "large";
|
|
343
|
+
return "huge";
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// packages/text2sql/src/lib/adapters/groundings/table.grounding.ts
|
|
348
|
+
var TableGrounding = class extends AbstractGrounding {
|
|
349
|
+
#filter;
|
|
350
|
+
#columns;
|
|
351
|
+
#forward;
|
|
352
|
+
#backward;
|
|
353
|
+
constructor(config = {}) {
|
|
354
|
+
super("table", "tables");
|
|
355
|
+
this.#filter = config.filter;
|
|
356
|
+
this.#columns = config.columns;
|
|
357
|
+
this.#forward = config.forward;
|
|
358
|
+
this.#backward = config.backward;
|
|
359
|
+
}
|
|
360
|
+
async contributeEntities(ctx) {
|
|
361
|
+
await this.execute(ctx);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Execute the grounding process.
|
|
365
|
+
* Writes discovered tables and relationships to the context.
|
|
366
|
+
*/
|
|
367
|
+
async execute(ctx) {
|
|
368
|
+
const seedTables = await this.applyFilter();
|
|
369
|
+
const forward = this.#forward;
|
|
370
|
+
const backward = this.#backward;
|
|
371
|
+
if (!forward && !backward) {
|
|
372
|
+
const tables3 = await Promise.all(
|
|
373
|
+
seedTables.map(async (name, index2) => {
|
|
374
|
+
ctx.onProgress({
|
|
375
|
+
type: "phase:progress",
|
|
376
|
+
phase: "tables",
|
|
377
|
+
table: name,
|
|
378
|
+
message: `Loading table ${name}...`,
|
|
379
|
+
current: index2 + 1,
|
|
380
|
+
total: seedTables.length
|
|
381
|
+
});
|
|
382
|
+
return applyColumnFilter(await this.getTable(name), this.#columns);
|
|
383
|
+
})
|
|
384
|
+
);
|
|
385
|
+
ctx.tables.push(...tables3);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const tables2 = {};
|
|
389
|
+
const allRelationships = [];
|
|
390
|
+
const seenRelationships = /* @__PURE__ */ new Set();
|
|
391
|
+
const forwardQueue = [];
|
|
392
|
+
const backwardQueue = [];
|
|
393
|
+
const forwardVisited = /* @__PURE__ */ new Set();
|
|
394
|
+
const backwardVisited = /* @__PURE__ */ new Set();
|
|
395
|
+
let loadedCount = 0;
|
|
396
|
+
for (const name of seedTables) {
|
|
397
|
+
if (forward) forwardQueue.push({ name, depth: 0 });
|
|
398
|
+
if (backward) backwardQueue.push({ name, depth: 0 });
|
|
399
|
+
}
|
|
400
|
+
const forwardLimit = forward === true ? Infinity : forward || 0;
|
|
401
|
+
while (forwardQueue.length > 0) {
|
|
402
|
+
const item = forwardQueue.shift();
|
|
403
|
+
if (!item) break;
|
|
404
|
+
const { name, depth } = item;
|
|
405
|
+
if (forwardVisited.has(name)) continue;
|
|
406
|
+
forwardVisited.add(name);
|
|
407
|
+
if (!tables2[name]) {
|
|
408
|
+
loadedCount++;
|
|
409
|
+
ctx.onProgress({
|
|
410
|
+
type: "phase:progress",
|
|
411
|
+
phase: "tables",
|
|
412
|
+
table: name,
|
|
413
|
+
message: `Loading table ${name}...`,
|
|
414
|
+
current: loadedCount
|
|
415
|
+
});
|
|
416
|
+
tables2[name] = applyColumnFilter(
|
|
417
|
+
await this.getTable(name),
|
|
418
|
+
this.#columns
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
if (depth < forwardLimit) {
|
|
422
|
+
const rels = await this.findOutgoingRelations(name);
|
|
423
|
+
for (const rel of rels) {
|
|
424
|
+
if (!this.isRelationshipVisible(rel)) continue;
|
|
425
|
+
this.addRelationship(rel, allRelationships, seenRelationships);
|
|
426
|
+
if (!forwardVisited.has(rel.referenced_table)) {
|
|
427
|
+
forwardQueue.push({ name: rel.referenced_table, depth: depth + 1 });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const backwardLimit = backward === true ? Infinity : backward || 0;
|
|
433
|
+
while (backwardQueue.length > 0) {
|
|
434
|
+
const item = backwardQueue.shift();
|
|
435
|
+
if (!item) break;
|
|
436
|
+
const { name, depth } = item;
|
|
437
|
+
if (backwardVisited.has(name)) continue;
|
|
438
|
+
backwardVisited.add(name);
|
|
439
|
+
if (!tables2[name]) {
|
|
440
|
+
loadedCount++;
|
|
441
|
+
ctx.onProgress({
|
|
442
|
+
type: "phase:progress",
|
|
443
|
+
phase: "tables",
|
|
444
|
+
table: name,
|
|
445
|
+
message: `Loading table ${name}...`,
|
|
446
|
+
current: loadedCount
|
|
447
|
+
});
|
|
448
|
+
tables2[name] = applyColumnFilter(
|
|
449
|
+
await this.getTable(name),
|
|
450
|
+
this.#columns
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (depth < backwardLimit) {
|
|
454
|
+
const rels = await this.findIncomingRelations(name);
|
|
455
|
+
for (const rel of rels) {
|
|
456
|
+
if (!this.isRelationshipVisible(rel)) continue;
|
|
457
|
+
this.addRelationship(rel, allRelationships, seenRelationships);
|
|
458
|
+
if (!backwardVisited.has(rel.table)) {
|
|
459
|
+
backwardQueue.push({ name: rel.table, depth: depth + 1 });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const tablesList = Object.values(tables2);
|
|
465
|
+
ctx.tables.push(...tablesList);
|
|
466
|
+
ctx.relationships.push(...allRelationships);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Apply the filter to get seed table names.
|
|
470
|
+
* If filter is an explicit array, skip querying all table names.
|
|
471
|
+
*/
|
|
472
|
+
async applyFilter() {
|
|
473
|
+
const filter = this.#filter;
|
|
474
|
+
if (Array.isArray(filter)) {
|
|
475
|
+
return filter;
|
|
476
|
+
}
|
|
477
|
+
const names = await this.getAllTableNames();
|
|
478
|
+
if (!filter) {
|
|
479
|
+
return names;
|
|
480
|
+
}
|
|
481
|
+
if (filter instanceof RegExp) {
|
|
482
|
+
return names.filter((name) => filter.test(name));
|
|
483
|
+
}
|
|
484
|
+
return names.filter(filter);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Add a relationship to the collection, deduplicating by key.
|
|
488
|
+
*/
|
|
489
|
+
addRelationship(rel, all, seen) {
|
|
490
|
+
const key = `${rel.table}:${rel.from.join(",")}:${rel.referenced_table}:${rel.to.join(",")}`;
|
|
491
|
+
if (!seen.has(key)) {
|
|
492
|
+
seen.add(key);
|
|
493
|
+
all.push(rel);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
isRelationshipVisible(rel) {
|
|
497
|
+
return this.areColumnsVisible(rel.from, this.#columns?.[rel.table]) && this.areColumnsVisible(rel.to, this.#columns?.[rel.referenced_table]);
|
|
498
|
+
}
|
|
499
|
+
areColumnsVisible(names, filter) {
|
|
500
|
+
if (!filter) return true;
|
|
501
|
+
return filterColumns(
|
|
502
|
+
names.map((name) => ({ name })),
|
|
503
|
+
filter
|
|
504
|
+
).length === names.length;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// packages/text2sql/src/lib/adapters/groundings/view.grounding.ts
|
|
509
|
+
var ViewGrounding = class extends AbstractGrounding {
|
|
510
|
+
#filter;
|
|
511
|
+
#columns;
|
|
512
|
+
includeDefinition;
|
|
513
|
+
constructor(config = {}) {
|
|
514
|
+
super("view", "views");
|
|
515
|
+
this.#filter = config.filter;
|
|
516
|
+
this.#columns = config.columns;
|
|
517
|
+
this.includeDefinition = config.includeDefinition ?? true;
|
|
518
|
+
}
|
|
519
|
+
async contributeEntities(ctx) {
|
|
520
|
+
await this.execute(ctx);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Execute the grounding process.
|
|
524
|
+
* Writes discovered views to the context.
|
|
525
|
+
*/
|
|
526
|
+
async execute(ctx) {
|
|
527
|
+
const viewNames = await this.applyFilter();
|
|
528
|
+
const views2 = await Promise.all(
|
|
529
|
+
viewNames.map(async (name, index2) => {
|
|
530
|
+
ctx.onProgress({
|
|
531
|
+
type: "phase:progress",
|
|
532
|
+
phase: "views",
|
|
533
|
+
table: name,
|
|
534
|
+
message: `Loading view ${name}...`,
|
|
535
|
+
current: index2 + 1,
|
|
536
|
+
total: viewNames.length
|
|
537
|
+
});
|
|
538
|
+
return applyColumnFilter(await this.getView(name), this.#columns);
|
|
539
|
+
})
|
|
540
|
+
);
|
|
541
|
+
ctx.views.push(...views2);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Apply the filter to get view names.
|
|
545
|
+
* If filter is an explicit array, skip querying all view names.
|
|
546
|
+
*/
|
|
547
|
+
async applyFilter() {
|
|
548
|
+
const filter = this.#filter;
|
|
549
|
+
if (Array.isArray(filter)) {
|
|
550
|
+
return filter;
|
|
551
|
+
}
|
|
552
|
+
const names = await this.getAllViewNames();
|
|
553
|
+
if (!filter) {
|
|
554
|
+
return names;
|
|
555
|
+
}
|
|
556
|
+
if (filter instanceof RegExp) {
|
|
557
|
+
return names.filter((name) => filter.test(name));
|
|
558
|
+
}
|
|
559
|
+
return names.filter(filter);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// packages/text2sql/src/lib/adapters/duckdb/duckdb-identifiers.ts
|
|
564
|
+
import { quotedIdentifier } from "@duckdb/node-api";
|
|
565
|
+
function formatDuckDBIdentifierPath(parts) {
|
|
566
|
+
return parts.map(quotedIdentifier).join(".");
|
|
567
|
+
}
|
|
568
|
+
function parseDuckDBRelationName(name) {
|
|
569
|
+
const parts = splitIdentifierPath(name);
|
|
570
|
+
if (parts.length === 1) return { table: parts[0] };
|
|
571
|
+
if (parts.length === 2) {
|
|
572
|
+
return { schema: parts[0], table: parts[1] };
|
|
573
|
+
}
|
|
574
|
+
if (parts.length === 3) {
|
|
575
|
+
return { catalog: parts[0], schema: parts[1], table: parts[2] };
|
|
576
|
+
}
|
|
577
|
+
throw new Error(`DuckDB relation must have at most three parts: ${name}`);
|
|
578
|
+
}
|
|
579
|
+
function splitIdentifierPath(input) {
|
|
580
|
+
const parts = [];
|
|
581
|
+
let part = "";
|
|
582
|
+
let quoted = false;
|
|
583
|
+
for (let index2 = 0; index2 < input.length; index2++) {
|
|
584
|
+
const character = input[index2];
|
|
585
|
+
if (character === '"') {
|
|
586
|
+
if (quoted && input[index2 + 1] === '"') {
|
|
587
|
+
part += '"';
|
|
588
|
+
index2++;
|
|
589
|
+
} else {
|
|
590
|
+
quoted = !quoted;
|
|
591
|
+
}
|
|
592
|
+
} else if (character === "." && !quoted) {
|
|
593
|
+
if (!part) throw new Error(`Invalid DuckDB relation: ${input}`);
|
|
594
|
+
parts.push(part);
|
|
595
|
+
part = "";
|
|
596
|
+
} else {
|
|
597
|
+
part += character;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (quoted || !part) throw new Error(`Invalid DuckDB relation: ${input}`);
|
|
601
|
+
parts.push(part);
|
|
602
|
+
return parts;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// packages/text2sql/src/lib/adapters/duckdb/duckdb.groundings.ts
|
|
606
|
+
var CONSTRAINT_TYPES = /* @__PURE__ */ new Map([
|
|
607
|
+
["CHECK", "CHECK"],
|
|
608
|
+
["UNIQUE", "UNIQUE"],
|
|
609
|
+
["NOT NULL", "NOT_NULL"],
|
|
610
|
+
["PRIMARY KEY", "PRIMARY_KEY"],
|
|
611
|
+
["FOREIGN KEY", "FOREIGN_KEY"]
|
|
612
|
+
]);
|
|
613
|
+
var DuckDBTableGrounding = class extends TableGrounding {
|
|
614
|
+
#adapter;
|
|
615
|
+
constructor(adapter, config = {}) {
|
|
616
|
+
super(config);
|
|
617
|
+
this.#adapter = adapter;
|
|
618
|
+
}
|
|
619
|
+
async applyFilter() {
|
|
620
|
+
const names = await super.applyFilter();
|
|
621
|
+
const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
|
|
622
|
+
const result = [];
|
|
623
|
+
for (const name of names) {
|
|
624
|
+
const relation = await this.#adapter.resolveRelationName(name);
|
|
625
|
+
if (catalogs.includes(relation.catalog) && schemas.includes(relation.schema)) {
|
|
626
|
+
result.push(
|
|
627
|
+
this.#adapter.formatRelationName(
|
|
628
|
+
relation.catalog,
|
|
629
|
+
relation.schema,
|
|
630
|
+
relation.table
|
|
631
|
+
)
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return result;
|
|
636
|
+
}
|
|
637
|
+
async getAllTableNames() {
|
|
638
|
+
const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
|
|
639
|
+
const rows = await this.#adapter.runQuery(`
|
|
640
|
+
SELECT database_name, schema_name, table_name
|
|
641
|
+
FROM duckdb_tables()
|
|
642
|
+
WHERE NOT internal
|
|
643
|
+
AND NOT temporary
|
|
644
|
+
AND database_name IN (${sqlLiterals(this.#adapter, catalogs)})
|
|
645
|
+
AND schema_name IN (${sqlLiterals(this.#adapter, schemas)})
|
|
646
|
+
ORDER BY database_name, schema_name, table_name
|
|
647
|
+
`);
|
|
648
|
+
return rows.map((row) => {
|
|
649
|
+
if (typeof row.database_name !== "string" || typeof row.schema_name !== "string" || typeof row.table_name !== "string") {
|
|
650
|
+
throw new Error("DuckDB table catalog returned an unknown row shape.");
|
|
651
|
+
}
|
|
652
|
+
return this.#adapter.formatRelationName(
|
|
653
|
+
row.database_name,
|
|
654
|
+
row.schema_name,
|
|
655
|
+
row.table_name
|
|
656
|
+
);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
async getTable(tableName) {
|
|
660
|
+
const { catalog, schema, table: table2 } = await this.#adapter.resolveRelationName(tableName);
|
|
661
|
+
const rows = await this.#adapter.runQuery(`
|
|
662
|
+
SELECT column_name, data_type
|
|
663
|
+
FROM duckdb_columns()
|
|
664
|
+
WHERE database_name = '${this.#adapter.escapeString(catalog)}'
|
|
665
|
+
AND schema_name = '${this.#adapter.escapeString(schema)}'
|
|
666
|
+
AND table_name = '${this.#adapter.escapeString(table2)}'
|
|
667
|
+
ORDER BY column_index
|
|
668
|
+
`);
|
|
669
|
+
return {
|
|
670
|
+
name: this.#adapter.formatRelationName(catalog, schema, table2),
|
|
671
|
+
schema: formatDuckDBIdentifierPath([catalog, schema]),
|
|
672
|
+
rawName: table2,
|
|
673
|
+
columns: rows.map((row) => {
|
|
674
|
+
if (typeof row.column_name !== "string" || typeof row.data_type !== "string") {
|
|
675
|
+
throw new Error(
|
|
676
|
+
"DuckDB column catalog returned an unknown row shape."
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return { name: row.column_name, type: row.data_type };
|
|
680
|
+
})
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async findOutgoingRelations(tableName) {
|
|
684
|
+
const relation = await this.#adapter.resolveRelationName(tableName);
|
|
685
|
+
const rows = await foreignKeys(this.#adapter, relation, "outgoing");
|
|
686
|
+
return rows.map((row) => relationshipFromRow(this.#adapter, relation, row));
|
|
687
|
+
}
|
|
688
|
+
async findIncomingRelations(tableName) {
|
|
689
|
+
const relation = await this.#adapter.resolveRelationName(tableName);
|
|
690
|
+
const rows = await foreignKeys(this.#adapter, relation, "incoming");
|
|
691
|
+
return rows.map((row) => {
|
|
692
|
+
const source = {
|
|
693
|
+
catalog: relation.catalog,
|
|
694
|
+
schema: relation.schema,
|
|
695
|
+
table: requiredString(row.table_name, "table_name")
|
|
696
|
+
};
|
|
697
|
+
return relationshipFromRow(this.#adapter, source, row);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
var DuckDBInfoGrounding = class extends InfoGrounding {
|
|
702
|
+
#adapter;
|
|
703
|
+
constructor(adapter, config = {}) {
|
|
704
|
+
super(config);
|
|
705
|
+
this.#adapter = adapter;
|
|
706
|
+
}
|
|
707
|
+
async collectInfo() {
|
|
708
|
+
const rows = await this.#adapter.runQuery(`
|
|
709
|
+
SELECT
|
|
710
|
+
version() AS version,
|
|
711
|
+
current_database() AS catalog,
|
|
712
|
+
current_schema() AS schema
|
|
713
|
+
`);
|
|
714
|
+
const row = rows[0];
|
|
715
|
+
return {
|
|
716
|
+
dialect: "duckdb",
|
|
717
|
+
version: typeof row?.version === "string" ? row.version : void 0,
|
|
718
|
+
database: typeof row?.catalog === "string" ? row.catalog : void 0,
|
|
719
|
+
details: {
|
|
720
|
+
currentSchema: typeof row?.schema === "string" ? row.schema : "unknown",
|
|
721
|
+
identifierQualification: "catalog.schema.table",
|
|
722
|
+
parameterPlaceholders: ["$1", "$name"]
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
var DuckDBViewGrounding = class extends ViewGrounding {
|
|
728
|
+
#adapter;
|
|
729
|
+
constructor(adapter, config = {}) {
|
|
730
|
+
super(config);
|
|
731
|
+
this.#adapter = adapter;
|
|
732
|
+
}
|
|
733
|
+
async applyFilter() {
|
|
734
|
+
const names = await super.applyFilter();
|
|
735
|
+
const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
|
|
736
|
+
const result = [];
|
|
737
|
+
for (const name of names) {
|
|
738
|
+
const relation = await this.#adapter.resolveRelationName(name);
|
|
739
|
+
if (catalogs.includes(relation.catalog) && schemas.includes(relation.schema)) {
|
|
740
|
+
result.push(
|
|
741
|
+
this.#adapter.formatRelationName(
|
|
742
|
+
relation.catalog,
|
|
743
|
+
relation.schema,
|
|
744
|
+
relation.table
|
|
745
|
+
)
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return result;
|
|
750
|
+
}
|
|
751
|
+
async getAllViewNames() {
|
|
752
|
+
const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
|
|
753
|
+
const rows = await this.#adapter.runQuery(`
|
|
754
|
+
SELECT database_name, schema_name, view_name
|
|
755
|
+
FROM duckdb_views()
|
|
756
|
+
WHERE NOT internal
|
|
757
|
+
AND NOT temporary
|
|
758
|
+
AND database_name IN (${sqlLiterals(this.#adapter, catalogs)})
|
|
759
|
+
AND schema_name IN (${sqlLiterals(this.#adapter, schemas)})
|
|
760
|
+
ORDER BY database_name, schema_name, view_name
|
|
761
|
+
`);
|
|
762
|
+
return rows.map(
|
|
763
|
+
(row) => this.#adapter.formatRelationName(
|
|
764
|
+
requiredString(row.database_name, "database_name"),
|
|
765
|
+
requiredString(row.schema_name, "schema_name"),
|
|
766
|
+
requiredString(row.view_name, "view_name")
|
|
767
|
+
)
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
async getView(viewName) {
|
|
771
|
+
const relation = await this.#adapter.resolveRelationName(viewName);
|
|
772
|
+
const where = relationPredicate(this.#adapter, relation);
|
|
773
|
+
const [columns, definitions] = await Promise.all([
|
|
774
|
+
this.#adapter.runQuery(`
|
|
775
|
+
SELECT column_name, data_type
|
|
776
|
+
FROM duckdb_columns()
|
|
777
|
+
WHERE ${where}
|
|
778
|
+
ORDER BY column_index
|
|
779
|
+
`),
|
|
780
|
+
this.includeDefinition ? this.#adapter.runQuery(`
|
|
781
|
+
SELECT sql
|
|
782
|
+
FROM duckdb_views()
|
|
783
|
+
WHERE database_name = '${this.#adapter.escapeString(relation.catalog)}'
|
|
784
|
+
AND schema_name = '${this.#adapter.escapeString(relation.schema)}'
|
|
785
|
+
AND view_name = '${this.#adapter.escapeString(relation.table)}'
|
|
786
|
+
`) : Promise.resolve([])
|
|
787
|
+
]);
|
|
788
|
+
return {
|
|
789
|
+
name: this.#adapter.formatRelationName(
|
|
790
|
+
relation.catalog,
|
|
791
|
+
relation.schema,
|
|
792
|
+
relation.table
|
|
793
|
+
),
|
|
794
|
+
schema: formatDuckDBIdentifierPath([relation.catalog, relation.schema]),
|
|
795
|
+
rawName: relation.table,
|
|
796
|
+
definition: typeof definitions[0]?.sql === "string" ? definitions[0].sql : void 0,
|
|
797
|
+
columns: columns.map(columnFromRow)
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
var DuckDBConstraintGrounding = class extends ConstraintGrounding {
|
|
802
|
+
#adapter;
|
|
803
|
+
constructor(adapter, config = {}) {
|
|
804
|
+
super(config);
|
|
805
|
+
this.#adapter = adapter;
|
|
806
|
+
}
|
|
807
|
+
async getConstraints(tableName) {
|
|
808
|
+
const relation = await this.#adapter.resolveRelationName(tableName);
|
|
809
|
+
const [rows, columns] = await Promise.all([
|
|
810
|
+
this.#adapter.runQuery(`
|
|
811
|
+
SELECT
|
|
812
|
+
constraint_name,
|
|
813
|
+
constraint_type,
|
|
814
|
+
expression,
|
|
815
|
+
constraint_column_names,
|
|
816
|
+
referenced_table,
|
|
817
|
+
referenced_column_names
|
|
818
|
+
FROM duckdb_constraints()
|
|
819
|
+
WHERE ${relationPredicate(this.#adapter, relation)}
|
|
820
|
+
ORDER BY constraint_index
|
|
821
|
+
`),
|
|
822
|
+
this.#adapter.runQuery(`
|
|
823
|
+
SELECT column_name, column_default
|
|
824
|
+
FROM duckdb_columns()
|
|
825
|
+
WHERE ${relationPredicate(this.#adapter, relation)}
|
|
826
|
+
AND column_default IS NOT NULL
|
|
827
|
+
ORDER BY column_index
|
|
828
|
+
`)
|
|
829
|
+
]);
|
|
830
|
+
const constraints2 = rows.map(
|
|
831
|
+
(row) => constraintFromRow(this.#adapter, relation, row)
|
|
832
|
+
);
|
|
833
|
+
for (const column2 of columns) {
|
|
834
|
+
if (typeof column2.column_name === "string" && typeof column2.column_default === "string") {
|
|
835
|
+
constraints2.push({
|
|
836
|
+
name: `${relation.table}_${column2.column_name}_default`,
|
|
837
|
+
type: "DEFAULT",
|
|
838
|
+
columns: [column2.column_name],
|
|
839
|
+
defaultValue: column2.column_default
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return constraints2;
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
var DuckDBIndexesGrounding = class extends IndexesGrounding {
|
|
847
|
+
#adapter;
|
|
848
|
+
constructor(adapter, config = {}) {
|
|
849
|
+
super(config);
|
|
850
|
+
this.#adapter = adapter;
|
|
851
|
+
}
|
|
852
|
+
async getIndexes(tableName) {
|
|
853
|
+
const relation = await this.#adapter.resolveRelationName(tableName);
|
|
854
|
+
const [rows, columnRows] = await Promise.all([
|
|
855
|
+
this.#adapter.runQuery(`
|
|
856
|
+
SELECT
|
|
857
|
+
index_name,
|
|
858
|
+
TRY_CAST(expressions AS VARCHAR[]) AS expressions,
|
|
859
|
+
is_unique,
|
|
860
|
+
is_primary
|
|
861
|
+
FROM duckdb_indexes()
|
|
862
|
+
WHERE ${relationPredicate(this.#adapter, relation)}
|
|
863
|
+
ORDER BY index_name
|
|
864
|
+
`),
|
|
865
|
+
this.#adapter.runQuery(`
|
|
866
|
+
SELECT column_name
|
|
867
|
+
FROM duckdb_columns()
|
|
868
|
+
WHERE ${relationPredicate(this.#adapter, relation)}
|
|
869
|
+
ORDER BY column_index
|
|
870
|
+
`)
|
|
871
|
+
]);
|
|
872
|
+
const tableColumns = columnRows.map(
|
|
873
|
+
(row) => requiredString(row.column_name, "column_name")
|
|
874
|
+
);
|
|
875
|
+
return rows.flatMap((row) => {
|
|
876
|
+
if (typeof row.index_name !== "string") {
|
|
877
|
+
throw new Error("DuckDB index catalog returned an unknown row shape.");
|
|
878
|
+
}
|
|
879
|
+
if (!Array.isArray(row.expressions) || row.expressions.some((expression) => typeof expression !== "string")) {
|
|
880
|
+
return [];
|
|
881
|
+
}
|
|
882
|
+
const columns = row.expressions.map(
|
|
883
|
+
(expression) => tableColumns.find(
|
|
884
|
+
(column2) => expression === column2 || expression === this.#adapter.quoteIdentifier(column2)
|
|
885
|
+
)
|
|
886
|
+
);
|
|
887
|
+
return columns.length > 0 && columns.every((column2) => column2 !== void 0) ? [
|
|
888
|
+
{
|
|
889
|
+
name: row.index_name,
|
|
890
|
+
columns,
|
|
891
|
+
unique: row.is_unique === true,
|
|
892
|
+
type: row.is_primary === true ? "PRIMARY" : void 0
|
|
893
|
+
}
|
|
894
|
+
] : [];
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
var DuckDBRowCountGrounding = class extends RowCountGrounding {
|
|
899
|
+
#adapter;
|
|
900
|
+
constructor(adapter, config = {}) {
|
|
901
|
+
super(config);
|
|
902
|
+
this.#adapter = adapter;
|
|
903
|
+
}
|
|
904
|
+
async getRowCount(tableName) {
|
|
905
|
+
const relation = await this.#adapter.resolveRelationName(tableName);
|
|
906
|
+
const rows = await this.#adapter.runQuery(`
|
|
907
|
+
SELECT estimated_size
|
|
908
|
+
FROM duckdb_tables()
|
|
909
|
+
WHERE ${relationPredicate(this.#adapter, relation)}
|
|
910
|
+
`);
|
|
911
|
+
return this.#adapter.toNumber(rows[0]?.estimated_size);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
var DuckDBColumnStatsGrounding = class extends ColumnStatsGrounding {
|
|
915
|
+
#adapter;
|
|
916
|
+
constructor(adapter, config = {}) {
|
|
917
|
+
super(config);
|
|
918
|
+
this.#adapter = adapter;
|
|
919
|
+
}
|
|
920
|
+
async collectStats(tableName, column2) {
|
|
921
|
+
if (!/int|real|numeric|double|float|decimal|date|time|bool/i.test(column2.type)) {
|
|
922
|
+
return void 0;
|
|
923
|
+
}
|
|
924
|
+
const relation = this.#adapter.quoteRelationName(tableName);
|
|
925
|
+
const identifier = this.#adapter.quoteIdentifier(column2.name);
|
|
926
|
+
const rows = await this.#adapter.runQuery(`
|
|
927
|
+
SELECT
|
|
928
|
+
CAST(MIN(${identifier}) AS VARCHAR) AS min_value,
|
|
929
|
+
CAST(MAX(${identifier}) AS VARCHAR) AS max_value,
|
|
930
|
+
CASE
|
|
931
|
+
WHEN COUNT(*) = 0 THEN NULL
|
|
932
|
+
ELSE COUNT(*) FILTER (WHERE ${identifier} IS NULL)::DOUBLE / COUNT(*)
|
|
933
|
+
END AS null_fraction,
|
|
934
|
+
COUNT(DISTINCT ${identifier}) AS n_distinct
|
|
935
|
+
FROM ${relation}
|
|
936
|
+
`);
|
|
937
|
+
const row = rows[0];
|
|
938
|
+
if (!row) return void 0;
|
|
939
|
+
return {
|
|
940
|
+
min: typeof row.min_value === "string" ? row.min_value : void 0,
|
|
941
|
+
max: typeof row.max_value === "string" ? row.max_value : void 0,
|
|
942
|
+
nullFraction: this.#adapter.toNumber(row.null_fraction),
|
|
943
|
+
nDistinct: this.#adapter.toNumber(row.n_distinct)
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
var DuckDBColumnValuesGrounding = class extends ColumnValuesGrounding {
|
|
948
|
+
#adapter;
|
|
949
|
+
constructor(adapter, config = {}) {
|
|
950
|
+
super(config);
|
|
951
|
+
this.#adapter = adapter;
|
|
952
|
+
}
|
|
953
|
+
async collectEnumValues(_tableName, column2) {
|
|
954
|
+
return parseEnumLabels(column2.type);
|
|
955
|
+
}
|
|
956
|
+
async collectLowCardinality(tableName, column2) {
|
|
957
|
+
if (/\b(?:BLOB|STRUCT|MAP|LIST|UNION|ARRAY)\b/i.test(column2.type)) {
|
|
958
|
+
return void 0;
|
|
959
|
+
}
|
|
960
|
+
const relation = this.#adapter.quoteRelationName(tableName);
|
|
961
|
+
const identifier = this.#adapter.quoteIdentifier(column2.name);
|
|
962
|
+
const rows = await this.#adapter.runQuery(`
|
|
963
|
+
SELECT DISTINCT CAST(${identifier} AS VARCHAR) AS value
|
|
964
|
+
FROM ${relation}
|
|
965
|
+
WHERE ${identifier} IS NOT NULL
|
|
966
|
+
ORDER BY value
|
|
967
|
+
LIMIT ${this.lowCardinalityLimit + 1}
|
|
968
|
+
`);
|
|
969
|
+
if (rows.length === 0 || rows.length > this.lowCardinalityLimit) {
|
|
970
|
+
return void 0;
|
|
971
|
+
}
|
|
972
|
+
if (rows.some((row) => typeof row.value !== "string")) return void 0;
|
|
973
|
+
return rows.map((row) => row.value);
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
function sqlLiterals(adapter, values) {
|
|
977
|
+
return values.map((value) => `'${adapter.escapeString(value)}'`).join(", ");
|
|
978
|
+
}
|
|
979
|
+
function relationPredicate(adapter, relation) {
|
|
980
|
+
return `database_name = '${adapter.escapeString(relation.catalog)}'
|
|
981
|
+
AND schema_name = '${adapter.escapeString(relation.schema)}'
|
|
982
|
+
AND table_name = '${adapter.escapeString(relation.table)}'`;
|
|
983
|
+
}
|
|
984
|
+
function columnFromRow(row) {
|
|
985
|
+
return {
|
|
986
|
+
name: requiredString(row.column_name, "column_name"),
|
|
987
|
+
type: requiredString(row.data_type, "data_type")
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
async function foreignKeys(adapter, relation, direction) {
|
|
991
|
+
const predicate = direction === "outgoing" ? `table_name = '${adapter.escapeString(relation.table)}'` : `referenced_table = '${adapter.escapeString(relation.table)}'`;
|
|
992
|
+
return adapter.runQuery(`
|
|
993
|
+
SELECT
|
|
994
|
+
table_name,
|
|
995
|
+
constraint_name,
|
|
996
|
+
constraint_type,
|
|
997
|
+
expression,
|
|
998
|
+
constraint_column_names,
|
|
999
|
+
referenced_table,
|
|
1000
|
+
referenced_column_names
|
|
1001
|
+
FROM duckdb_constraints()
|
|
1002
|
+
WHERE database_name = '${adapter.escapeString(relation.catalog)}'
|
|
1003
|
+
AND schema_name = '${adapter.escapeString(relation.schema)}'
|
|
1004
|
+
AND constraint_type = 'FOREIGN KEY'
|
|
1005
|
+
AND ${predicate}
|
|
1006
|
+
ORDER BY table_name, constraint_index
|
|
1007
|
+
`);
|
|
1008
|
+
}
|
|
1009
|
+
function relationshipFromRow(adapter, source, row) {
|
|
1010
|
+
return {
|
|
1011
|
+
table: adapter.formatRelationName(
|
|
1012
|
+
source.catalog,
|
|
1013
|
+
source.schema,
|
|
1014
|
+
source.table
|
|
1015
|
+
),
|
|
1016
|
+
from: stringArray(row.constraint_column_names, "constraint_column_names"),
|
|
1017
|
+
referenced_table: adapter.formatRelationName(
|
|
1018
|
+
source.catalog,
|
|
1019
|
+
source.schema,
|
|
1020
|
+
requiredString(row.referenced_table, "referenced_table")
|
|
1021
|
+
),
|
|
1022
|
+
to: stringArray(row.referenced_column_names, "referenced_column_names")
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function constraintFromRow(adapter, relation, row) {
|
|
1026
|
+
const type = requiredString(row.constraint_type, "constraint_type");
|
|
1027
|
+
const mapped = CONSTRAINT_TYPES.get(type);
|
|
1028
|
+
if (!mapped) throw new Error(`Unknown DuckDB constraint type: ${type}`);
|
|
1029
|
+
const referenced = mapped === "FOREIGN_KEY" ? adapter.formatRelationName(
|
|
1030
|
+
relation.catalog,
|
|
1031
|
+
relation.schema,
|
|
1032
|
+
requiredString(row.referenced_table, "referenced_table")
|
|
1033
|
+
) : void 0;
|
|
1034
|
+
return {
|
|
1035
|
+
name: requiredString(row.constraint_name, "constraint_name"),
|
|
1036
|
+
type: mapped,
|
|
1037
|
+
columns: stringArray(
|
|
1038
|
+
row.constraint_column_names,
|
|
1039
|
+
"constraint_column_names"
|
|
1040
|
+
),
|
|
1041
|
+
definition: mapped === "CHECK" && typeof row.expression === "string" ? row.expression : void 0,
|
|
1042
|
+
referencedTable: referenced,
|
|
1043
|
+
referencedColumns: mapped === "FOREIGN_KEY" ? stringArray(row.referenced_column_names, "referenced_column_names") : void 0
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
function parseEnumLabels(type) {
|
|
1047
|
+
const body = /^ENUM\((.*)\)$/i.exec(type)?.[1];
|
|
1048
|
+
if (body === void 0) return void 0;
|
|
1049
|
+
const labels = [...body.matchAll(/'((?:''|[^'])*)'/g)].map(
|
|
1050
|
+
(match) => match[1].replaceAll("''", "'")
|
|
1051
|
+
);
|
|
1052
|
+
return labels.length > 0 ? labels : void 0;
|
|
1053
|
+
}
|
|
1054
|
+
function requiredString(value, field) {
|
|
1055
|
+
if (typeof value !== "string") {
|
|
1056
|
+
throw new Error(`DuckDB catalog field ${field} must be a string.`);
|
|
1057
|
+
}
|
|
1058
|
+
return value;
|
|
1059
|
+
}
|
|
1060
|
+
function stringArray(value, field) {
|
|
1061
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
1062
|
+
throw new Error(`DuckDB catalog field ${field} must be a string array.`);
|
|
1063
|
+
}
|
|
1064
|
+
return value;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// packages/text2sql/src/lib/adapters/adapter.ts
|
|
1068
|
+
import { format as formatSql } from "sql-formatter";
|
|
1069
|
+
|
|
1070
|
+
// packages/text2sql/src/lib/agents/exceptions.ts
|
|
1071
|
+
var sqlValidationMarker = /* @__PURE__ */ Symbol("SQLValidationError");
|
|
1072
|
+
var unanswerableSqlMarker = /* @__PURE__ */ Symbol("UnanswerableSQLError");
|
|
1073
|
+
var sqlScopeMarker = /* @__PURE__ */ Symbol("SQLScopeError");
|
|
1074
|
+
var sqlReadOnlyMarker = /* @__PURE__ */ Symbol("SQLReadOnlyError");
|
|
1075
|
+
var SQLValidationError = class _SQLValidationError extends Error {
|
|
1076
|
+
[sqlValidationMarker];
|
|
1077
|
+
constructor(message) {
|
|
1078
|
+
super(message);
|
|
1079
|
+
this.name = "SQLValidationError";
|
|
1080
|
+
this[sqlValidationMarker] = true;
|
|
1081
|
+
}
|
|
1082
|
+
static isInstance(error) {
|
|
1083
|
+
return error instanceof _SQLValidationError && error[sqlValidationMarker] === true;
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
var UnanswerableSQLError = class _UnanswerableSQLError extends Error {
|
|
1087
|
+
[unanswerableSqlMarker];
|
|
1088
|
+
constructor(message) {
|
|
1089
|
+
super(message);
|
|
1090
|
+
this.name = "UnanswerableSQLError";
|
|
1091
|
+
this[unanswerableSqlMarker] = true;
|
|
1092
|
+
}
|
|
1093
|
+
static isInstance(error) {
|
|
1094
|
+
return error instanceof _UnanswerableSQLError && error[unanswerableSqlMarker] === true;
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
var SQLScopeError = class _SQLScopeError extends Error {
|
|
1098
|
+
[sqlScopeMarker];
|
|
1099
|
+
payload;
|
|
1100
|
+
errorType;
|
|
1101
|
+
constructor(payload) {
|
|
1102
|
+
super(JSON.stringify(payload));
|
|
1103
|
+
this.name = "SQLScopeError";
|
|
1104
|
+
this.payload = payload;
|
|
1105
|
+
this.errorType = payload.error_type;
|
|
1106
|
+
this[sqlScopeMarker] = true;
|
|
1107
|
+
}
|
|
1108
|
+
static isInstance(error) {
|
|
1109
|
+
return error instanceof _SQLScopeError && error[sqlScopeMarker] === true;
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
var SQLReadOnlyError = class _SQLReadOnlyError extends Error {
|
|
1113
|
+
[sqlReadOnlyMarker];
|
|
1114
|
+
constructor(message) {
|
|
1115
|
+
super(message);
|
|
1116
|
+
this.name = "SQLReadOnlyError";
|
|
1117
|
+
this[sqlReadOnlyMarker] = true;
|
|
1118
|
+
}
|
|
1119
|
+
static isInstance(error) {
|
|
1120
|
+
return error instanceof _SQLReadOnlyError && error[sqlReadOnlyMarker] === true;
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// packages/text2sql/src/lib/fragments/schema.ts
|
|
1125
|
+
function dialectInfo(input) {
|
|
1126
|
+
return {
|
|
1127
|
+
name: "dialectInfo",
|
|
1128
|
+
data: {
|
|
1129
|
+
dialect: input.dialect,
|
|
1130
|
+
...input.version && { version: input.version },
|
|
1131
|
+
...input.database && { database: input.database },
|
|
1132
|
+
...input.details && { details: input.details }
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function table(input) {
|
|
1137
|
+
return {
|
|
1138
|
+
name: "table",
|
|
1139
|
+
data: {
|
|
1140
|
+
name: input.name,
|
|
1141
|
+
...input.schema && { schema: input.schema },
|
|
1142
|
+
...input.rowCount != null && { rowCount: input.rowCount },
|
|
1143
|
+
...input.sizeHint && { sizeHint: input.sizeHint },
|
|
1144
|
+
columns: input.columns,
|
|
1145
|
+
...input.indexes?.length && { indexes: input.indexes },
|
|
1146
|
+
...input.constraints?.length && { constraints: input.constraints }
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function column(input) {
|
|
1151
|
+
return {
|
|
1152
|
+
name: "column",
|
|
1153
|
+
data: {
|
|
1154
|
+
name: input.name,
|
|
1155
|
+
type: input.type,
|
|
1156
|
+
...input.pk && { pk: true },
|
|
1157
|
+
...input.fk && { fk: input.fk },
|
|
1158
|
+
...input.unique && { unique: true },
|
|
1159
|
+
...input.notNull && { notNull: true },
|
|
1160
|
+
...input.default && { default: input.default },
|
|
1161
|
+
...input.indexed && { indexed: true },
|
|
1162
|
+
...input.values?.length && { values: input.values },
|
|
1163
|
+
...input.stats && { stats: input.stats }
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function index(input) {
|
|
1168
|
+
return {
|
|
1169
|
+
name: "index",
|
|
1170
|
+
data: {
|
|
1171
|
+
name: input.name,
|
|
1172
|
+
columns: input.columns,
|
|
1173
|
+
...input.unique && { unique: true },
|
|
1174
|
+
...input.type && { type: input.type }
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function constraint(input) {
|
|
1179
|
+
return {
|
|
1180
|
+
name: "constraint",
|
|
1181
|
+
data: {
|
|
1182
|
+
name: input.name,
|
|
1183
|
+
type: input.type,
|
|
1184
|
+
...input.columns?.length && { columns: input.columns },
|
|
1185
|
+
...input.definition && { definition: input.definition },
|
|
1186
|
+
...input.defaultValue && { defaultValue: input.defaultValue },
|
|
1187
|
+
...input.referencedTable && { referencedTable: input.referencedTable },
|
|
1188
|
+
...input.referencedColumns?.length && {
|
|
1189
|
+
referencedColumns: input.referencedColumns
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function view(input) {
|
|
1195
|
+
return {
|
|
1196
|
+
name: "view",
|
|
1197
|
+
data: {
|
|
1198
|
+
name: input.name,
|
|
1199
|
+
...input.schema && { schema: input.schema },
|
|
1200
|
+
columns: input.columns,
|
|
1201
|
+
...input.definition && { definition: input.definition }
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function relationship(input) {
|
|
1206
|
+
return {
|
|
1207
|
+
name: "relationship",
|
|
1208
|
+
data: {
|
|
1209
|
+
from: input.from,
|
|
1210
|
+
to: input.to,
|
|
1211
|
+
...input.cardinality && { cardinality: input.cardinality }
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// packages/text2sql/src/lib/adapters/groundings/context.ts
|
|
1217
|
+
var noopProgress = () => {
|
|
1218
|
+
};
|
|
1219
|
+
function timestampProgress(onProgress) {
|
|
1220
|
+
if (!onProgress) return noopProgress;
|
|
1221
|
+
return (progress) => {
|
|
1222
|
+
onProgress({
|
|
1223
|
+
...progress,
|
|
1224
|
+
timestampMs: progress.timestampMs ?? Date.now()
|
|
1225
|
+
});
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
function createGroundingContext(options = {}) {
|
|
1229
|
+
return {
|
|
1230
|
+
tables: [],
|
|
1231
|
+
views: [],
|
|
1232
|
+
relationships: [],
|
|
1233
|
+
info: void 0,
|
|
1234
|
+
fragments: [],
|
|
1235
|
+
cache: /* @__PURE__ */ new Map(),
|
|
1236
|
+
onProgress: timestampProgress(options.onProgress)
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// packages/text2sql/src/lib/adapters/adapter.ts
|
|
1241
|
+
var Adapter = class {
|
|
1242
|
+
#policyAnalyzer;
|
|
1243
|
+
/**
|
|
1244
|
+
* Injects the strategy that guards validate() and execute().
|
|
1245
|
+
*/
|
|
1246
|
+
constructor(policyAnalyzer) {
|
|
1247
|
+
this.#policyAnalyzer = policyAnalyzer;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Introspect the database schema and return context fragments.
|
|
1251
|
+
*
|
|
1252
|
+
* Executes all configured groundings to populate the context, then
|
|
1253
|
+
* generates fragments from the complete context data.
|
|
1254
|
+
*
|
|
1255
|
+
* @param ctx - Optional grounding context for sharing state between groundings
|
|
1256
|
+
* @returns Array of context fragments representing the database schema
|
|
1257
|
+
*/
|
|
1258
|
+
async introspect(ctx = createGroundingContext()) {
|
|
1259
|
+
for (const fn of this.grounding) {
|
|
1260
|
+
const grounding = fn(this);
|
|
1261
|
+
if (grounding.phase) {
|
|
1262
|
+
ctx.onProgress({
|
|
1263
|
+
type: "phase:start",
|
|
1264
|
+
phase: grounding.phase,
|
|
1265
|
+
message: `Starting ${grounding.name} introspection...`
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
try {
|
|
1269
|
+
await grounding.execute(ctx);
|
|
1270
|
+
} finally {
|
|
1271
|
+
if (grounding.phase) {
|
|
1272
|
+
ctx.onProgress({
|
|
1273
|
+
type: "phase:end",
|
|
1274
|
+
phase: grounding.phase,
|
|
1275
|
+
message: `Finished ${grounding.name} introspection.`
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return this.#toSchemaFragments(ctx);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Resolve the allowed entity names (tables + views) from grounding config.
|
|
1284
|
+
* Asks each grounding to contribute its entities; annotators contribute
|
|
1285
|
+
* nothing, so their metadata scans are skipped.
|
|
1286
|
+
* Results are NOT cached — call once and store the result.
|
|
1287
|
+
*/
|
|
1288
|
+
async resolveAllowedEntities() {
|
|
1289
|
+
const ctx = createGroundingContext();
|
|
1290
|
+
for (const fn of this.grounding) {
|
|
1291
|
+
await fn(this).contributeEntities(ctx);
|
|
1292
|
+
}
|
|
1293
|
+
return [...ctx.tables.map((t) => t.name), ...ctx.views.map((v) => v.name)];
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Convert complete grounding context to schema fragments.
|
|
1297
|
+
* Called after all groundings have populated ctx with data.
|
|
1298
|
+
*/
|
|
1299
|
+
#toSchemaFragments(ctx) {
|
|
1300
|
+
const fragments = [];
|
|
1301
|
+
if (ctx.info) {
|
|
1302
|
+
fragments.push(
|
|
1303
|
+
dialectInfo({
|
|
1304
|
+
dialect: ctx.info.dialect,
|
|
1305
|
+
version: ctx.info.version,
|
|
1306
|
+
database: ctx.info.database,
|
|
1307
|
+
details: ctx.info.details
|
|
1308
|
+
})
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
for (const t of ctx.tables) {
|
|
1312
|
+
fragments.push(this.#tableToFragment(t));
|
|
1313
|
+
}
|
|
1314
|
+
for (const v of ctx.views) {
|
|
1315
|
+
fragments.push(this.#viewToFragment(v));
|
|
1316
|
+
}
|
|
1317
|
+
const tableMap = new Map(ctx.tables.map((t) => [t.name, t]));
|
|
1318
|
+
const tableColumnSets = new Map(
|
|
1319
|
+
ctx.tables.map((t) => [t.name, new Set(t.columns.map((c) => c.name))])
|
|
1320
|
+
);
|
|
1321
|
+
for (const rel of ctx.relationships) {
|
|
1322
|
+
const sourceColumns = tableColumnSets.get(rel.table);
|
|
1323
|
+
const targetColumns = tableColumnSets.get(rel.referenced_table);
|
|
1324
|
+
if (sourceColumns && rel.from.some((column2) => !sourceColumns.has(column2))) {
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
if (targetColumns && rel.to.some((column2) => !targetColumns.has(column2))) {
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
const sourceTable = tableMap.get(rel.table);
|
|
1331
|
+
const targetTable = tableMap.get(rel.referenced_table);
|
|
1332
|
+
fragments.push(
|
|
1333
|
+
this.#relationshipToFragment(rel, sourceTable, targetTable)
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
fragments.push(...ctx.fragments ?? []);
|
|
1337
|
+
return fragments;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Convert a Table to a table fragment with nested column, index, and constraint fragments.
|
|
1341
|
+
*/
|
|
1342
|
+
#tableToFragment(t) {
|
|
1343
|
+
const pkConstraint = t.constraints?.find((c) => c.type === "PRIMARY_KEY");
|
|
1344
|
+
const pkColumns = new Set(pkConstraint?.columns ?? []);
|
|
1345
|
+
const notNullColumns = new Set(
|
|
1346
|
+
t.constraints?.filter((c) => c.type === "NOT_NULL").flatMap((c) => c.columns ?? []) ?? []
|
|
1347
|
+
);
|
|
1348
|
+
const defaultByColumn = /* @__PURE__ */ new Map();
|
|
1349
|
+
for (const c of t.constraints?.filter((c2) => c2.type === "DEFAULT") ?? []) {
|
|
1350
|
+
for (const col of c.columns ?? []) {
|
|
1351
|
+
if (c.defaultValue != null) {
|
|
1352
|
+
defaultByColumn.set(col, c.defaultValue);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
const uniqueColumns = new Set(
|
|
1357
|
+
t.constraints?.filter((c) => c.type === "UNIQUE" && c.columns?.length === 1).flatMap((c) => c.columns ?? []) ?? []
|
|
1358
|
+
);
|
|
1359
|
+
const fkByColumn = /* @__PURE__ */ new Map();
|
|
1360
|
+
for (const c of t.constraints?.filter((c2) => c2.type === "FOREIGN_KEY") ?? []) {
|
|
1361
|
+
const cols = c.columns ?? [];
|
|
1362
|
+
const refCols = c.referencedColumns ?? [];
|
|
1363
|
+
for (let i = 0; i < cols.length; i++) {
|
|
1364
|
+
const refCol = refCols[i] ?? refCols[0] ?? cols[i];
|
|
1365
|
+
fkByColumn.set(cols[i], `${c.referencedTable}.${refCol}`);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const columnFragments = t.columns.map(
|
|
1369
|
+
(col) => column({
|
|
1370
|
+
name: col.name,
|
|
1371
|
+
type: col.type,
|
|
1372
|
+
pk: pkColumns.has(col.name) || void 0,
|
|
1373
|
+
fk: fkByColumn.get(col.name),
|
|
1374
|
+
unique: uniqueColumns.has(col.name) || void 0,
|
|
1375
|
+
notNull: notNullColumns.has(col.name) || void 0,
|
|
1376
|
+
default: defaultByColumn.get(col.name),
|
|
1377
|
+
indexed: col.isIndexed || void 0,
|
|
1378
|
+
values: col.values,
|
|
1379
|
+
stats: col.stats
|
|
1380
|
+
})
|
|
1381
|
+
);
|
|
1382
|
+
const presentColumns = new Set(t.columns.map((c) => c.name));
|
|
1383
|
+
const indexFragments = (t.indexes ?? []).filter(
|
|
1384
|
+
(idx) => idx.columns.every((column2) => presentColumns.has(column2))
|
|
1385
|
+
).map(
|
|
1386
|
+
(idx) => index({
|
|
1387
|
+
name: idx.name,
|
|
1388
|
+
columns: idx.columns,
|
|
1389
|
+
unique: idx.unique,
|
|
1390
|
+
type: idx.type
|
|
1391
|
+
})
|
|
1392
|
+
);
|
|
1393
|
+
const constraintFragments = (t.constraints ?? []).filter(
|
|
1394
|
+
(c) => c.type === "CHECK" || c.type === "UNIQUE" && (c.columns?.length ?? 0) > 1
|
|
1395
|
+
).filter(
|
|
1396
|
+
(c) => !c.columns?.length || c.columns.every((column2) => presentColumns.has(column2))
|
|
1397
|
+
).map(
|
|
1398
|
+
(c) => constraint({
|
|
1399
|
+
name: c.name,
|
|
1400
|
+
type: c.type,
|
|
1401
|
+
columns: c.columns,
|
|
1402
|
+
definition: c.definition
|
|
1403
|
+
})
|
|
1404
|
+
);
|
|
1405
|
+
return table({
|
|
1406
|
+
name: t.name,
|
|
1407
|
+
schema: t.schema,
|
|
1408
|
+
rowCount: t.rowCount,
|
|
1409
|
+
sizeHint: t.sizeHint,
|
|
1410
|
+
columns: columnFragments,
|
|
1411
|
+
indexes: indexFragments.length > 0 ? indexFragments : void 0,
|
|
1412
|
+
constraints: constraintFragments.length > 0 ? constraintFragments : void 0
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Convert a View to a view fragment with nested column fragments.
|
|
1417
|
+
*/
|
|
1418
|
+
#viewToFragment(v) {
|
|
1419
|
+
const columnFragments = v.columns.map(
|
|
1420
|
+
(col) => column({
|
|
1421
|
+
name: col.name,
|
|
1422
|
+
type: col.type,
|
|
1423
|
+
values: col.values,
|
|
1424
|
+
stats: col.stats
|
|
1425
|
+
})
|
|
1426
|
+
);
|
|
1427
|
+
return view({
|
|
1428
|
+
name: v.name,
|
|
1429
|
+
schema: v.schema,
|
|
1430
|
+
columns: columnFragments,
|
|
1431
|
+
definition: v.definition
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Convert a Relationship to a relationship fragment.
|
|
1436
|
+
* Infers cardinality from row counts if available.
|
|
1437
|
+
*/
|
|
1438
|
+
#relationshipToFragment(rel, sourceTable, targetTable) {
|
|
1439
|
+
const sourceCount = sourceTable?.rowCount;
|
|
1440
|
+
const targetCount = targetTable?.rowCount;
|
|
1441
|
+
let cardinality;
|
|
1442
|
+
if (sourceCount != null && targetCount != null && targetCount > 0) {
|
|
1443
|
+
const ratio = sourceCount / targetCount;
|
|
1444
|
+
if (ratio > 5) {
|
|
1445
|
+
cardinality = "many-to-one";
|
|
1446
|
+
} else if (ratio < 1.2 && ratio > 0.8) {
|
|
1447
|
+
cardinality = "one-to-one";
|
|
1448
|
+
} else if (ratio < 0.2) {
|
|
1449
|
+
cardinality = "one-to-many";
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return relationship({
|
|
1453
|
+
from: { table: rel.table, columns: rel.from },
|
|
1454
|
+
to: { table: rel.referenced_table, columns: rel.to },
|
|
1455
|
+
cardinality
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
format(sql) {
|
|
1459
|
+
const decoded = this.#decodeShellEscapes(sql);
|
|
1460
|
+
try {
|
|
1461
|
+
return formatSql(decoded, { language: this.formatterLanguage });
|
|
1462
|
+
} catch {
|
|
1463
|
+
return decoded;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
#decodeShellEscapes(sql) {
|
|
1467
|
+
return sql.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\([().*$])/g, "$1").replace(/\\(?=$)/gm, "");
|
|
1468
|
+
}
|
|
1469
|
+
#cachedAllowedEntities = null;
|
|
1470
|
+
async #resolveScope() {
|
|
1471
|
+
if (this.#cachedAllowedEntities) return this.#cachedAllowedEntities;
|
|
1472
|
+
this.#cachedAllowedEntities = await this.resolveAllowedEntities();
|
|
1473
|
+
return this.#cachedAllowedEntities;
|
|
1474
|
+
}
|
|
1475
|
+
async #analyzePolicy(sql) {
|
|
1476
|
+
return this.#policyAnalyzer.analyze(sql, {
|
|
1477
|
+
resolveAllowedEntities: () => this.#resolveScope()
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
async validate(sql) {
|
|
1481
|
+
const decoded = this.#decodeShellEscapes(sql);
|
|
1482
|
+
const violation = await this.#analyzePolicy(decoded);
|
|
1483
|
+
if (violation?.kind === "read-only") return violation.message;
|
|
1484
|
+
if (violation?.kind === "scope") return JSON.stringify(violation.payload);
|
|
1485
|
+
return this.validateImpl(decoded);
|
|
1486
|
+
}
|
|
1487
|
+
async execute(sql) {
|
|
1488
|
+
return this.executeImpl(await this.enforceExecutionPolicy(sql));
|
|
1489
|
+
}
|
|
1490
|
+
async enforceExecutionPolicy(sql) {
|
|
1491
|
+
const decoded = this.#decodeShellEscapes(sql);
|
|
1492
|
+
const violation = await this.#analyzePolicy(decoded);
|
|
1493
|
+
if (violation?.kind === "read-only") {
|
|
1494
|
+
throw new SQLReadOnlyError(violation.message);
|
|
1495
|
+
}
|
|
1496
|
+
if (violation?.kind === "scope") {
|
|
1497
|
+
throw new SQLScopeError(violation.payload);
|
|
1498
|
+
}
|
|
1499
|
+
return decoded;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Convert unknown database value to number.
|
|
1503
|
+
* Handles number, bigint, and string types.
|
|
1504
|
+
*/
|
|
1505
|
+
toNumber(value) {
|
|
1506
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1507
|
+
return value;
|
|
1508
|
+
}
|
|
1509
|
+
if (typeof value === "bigint") {
|
|
1510
|
+
return Number(value);
|
|
1511
|
+
}
|
|
1512
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1513
|
+
const parsed = Number(value);
|
|
1514
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1515
|
+
}
|
|
1516
|
+
return void 0;
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Parse a potentially qualified table name into schema and table parts.
|
|
1520
|
+
*/
|
|
1521
|
+
parseTableName(name) {
|
|
1522
|
+
if (name.includes(".")) {
|
|
1523
|
+
const [schema, ...rest] = name.split(".");
|
|
1524
|
+
return { schema, table: rest.join(".") };
|
|
1525
|
+
}
|
|
1526
|
+
return { schema: this.defaultSchema ?? "", table: name };
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Escape a string value for use in SQL string literals (single quotes).
|
|
1530
|
+
* Used in WHERE clauses like: WHERE name = '${escapeString(value)}'
|
|
1531
|
+
*/
|
|
1532
|
+
escapeString(value) {
|
|
1533
|
+
return value.replace(/'/g, "''");
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Build a SQL filter clause to include/exclude schemas.
|
|
1537
|
+
* @param columnName - The schema column name (e.g., 'TABLE_SCHEMA')
|
|
1538
|
+
* @param allowedSchemas - If provided, filter to these schemas only
|
|
1539
|
+
*/
|
|
1540
|
+
buildSchemaFilter(columnName, allowedSchemas) {
|
|
1541
|
+
if (allowedSchemas && allowedSchemas.length > 0) {
|
|
1542
|
+
const values = allowedSchemas.map((s) => `'${this.escapeString(s)}'`).join(", ");
|
|
1543
|
+
return `AND ${columnName} IN (${values})`;
|
|
1544
|
+
}
|
|
1545
|
+
if (this.systemSchemas.length > 0) {
|
|
1546
|
+
const values = this.systemSchemas.map((s) => `'${this.escapeString(s)}'`).join(", ");
|
|
1547
|
+
return `AND ${columnName} NOT IN (${values})`;
|
|
1548
|
+
}
|
|
1549
|
+
return "";
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
// packages/text2sql/src/lib/sql-scope-error.ts
|
|
1554
|
+
function buildScopeParseErrorPayload(sql, parserDialect, error) {
|
|
1555
|
+
const parserError = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
1556
|
+
return {
|
|
1557
|
+
error: `SQL scope analysis failed before validation/execution: ${parserError}`,
|
|
1558
|
+
error_type: "SQL_SCOPE_PARSE_ERROR",
|
|
1559
|
+
suggestion: "Rewrite the query into simpler SQL that can be analyzed safely, or extend parser coverage for this dialect feature.",
|
|
1560
|
+
sql_attempted: sql,
|
|
1561
|
+
parser_dialect: parserDialect,
|
|
1562
|
+
parser_error: parserError
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// packages/text2sql/src/lib/adapters/duckdb/duckdb.sql-policy.ts
|
|
1567
|
+
var READ_ONLY_MESSAGE = "only SELECT queries allowed";
|
|
1568
|
+
var SAFE_TABLE_FUNCTIONS = /* @__PURE__ */ new Set(["generate_series", "range", "unnest"]);
|
|
1569
|
+
var DuckDBSqlPolicyAnalyzer = class {
|
|
1570
|
+
#query;
|
|
1571
|
+
#namespace;
|
|
1572
|
+
constructor(query) {
|
|
1573
|
+
this.#query = query;
|
|
1574
|
+
}
|
|
1575
|
+
async analyze(sql, context) {
|
|
1576
|
+
try {
|
|
1577
|
+
const namespace = await (this.#namespace ??= this.#loadNamespace());
|
|
1578
|
+
const ast = await this.#serialize(sql);
|
|
1579
|
+
const analysis = analyzeAst(ast, namespace);
|
|
1580
|
+
if (analysis.tableFunctions.some(
|
|
1581
|
+
(name) => !SAFE_TABLE_FUNCTIONS.has(caseFold(name))
|
|
1582
|
+
) || await this.#hasUnsafeFunction(analysis.functions)) {
|
|
1583
|
+
return { kind: "read-only", message: READ_ONLY_MESSAGE };
|
|
1584
|
+
}
|
|
1585
|
+
const allowedEntities = await context.resolveAllowedEntities();
|
|
1586
|
+
const allowed = new Set(
|
|
1587
|
+
allowedEntities.map(
|
|
1588
|
+
(name) => caseFold(qualifyAllowedRelation(name, namespace))
|
|
1589
|
+
)
|
|
1590
|
+
);
|
|
1591
|
+
const rejected = analysis.relations.filter(
|
|
1592
|
+
(relation) => !allowed.has(caseFold(relation))
|
|
1593
|
+
);
|
|
1594
|
+
return rejected.length > 0 ? {
|
|
1595
|
+
kind: "scope",
|
|
1596
|
+
payload: buildOutOfScopePayload(sql, rejected, [
|
|
1597
|
+
...allowedEntities
|
|
1598
|
+
])
|
|
1599
|
+
} : null;
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
if (isReadOnlySerializationError(error)) {
|
|
1602
|
+
return { kind: "read-only", message: READ_ONLY_MESSAGE };
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
kind: "scope",
|
|
1606
|
+
payload: buildScopeParseErrorPayload(sql, "duckdb", error)
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
async #loadNamespace() {
|
|
1611
|
+
const rows = readRows(
|
|
1612
|
+
await this.#query(`
|
|
1613
|
+
SELECT current_database() AS catalog, current_schema() AS schema
|
|
1614
|
+
`)
|
|
1615
|
+
);
|
|
1616
|
+
const row = rows[0];
|
|
1617
|
+
if (rows.length !== 1 || typeof row?.catalog !== "string" || typeof row.schema !== "string") {
|
|
1618
|
+
throw new Error(
|
|
1619
|
+
"DuckDB current namespace returned an unknown row shape."
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
return { catalog: row.catalog, schema: row.schema };
|
|
1623
|
+
}
|
|
1624
|
+
async #serialize(sql) {
|
|
1625
|
+
const literal = sql.replaceAll("'", "''");
|
|
1626
|
+
const rows = readRows(
|
|
1627
|
+
await this.#query(`
|
|
1628
|
+
SELECT json_serialize_sql(
|
|
1629
|
+
'${literal}',
|
|
1630
|
+
skip_empty := true,
|
|
1631
|
+
skip_null := true
|
|
1632
|
+
) AS ast
|
|
1633
|
+
`)
|
|
1634
|
+
);
|
|
1635
|
+
if (rows.length !== 1 || typeof rows[0]?.ast !== "string") {
|
|
1636
|
+
throw new Error(
|
|
1637
|
+
"DuckDB json_serialize_sql returned an unknown row shape."
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
const result = JSON.parse(rows[0].ast);
|
|
1641
|
+
if (!isRecord(result)) {
|
|
1642
|
+
throw new Error("DuckDB json_serialize_sql returned invalid JSON.");
|
|
1643
|
+
}
|
|
1644
|
+
if (result.error === true) {
|
|
1645
|
+
const message = typeof result.error_message === "string" ? result.error_message : "DuckDB could not serialize the SQL statement.";
|
|
1646
|
+
if (result.error_type === "not implemented" && message === "Only SELECT statements can be serialized to json!") {
|
|
1647
|
+
throw new DuckDBSerializationError(message);
|
|
1648
|
+
}
|
|
1649
|
+
throw new Error(
|
|
1650
|
+
`DuckDB rejected the SQL during AST analysis: ${message}`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
if (result.error !== false || !Array.isArray(result.statements) || result.statements.length !== 1) {
|
|
1654
|
+
throw new Error("DuckDB policy requires exactly one SELECT statement.");
|
|
1655
|
+
}
|
|
1656
|
+
return result.statements[0];
|
|
1657
|
+
}
|
|
1658
|
+
async #hasUnsafeFunction(functionNames) {
|
|
1659
|
+
const names = [...new Set(functionNames.map(caseFold))];
|
|
1660
|
+
if (names.length === 0) return false;
|
|
1661
|
+
const literals = names.map((name) => `'${name.replaceAll("'", "''")}'`).join(", ");
|
|
1662
|
+
const rows = readRows(
|
|
1663
|
+
await this.#query(`
|
|
1664
|
+
SELECT function_name, has_side_effects, internal
|
|
1665
|
+
FROM duckdb_functions()
|
|
1666
|
+
WHERE lower(function_name) IN (${literals})
|
|
1667
|
+
`)
|
|
1668
|
+
);
|
|
1669
|
+
return rows.some(
|
|
1670
|
+
(row) => row.has_side_effects === true || row.internal === false
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
function analyzeAst(ast, namespace) {
|
|
1675
|
+
const cteNames = /* @__PURE__ */ new Set();
|
|
1676
|
+
const relations = /* @__PURE__ */ new Set();
|
|
1677
|
+
const functions = /* @__PURE__ */ new Set();
|
|
1678
|
+
const tableFunctions = /* @__PURE__ */ new Set();
|
|
1679
|
+
visit(ast, (node) => {
|
|
1680
|
+
if (Array.isArray(node.map)) {
|
|
1681
|
+
for (const entry of node.map) {
|
|
1682
|
+
if (isRecord(entry) && typeof entry.key === "string") {
|
|
1683
|
+
cteNames.add(caseFold(entry.key));
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
visit(ast, (node) => {
|
|
1689
|
+
if (node.type === "BASE_TABLE" && typeof node.table_name === "string") {
|
|
1690
|
+
const unqualifiedCte = node.catalog_name === void 0 && node.schema_name === void 0 && cteNames.has(caseFold(node.table_name));
|
|
1691
|
+
if (!unqualifiedCte) {
|
|
1692
|
+
relations.add(
|
|
1693
|
+
formatDuckDBIdentifierPath([
|
|
1694
|
+
typeof node.catalog_name === "string" ? node.catalog_name : namespace.catalog,
|
|
1695
|
+
typeof node.schema_name === "string" ? node.schema_name : namespace.schema,
|
|
1696
|
+
node.table_name
|
|
1697
|
+
])
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
if (node.class === "FUNCTION" && typeof node.function_name === "string") {
|
|
1702
|
+
functions.add(node.function_name);
|
|
1703
|
+
}
|
|
1704
|
+
if (node.type === "TABLE_FUNCTION") {
|
|
1705
|
+
const fn = node.function;
|
|
1706
|
+
if (!isRecord(fn) || typeof fn.function_name !== "string") {
|
|
1707
|
+
throw new Error("DuckDB table function has an unknown AST shape.");
|
|
1708
|
+
}
|
|
1709
|
+
tableFunctions.add(fn.function_name);
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
return {
|
|
1713
|
+
relations: [...relations],
|
|
1714
|
+
functions: [...functions],
|
|
1715
|
+
tableFunctions: [...tableFunctions]
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function visit(value, callback) {
|
|
1719
|
+
if (Array.isArray(value)) {
|
|
1720
|
+
for (const item of value) visit(item, callback);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
if (!isRecord(value)) return;
|
|
1724
|
+
callback(value);
|
|
1725
|
+
for (const child of Object.values(value)) visit(child, callback);
|
|
1726
|
+
}
|
|
1727
|
+
function qualifyAllowedRelation(name, namespace) {
|
|
1728
|
+
const relation = parseDuckDBRelationName(name);
|
|
1729
|
+
return formatDuckDBIdentifierPath([
|
|
1730
|
+
relation.catalog ?? namespace.catalog,
|
|
1731
|
+
relation.schema ?? namespace.schema,
|
|
1732
|
+
relation.table
|
|
1733
|
+
]);
|
|
1734
|
+
}
|
|
1735
|
+
function readRows(result) {
|
|
1736
|
+
const rows = Array.isArray(result) ? result : isRecord(result) && Array.isArray(result.data) ? result.data : isRecord(result) && Array.isArray(result.rows) ? result.rows : void 0;
|
|
1737
|
+
if (!rows || rows.some((row) => !isRecord(row))) {
|
|
1738
|
+
throw new Error(
|
|
1739
|
+
"DuckDB execute() must return row objects as an array, { data }, or { rows }."
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
return rows;
|
|
1743
|
+
}
|
|
1744
|
+
function buildOutOfScopePayload(sql, referencedEntities, allowedEntities) {
|
|
1745
|
+
return {
|
|
1746
|
+
error: `Query references entities outside grounded scope: ${referencedEntities.join(", ")}`,
|
|
1747
|
+
error_type: "OUT_OF_SCOPE",
|
|
1748
|
+
suggestion: "Restrict the query to grounded tables/views or expand grounding to include the referenced entities.",
|
|
1749
|
+
sql_attempted: sql,
|
|
1750
|
+
referenced_entities: referencedEntities,
|
|
1751
|
+
allowed_entities: allowedEntities
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
var DuckDBSerializationError = class extends Error {
|
|
1755
|
+
};
|
|
1756
|
+
function isReadOnlySerializationError(error) {
|
|
1757
|
+
return error instanceof DuckDBSerializationError;
|
|
1758
|
+
}
|
|
1759
|
+
function caseFold(value) {
|
|
1760
|
+
return value.toLocaleLowerCase("en-US");
|
|
1761
|
+
}
|
|
1762
|
+
function isRecord(value) {
|
|
1763
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// packages/text2sql/src/lib/adapters/duckdb/duckdb.ts
|
|
1767
|
+
var DuckDB = class extends Adapter {
|
|
1768
|
+
#options;
|
|
1769
|
+
#namespace;
|
|
1770
|
+
grounding;
|
|
1771
|
+
defaultSchema = "main";
|
|
1772
|
+
systemSchemas = ["information_schema", "pg_catalog"];
|
|
1773
|
+
formatterLanguage = "duckdb";
|
|
1774
|
+
constructor(options) {
|
|
1775
|
+
if (!options || typeof options.execute !== "function") {
|
|
1776
|
+
throw new Error("DuckDB adapter requires an execute(sql) function.");
|
|
1777
|
+
}
|
|
1778
|
+
validateScopeOption("catalogs", options.catalogs);
|
|
1779
|
+
validateScopeOption("schemas", options.schemas);
|
|
1780
|
+
super(new DuckDBSqlPolicyAnalyzer(options.execute));
|
|
1781
|
+
this.#options = options;
|
|
1782
|
+
this.grounding = options.grounding ?? [];
|
|
1783
|
+
}
|
|
1784
|
+
get catalogs() {
|
|
1785
|
+
return this.#options.catalogs;
|
|
1786
|
+
}
|
|
1787
|
+
get schemas() {
|
|
1788
|
+
return this.#options.schemas;
|
|
1789
|
+
}
|
|
1790
|
+
async executeImpl(sql) {
|
|
1791
|
+
return rowsFromResult(await this.#options.execute(sql));
|
|
1792
|
+
}
|
|
1793
|
+
async validateImpl(sql) {
|
|
1794
|
+
try {
|
|
1795
|
+
if (this.#options.validate) return await this.#options.validate(sql);
|
|
1796
|
+
await this.#options.execute(`EXPLAIN ${sql}`);
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1799
|
+
return JSON.stringify({
|
|
1800
|
+
error: message,
|
|
1801
|
+
error_type: "DUCKDB_ERROR",
|
|
1802
|
+
suggestion: "Review the DuckDB syntax and verify referenced catalogs, schemas, tables, columns, and functions.",
|
|
1803
|
+
sql_attempted: sql
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
async runQuery(sql) {
|
|
1808
|
+
return rowsFromResult(await this.#options.execute(sql));
|
|
1809
|
+
}
|
|
1810
|
+
quoteIdentifier(name) {
|
|
1811
|
+
return formatDuckDBIdentifierPath([name]);
|
|
1812
|
+
}
|
|
1813
|
+
escape(value) {
|
|
1814
|
+
return value.replaceAll('"', '""');
|
|
1815
|
+
}
|
|
1816
|
+
buildSampleRowsQuery(tableName, columns, limit) {
|
|
1817
|
+
const relation = parseDuckDBRelationName(tableName);
|
|
1818
|
+
const path = [relation.catalog, relation.schema, relation.table].filter(
|
|
1819
|
+
(part) => part !== void 0
|
|
1820
|
+
);
|
|
1821
|
+
const projection = columns?.length ? columns.map((column2) => this.quoteIdentifier(column2)).join(", ") : "*";
|
|
1822
|
+
return `SELECT ${projection} FROM ${formatDuckDBIdentifierPath(path)} LIMIT ${limit}`;
|
|
1823
|
+
}
|
|
1824
|
+
async currentNamespace() {
|
|
1825
|
+
return this.#namespace ??= this.#loadNamespace();
|
|
1826
|
+
}
|
|
1827
|
+
async resolveRelationName(name) {
|
|
1828
|
+
const relation = parseDuckDBRelationName(name);
|
|
1829
|
+
const current = await this.currentNamespace();
|
|
1830
|
+
return {
|
|
1831
|
+
catalog: relation.catalog ?? current.catalog,
|
|
1832
|
+
schema: relation.schema ?? current.schema,
|
|
1833
|
+
table: relation.table
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
formatRelationName(catalog, schema, table2) {
|
|
1837
|
+
return formatDuckDBIdentifierPath([catalog, schema, table2]);
|
|
1838
|
+
}
|
|
1839
|
+
quoteRelationName(name) {
|
|
1840
|
+
const relation = parseDuckDBRelationName(name);
|
|
1841
|
+
return formatDuckDBIdentifierPath(
|
|
1842
|
+
[relation.catalog, relation.schema, relation.table].filter(
|
|
1843
|
+
(part) => part !== void 0
|
|
1844
|
+
)
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
async scopedNamespaces() {
|
|
1848
|
+
const current = await this.currentNamespace();
|
|
1849
|
+
return {
|
|
1850
|
+
catalogs: [...this.catalogs ?? [current.catalog]],
|
|
1851
|
+
schemas: [...this.schemas ?? [current.schema]]
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
async #loadNamespace() {
|
|
1855
|
+
const rows = await this.runQuery(`
|
|
1856
|
+
SELECT current_database() AS catalog, current_schema() AS schema
|
|
1857
|
+
`);
|
|
1858
|
+
const row = rows[0];
|
|
1859
|
+
if (rows.length !== 1 || typeof row?.catalog !== "string" || typeof row.schema !== "string") {
|
|
1860
|
+
throw new Error(
|
|
1861
|
+
"DuckDB current namespace returned an unknown row shape."
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
return { catalog: row.catalog, schema: row.schema };
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
function validateScopeOption(name, values) {
|
|
1868
|
+
if (values !== void 0 && (values.length === 0 || values.some((value) => !value))) {
|
|
1869
|
+
throw new Error(`DuckDB ${name} must contain at least one non-empty name.`);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
function rowsFromResult(result) {
|
|
1873
|
+
if (Array.isArray(result)) return result;
|
|
1874
|
+
if (result && typeof result === "object") {
|
|
1875
|
+
const candidate = result;
|
|
1876
|
+
if (Array.isArray(candidate.data)) return candidate.data;
|
|
1877
|
+
if (Array.isArray(candidate.rows)) return candidate.rows;
|
|
1878
|
+
}
|
|
1879
|
+
throw new Error(
|
|
1880
|
+
"DuckDB execute() must return an array of rows, { data: rows }, or { rows }."
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// packages/text2sql/src/lib/adapters/duckdb/index.ts
|
|
1885
|
+
function tables(config = {}) {
|
|
1886
|
+
return (adapter) => new DuckDBTableGrounding(adapter, config);
|
|
1887
|
+
}
|
|
1888
|
+
function info(config = {}) {
|
|
1889
|
+
return (adapter) => new DuckDBInfoGrounding(adapter, config);
|
|
1890
|
+
}
|
|
1891
|
+
function views(config = {}) {
|
|
1892
|
+
return (adapter) => new DuckDBViewGrounding(adapter, config);
|
|
1893
|
+
}
|
|
1894
|
+
function constraints(config = {}) {
|
|
1895
|
+
return (adapter) => new DuckDBConstraintGrounding(adapter, config);
|
|
1896
|
+
}
|
|
1897
|
+
function indexes(config = {}) {
|
|
1898
|
+
return (adapter) => new DuckDBIndexesGrounding(adapter, config);
|
|
1899
|
+
}
|
|
1900
|
+
function rowCount(config = {}) {
|
|
1901
|
+
return (adapter) => new DuckDBRowCountGrounding(adapter, config);
|
|
1902
|
+
}
|
|
1903
|
+
function columnStats(config = {}) {
|
|
1904
|
+
return (adapter) => new DuckDBColumnStatsGrounding(adapter, config);
|
|
1905
|
+
}
|
|
1906
|
+
function columnValues(config = {}) {
|
|
1907
|
+
return (adapter) => new DuckDBColumnValuesGrounding(adapter, config);
|
|
1908
|
+
}
|
|
1909
|
+
var duckdb_default = {
|
|
1910
|
+
tables,
|
|
1911
|
+
info,
|
|
1912
|
+
views,
|
|
1913
|
+
constraints,
|
|
1914
|
+
indexes,
|
|
1915
|
+
rowCount,
|
|
1916
|
+
columnStats,
|
|
1917
|
+
columnValues,
|
|
1918
|
+
DuckDB
|
|
1919
|
+
};
|
|
1920
|
+
export {
|
|
1921
|
+
DuckDB,
|
|
1922
|
+
DuckDBSqlPolicyAnalyzer,
|
|
1923
|
+
columnStats,
|
|
1924
|
+
columnValues,
|
|
1925
|
+
constraints,
|
|
1926
|
+
duckdb_default as default,
|
|
1927
|
+
indexes,
|
|
1928
|
+
info,
|
|
1929
|
+
rowCount,
|
|
1930
|
+
tables,
|
|
1931
|
+
views
|
|
1932
|
+
};
|
|
1933
|
+
//# sourceMappingURL=index.js.map
|