@deepagents/text2sql 4.3.0 → 6.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/CLICKHOUSE-DEPLOYMENT.md +181 -0
- package/README.md +33 -7
- package/dist/bin/sql.js +33 -51
- package/dist/index.js +68 -299
- package/dist/index.js.map +4 -4
- package/dist/lib/adapters/adapter.d.ts +8 -1
- package/dist/lib/adapters/adapter.d.ts.map +1 -1
- package/dist/lib/adapters/bigquery/bigquery.d.ts.map +1 -1
- package/dist/lib/adapters/bigquery/bigquery.sql-policy.d.ts +9 -0
- package/dist/lib/adapters/bigquery/bigquery.sql-policy.d.ts.map +1 -0
- package/dist/lib/adapters/bigquery/index.d.ts +1 -0
- package/dist/lib/adapters/bigquery/index.d.ts.map +1 -1
- package/dist/lib/adapters/bigquery/index.js +303 -259
- package/dist/lib/adapters/bigquery/index.js.map +4 -4
- package/dist/lib/adapters/clickhouse/clickhouse.d.ts +22 -0
- package/dist/lib/adapters/clickhouse/clickhouse.d.ts.map +1 -0
- package/dist/lib/adapters/clickhouse/clickhouse.sql-policy.d.ts +8 -0
- package/dist/lib/adapters/clickhouse/clickhouse.sql-policy.d.ts.map +1 -0
- package/dist/lib/adapters/clickhouse/constraint.clickhouse.grounding.d.ts +8 -0
- package/dist/lib/adapters/clickhouse/constraint.clickhouse.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/clickhouse/index.d.ts +33 -0
- package/dist/lib/adapters/clickhouse/index.d.ts.map +1 -0
- package/dist/lib/adapters/clickhouse/index.js +1544 -0
- package/dist/lib/adapters/clickhouse/index.js.map +7 -0
- package/dist/lib/adapters/clickhouse/indexes.clickhouse.grounding.d.ts +8 -0
- package/dist/lib/adapters/clickhouse/indexes.clickhouse.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/clickhouse/info.clickhouse.grounding.d.ts +8 -0
- package/dist/lib/adapters/clickhouse/info.clickhouse.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/clickhouse/row-count.clickhouse.grounding.d.ts +8 -0
- package/dist/lib/adapters/clickhouse/row-count.clickhouse.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/clickhouse/table.clickhouse.grounding.d.ts +11 -0
- package/dist/lib/adapters/clickhouse/table.clickhouse.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/clickhouse/view.clickhouse.grounding.d.ts +9 -0
- package/dist/lib/adapters/clickhouse/view.clickhouse.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/groundings/abstract.grounding.d.ts +11 -16
- package/dist/lib/adapters/groundings/abstract.grounding.d.ts.map +1 -1
- package/dist/lib/adapters/groundings/index.js +16 -0
- package/dist/lib/adapters/groundings/index.js.map +2 -2
- package/dist/lib/adapters/groundings/table.grounding.d.ts +1 -0
- package/dist/lib/adapters/groundings/table.grounding.d.ts.map +1 -1
- package/dist/lib/adapters/groundings/view.grounding.d.ts +1 -0
- package/dist/lib/adapters/groundings/view.grounding.d.ts.map +1 -1
- package/dist/lib/adapters/mysql/index.d.ts +1 -0
- package/dist/lib/adapters/mysql/index.d.ts.map +1 -1
- package/dist/lib/adapters/mysql/index.js +317 -259
- package/dist/lib/adapters/mysql/index.js.map +4 -4
- package/dist/lib/adapters/mysql/mysql.d.ts.map +1 -1
- package/dist/lib/adapters/mysql/mysql.sql-policy.d.ts +11 -0
- package/dist/lib/adapters/mysql/mysql.sql-policy.d.ts.map +1 -0
- package/dist/lib/adapters/parser-sql-policy.d.ts +23 -0
- package/dist/lib/adapters/parser-sql-policy.d.ts.map +1 -0
- package/dist/lib/adapters/postgres/index.d.ts +1 -0
- package/dist/lib/adapters/postgres/index.d.ts.map +1 -1
- package/dist/lib/adapters/postgres/index.js +323 -259
- package/dist/lib/adapters/postgres/index.js.map +4 -4
- package/dist/lib/adapters/postgres/postgres.d.ts.map +1 -1
- package/dist/lib/adapters/postgres/postgres.sql-policy.d.ts +10 -0
- package/dist/lib/adapters/postgres/postgres.sql-policy.d.ts.map +1 -0
- package/dist/lib/adapters/spreadsheet/index.js +301 -259
- package/dist/lib/adapters/spreadsheet/index.js.map +4 -4
- package/dist/lib/adapters/spreadsheet/parser.d.ts.map +1 -1
- package/dist/lib/adapters/sql-policy.d.ts +19 -0
- package/dist/lib/adapters/sql-policy.d.ts.map +1 -0
- package/dist/lib/adapters/sqlite/index.d.ts +1 -0
- package/dist/lib/adapters/sqlite/index.d.ts.map +1 -1
- package/dist/lib/adapters/sqlite/index.js +303 -259
- package/dist/lib/adapters/sqlite/index.js.map +4 -4
- package/dist/lib/adapters/sqlite/sqlite.d.ts.map +1 -1
- package/dist/lib/adapters/sqlite/sqlite.sql-policy.d.ts +8 -0
- package/dist/lib/adapters/sqlite/sqlite.sql-policy.d.ts.map +1 -0
- package/dist/lib/adapters/sqlserver/index.d.ts +1 -0
- package/dist/lib/adapters/sqlserver/index.d.ts.map +1 -1
- package/dist/lib/adapters/sqlserver/index.js +310 -259
- package/dist/lib/adapters/sqlserver/index.js.map +4 -4
- package/dist/lib/adapters/sqlserver/sqlserver.d.ts.map +1 -1
- package/dist/lib/adapters/sqlserver/sqlserver.sql-policy.d.ts +11 -0
- package/dist/lib/adapters/sqlserver/sqlserver.sql-policy.d.ts.map +1 -0
- package/dist/lib/agents/exceptions.d.ts +1 -1
- package/dist/lib/agents/sql-transform-plugins.d.ts +1 -2
- package/dist/lib/agents/sql-transform-plugins.d.ts.map +1 -1
- package/dist/lib/agents/sql.agent.d.ts.map +1 -1
- package/dist/lib/sql-command.d.ts.map +1 -1
- package/dist/lib/sql-hooks.d.ts +5 -1
- package/dist/lib/sql-hooks.d.ts.map +1 -1
- package/dist/lib/sql-meta-hook.d.ts +6 -2
- package/dist/lib/sql-meta-hook.d.ts.map +1 -1
- package/dist/lib/sql-scope-error.d.ts +3 -0
- package/dist/lib/sql-scope-error.d.ts.map +1 -0
- package/dist/lib/sql-validate-reminder.d.ts +3 -3
- package/dist/lib/sql-validate-reminder.d.ts.map +1 -1
- package/dist/lib/synthesis/index.js +32 -38
- package/dist/lib/synthesis/index.js.map +4 -4
- package/package.json +30 -24
- package/dist/lib/adapters/runtime-scope.d.ts +0 -16
- package/dist/lib/adapters/runtime-scope.d.ts.map +0 -1
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { format as formatSql } from "sql-formatter";
|
|
3
3
|
|
|
4
4
|
// packages/text2sql/src/lib/agents/exceptions.ts
|
|
5
|
-
var sqlValidationMarker = Symbol("SQLValidationError");
|
|
6
|
-
var unanswerableSqlMarker = Symbol("UnanswerableSQLError");
|
|
7
|
-
var sqlScopeMarker = Symbol("SQLScopeError");
|
|
8
|
-
var sqlReadOnlyMarker = Symbol("SQLReadOnlyError");
|
|
5
|
+
var sqlValidationMarker = /* @__PURE__ */ Symbol("SQLValidationError");
|
|
6
|
+
var unanswerableSqlMarker = /* @__PURE__ */ Symbol("UnanswerableSQLError");
|
|
7
|
+
var sqlScopeMarker = /* @__PURE__ */ Symbol("SQLScopeError");
|
|
8
|
+
var sqlReadOnlyMarker = /* @__PURE__ */ Symbol("SQLReadOnlyError");
|
|
9
9
|
var SQLValidationError = class _SQLValidationError extends Error {
|
|
10
10
|
[sqlValidationMarker];
|
|
11
11
|
constructor(message) {
|
|
@@ -170,159 +170,15 @@ function createGroundingContext(options = {}) {
|
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// packages/text2sql/src/lib/adapters/runtime-scope.ts
|
|
174
|
-
import nodeSqlParser from "node-sql-parser";
|
|
175
|
-
var { Parser } = nodeSqlParser;
|
|
176
|
-
var parser = new Parser();
|
|
177
|
-
function parseStatementTypes(sql, dialect) {
|
|
178
|
-
const ast = parser.astify(sql, { database: dialect });
|
|
179
|
-
const statements = Array.isArray(ast) ? ast : [ast];
|
|
180
|
-
return statements.flatMap(
|
|
181
|
-
(statement) => isAstLike(statement) && typeof statement.type === "string" ? [statement.type] : []
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
function extractBaseEntityReferences(sql, dialect) {
|
|
185
|
-
const ast = parser.astify(sql, { database: dialect });
|
|
186
|
-
const state = {
|
|
187
|
-
cteNames: /* @__PURE__ */ new Set(),
|
|
188
|
-
references: /* @__PURE__ */ new Map()
|
|
189
|
-
};
|
|
190
|
-
visitNode(ast, state);
|
|
191
|
-
return Array.from(state.references.values());
|
|
192
|
-
}
|
|
193
|
-
function buildOutOfScopePayload(sql, referencedEntities, allowedEntities) {
|
|
194
|
-
return {
|
|
195
|
-
error: `Query references entities outside grounded scope: ${referencedEntities.join(", ")}`,
|
|
196
|
-
error_type: "OUT_OF_SCOPE",
|
|
197
|
-
suggestion: "Restrict the query to grounded tables/views or expand grounding to include the referenced entities.",
|
|
198
|
-
sql_attempted: sql,
|
|
199
|
-
referenced_entities: referencedEntities,
|
|
200
|
-
allowed_entities: allowedEntities
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
function buildScopeParseErrorPayload(sql, dialect, error) {
|
|
204
|
-
const parserError = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
205
|
-
return {
|
|
206
|
-
error: `SQL scope analysis failed before validation/execution: ${parserError}`,
|
|
207
|
-
error_type: "SQL_SCOPE_PARSE_ERROR",
|
|
208
|
-
suggestion: "Rewrite the query into simpler SQL that can be analyzed safely, or extend parser coverage for this dialect feature.",
|
|
209
|
-
sql_attempted: sql,
|
|
210
|
-
parser_dialect: dialect,
|
|
211
|
-
parser_error: parserError
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
function visitNode(node, state) {
|
|
215
|
-
if (Array.isArray(node)) {
|
|
216
|
-
for (const item of node) {
|
|
217
|
-
visitNode(item, state);
|
|
218
|
-
}
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
if (!isAstLike(node)) {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (isStatementNode(node)) {
|
|
225
|
-
visitStatement(node, state);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (isTableReferenceNode(node)) {
|
|
229
|
-
addReference(node, state);
|
|
230
|
-
}
|
|
231
|
-
for (const value of Object.values(node)) {
|
|
232
|
-
visitNode(value, state);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
function visitStatement(node, parentState) {
|
|
236
|
-
const localState = {
|
|
237
|
-
cteNames: new Set(parentState.cteNames),
|
|
238
|
-
references: parentState.references
|
|
239
|
-
};
|
|
240
|
-
const withItems = Array.isArray(node.with) ? node.with : [];
|
|
241
|
-
for (const item of withItems) {
|
|
242
|
-
if (!isAstLike(item)) {
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
const cteName = readCteName(item);
|
|
246
|
-
if (cteName) {
|
|
247
|
-
localState.cteNames.add(caseFold(cteName));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
for (const item of withItems) {
|
|
251
|
-
if (!isAstLike(item)) {
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
visitNode(item.stmt, localState);
|
|
255
|
-
}
|
|
256
|
-
for (const [key, value] of Object.entries(node)) {
|
|
257
|
-
if (key === "with") {
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
visitNode(value, localState);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
function addReference(node, state) {
|
|
264
|
-
const table2 = typeof node.table === "string" ? node.table : null;
|
|
265
|
-
if (!table2) {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
if (state.cteNames.has(caseFold(table2))) {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
const db = typeof node.db === "string" ? node.db : null;
|
|
272
|
-
const key = db ? `${db}.${table2}` : table2;
|
|
273
|
-
if (!state.references.has(key)) {
|
|
274
|
-
state.references.set(key, { db, table: table2 });
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
function readCteName(node) {
|
|
278
|
-
const name = node.name;
|
|
279
|
-
if (typeof name === "string") {
|
|
280
|
-
return name;
|
|
281
|
-
}
|
|
282
|
-
if (!isAstLike(name)) {
|
|
283
|
-
return void 0;
|
|
284
|
-
}
|
|
285
|
-
const value = name.value;
|
|
286
|
-
return typeof value === "string" ? value : void 0;
|
|
287
|
-
}
|
|
288
|
-
function isStatementNode(node) {
|
|
289
|
-
const type = node.type;
|
|
290
|
-
return typeof type === "string" && ["delete", "insert", "replace", "select", "update"].includes(type);
|
|
291
|
-
}
|
|
292
|
-
function isTableReferenceNode(node) {
|
|
293
|
-
if (node.type === "column_ref") {
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
if (typeof node.table !== "string") {
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
return "addition" in node || "as" in node || "db" in node || "join" in node || "operator" in node || "surround" in node || "table_hint" in node || "temporal_table" in node;
|
|
300
|
-
}
|
|
301
|
-
function isAstLike(value) {
|
|
302
|
-
return typeof value === "object" && value !== null;
|
|
303
|
-
}
|
|
304
|
-
function caseFold(value) {
|
|
305
|
-
return value.toLowerCase();
|
|
306
|
-
}
|
|
307
|
-
|
|
308
173
|
// packages/text2sql/src/lib/adapters/adapter.ts
|
|
309
|
-
var PARSER_DIALECTS_BY_FORMATTER_LANGUAGE = {
|
|
310
|
-
sqlite: ["sqlite", "mysql"],
|
|
311
|
-
postgresql: ["postgresql"],
|
|
312
|
-
bigquery: ["bigquery"],
|
|
313
|
-
transactsql: ["transactsql"],
|
|
314
|
-
mysql: ["mysql"]
|
|
315
|
-
};
|
|
316
|
-
function parserDialectsFor(formatterLanguage) {
|
|
317
|
-
const candidates = PARSER_DIALECTS_BY_FORMATTER_LANGUAGE[formatterLanguage];
|
|
318
|
-
if (!candidates) {
|
|
319
|
-
throw new TypeError(
|
|
320
|
-
`No scope dialect mapping for formatter language "${formatterLanguage}". Add it to PARSER_DIALECTS_BY_FORMATTER_LANGUAGE in adapter.ts.`
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
return candidates;
|
|
324
|
-
}
|
|
325
174
|
var Adapter = class {
|
|
175
|
+
#policyAnalyzer;
|
|
176
|
+
/**
|
|
177
|
+
* Injects the strategy that guards validate() and execute().
|
|
178
|
+
*/
|
|
179
|
+
constructor(policyAnalyzer) {
|
|
180
|
+
this.#policyAnalyzer = policyAnalyzer;
|
|
181
|
+
}
|
|
326
182
|
/**
|
|
327
183
|
* Introspect the database schema and return context fragments.
|
|
328
184
|
*
|
|
@@ -358,14 +214,14 @@ var Adapter = class {
|
|
|
358
214
|
}
|
|
359
215
|
/**
|
|
360
216
|
* Resolve the allowed entity names (tables + views) from grounding config.
|
|
361
|
-
*
|
|
217
|
+
* Asks each grounding to contribute its entities; annotators contribute
|
|
218
|
+
* nothing, so their metadata scans are skipped.
|
|
362
219
|
* Results are NOT cached — call once and store the result.
|
|
363
220
|
*/
|
|
364
221
|
async resolveAllowedEntities() {
|
|
365
222
|
const ctx = createGroundingContext();
|
|
366
223
|
for (const fn of this.grounding) {
|
|
367
|
-
|
|
368
|
-
await grounding.execute(ctx);
|
|
224
|
+
await fn(this).contributeEntities(ctx);
|
|
369
225
|
}
|
|
370
226
|
return [...ctx.tables.map((t) => t.name), ...ctx.views.map((v) => v.name)];
|
|
371
227
|
}
|
|
@@ -542,120 +398,33 @@ var Adapter = class {
|
|
|
542
398
|
#decodeShellEscapes(sql) {
|
|
543
399
|
return sql.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\([().*])/g, "$1").replace(/\\(?=$)/gm, "");
|
|
544
400
|
}
|
|
545
|
-
#checkReadOnly(sql) {
|
|
546
|
-
const statementTypes = this.#readParsedStatementTypes(sql);
|
|
547
|
-
if (statementTypes) {
|
|
548
|
-
if (statementTypes.length === 1 && statementTypes[0] === "select") {
|
|
549
|
-
return null;
|
|
550
|
-
}
|
|
551
|
-
return "only SELECT or WITH queries allowed";
|
|
552
|
-
}
|
|
553
|
-
const keyword = this.#firstStatementKeyword(sql);
|
|
554
|
-
if (keyword === "SELECT" || keyword === "WITH") return null;
|
|
555
|
-
return "only SELECT or WITH queries allowed";
|
|
556
|
-
}
|
|
557
|
-
#readParsedStatementTypes(sql) {
|
|
558
|
-
for (const dialect of parserDialectsFor(this.formatterLanguage)) {
|
|
559
|
-
try {
|
|
560
|
-
return parseStatementTypes(sql, dialect);
|
|
561
|
-
} catch {
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
#firstStatementKeyword(sql) {
|
|
567
|
-
let offset = 0;
|
|
568
|
-
while (offset < sql.length) {
|
|
569
|
-
if (/\s/.test(sql[offset])) {
|
|
570
|
-
offset++;
|
|
571
|
-
continue;
|
|
572
|
-
}
|
|
573
|
-
if (sql.startsWith("--", offset)) {
|
|
574
|
-
offset += 2;
|
|
575
|
-
while (offset < sql.length && sql[offset] !== "\n" && sql[offset] !== "\r") {
|
|
576
|
-
offset++;
|
|
577
|
-
}
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
if (sql.startsWith("/*", offset)) {
|
|
581
|
-
const commentEnd = sql.indexOf("*/", offset + 2);
|
|
582
|
-
if (commentEnd === -1) return null;
|
|
583
|
-
offset = commentEnd + 2;
|
|
584
|
-
continue;
|
|
585
|
-
}
|
|
586
|
-
const keyword = /^[A-Za-z]+/.exec(sql.slice(offset));
|
|
587
|
-
return keyword ? keyword[0].toUpperCase() : null;
|
|
588
|
-
}
|
|
589
|
-
return null;
|
|
590
|
-
}
|
|
591
401
|
#cachedAllowedEntities = null;
|
|
592
402
|
async #resolveScope() {
|
|
593
403
|
if (this.#cachedAllowedEntities) return this.#cachedAllowedEntities;
|
|
594
404
|
this.#cachedAllowedEntities = await this.resolveAllowedEntities();
|
|
595
405
|
return this.#cachedAllowedEntities;
|
|
596
406
|
}
|
|
597
|
-
async #
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
let lastError;
|
|
601
|
-
let lastDialect = candidates[0];
|
|
602
|
-
for (const candidate of candidates) {
|
|
603
|
-
try {
|
|
604
|
-
references = extractBaseEntityReferences(sql, candidate);
|
|
605
|
-
break;
|
|
606
|
-
} catch (error) {
|
|
607
|
-
lastDialect = candidate;
|
|
608
|
-
lastError = error;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
if (references === null) {
|
|
612
|
-
return buildScopeParseErrorPayload(sql, lastDialect, lastError);
|
|
613
|
-
}
|
|
614
|
-
if (references.length === 0) return null;
|
|
615
|
-
const allowedQualified = new Set(
|
|
616
|
-
allowedEntities.map((e) => e.toLowerCase())
|
|
617
|
-
);
|
|
618
|
-
const allowedUnqualified = /* @__PURE__ */ new Set();
|
|
619
|
-
for (const entity of allowedEntities) {
|
|
620
|
-
const dot = entity.lastIndexOf(".");
|
|
621
|
-
if (dot !== -1) {
|
|
622
|
-
allowedUnqualified.add(entity.slice(dot + 1).toLowerCase());
|
|
623
|
-
} else {
|
|
624
|
-
allowedUnqualified.add(entity.toLowerCase());
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
const outOfScope = references.map((ref) => ref.db ? `${ref.db}.${ref.table}` : ref.table).filter((name) => {
|
|
628
|
-
const lower = name.toLowerCase();
|
|
629
|
-
if (name.includes(".")) {
|
|
630
|
-
if (allowedQualified.has(lower)) return false;
|
|
631
|
-
const parts = lower.split(".");
|
|
632
|
-
if (parts.length >= 3) {
|
|
633
|
-
const datasetTable = parts.slice(-2).join(".");
|
|
634
|
-
if (allowedQualified.has(datasetTable)) return false;
|
|
635
|
-
}
|
|
636
|
-
return true;
|
|
637
|
-
}
|
|
638
|
-
return !allowedQualified.has(lower) && !allowedUnqualified.has(lower);
|
|
407
|
+
async #analyzePolicy(sql) {
|
|
408
|
+
return this.#policyAnalyzer.analyze(sql, {
|
|
409
|
+
resolveAllowedEntities: () => this.#resolveScope()
|
|
639
410
|
});
|
|
640
|
-
if (outOfScope.length === 0) return null;
|
|
641
|
-
return buildOutOfScopePayload(sql, outOfScope, allowedEntities);
|
|
642
411
|
}
|
|
643
412
|
async validate(sql) {
|
|
644
413
|
const decoded = this.#decodeShellEscapes(sql);
|
|
645
|
-
const
|
|
646
|
-
if (
|
|
647
|
-
|
|
648
|
-
const scopeError = await this.#checkScope(decoded, allowed);
|
|
649
|
-
if (scopeError) return JSON.stringify(scopeError);
|
|
414
|
+
const violation = await this.#analyzePolicy(decoded);
|
|
415
|
+
if (violation?.kind === "read-only") return violation.message;
|
|
416
|
+
if (violation?.kind === "scope") return JSON.stringify(violation.payload);
|
|
650
417
|
return this.validateImpl(decoded);
|
|
651
418
|
}
|
|
652
419
|
async execute(sql) {
|
|
653
420
|
const decoded = this.#decodeShellEscapes(sql);
|
|
654
|
-
const
|
|
655
|
-
if (
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
if (
|
|
421
|
+
const violation = await this.#analyzePolicy(decoded);
|
|
422
|
+
if (violation?.kind === "read-only") {
|
|
423
|
+
throw new SQLReadOnlyError(violation.message);
|
|
424
|
+
}
|
|
425
|
+
if (violation?.kind === "scope") {
|
|
426
|
+
throw new SQLScopeError(violation.payload);
|
|
427
|
+
}
|
|
659
428
|
return this.executeImpl(decoded);
|
|
660
429
|
}
|
|
661
430
|
/**
|
|
@@ -736,6 +505,16 @@ var AbstractGrounding = class {
|
|
|
736
505
|
this.name = name;
|
|
737
506
|
this.phase = phase;
|
|
738
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Populate ctx with the tables/views this grounding makes available for scope
|
|
510
|
+
* resolution. Annotators decorate entities other groundings produced and add
|
|
511
|
+
* no membership, so the default is a no-op; entity-producing groundings
|
|
512
|
+
* override it. This lets scope resolution skip the annotators' metadata scans.
|
|
513
|
+
*
|
|
514
|
+
* @param _ctx - Shared context for accumulating entity membership
|
|
515
|
+
*/
|
|
516
|
+
async contributeEntities(_ctx) {
|
|
517
|
+
}
|
|
739
518
|
};
|
|
740
519
|
|
|
741
520
|
// packages/text2sql/src/lib/adapters/groundings/column-stats.grounding.ts
|
|
@@ -1059,6 +838,9 @@ var TableGrounding = class extends AbstractGrounding {
|
|
|
1059
838
|
this.#forward = config.forward;
|
|
1060
839
|
this.#backward = config.backward;
|
|
1061
840
|
}
|
|
841
|
+
async contributeEntities(ctx) {
|
|
842
|
+
await this.execute(ctx);
|
|
843
|
+
}
|
|
1062
844
|
/**
|
|
1063
845
|
* Execute the grounding process.
|
|
1064
846
|
* Writes discovered tables and relationships to the context.
|
|
@@ -1605,6 +1387,271 @@ var SqlServerRowCountGrounding = class extends RowCountGrounding {
|
|
|
1605
1387
|
}
|
|
1606
1388
|
};
|
|
1607
1389
|
|
|
1390
|
+
// packages/text2sql/src/lib/adapters/parser-sql-policy.ts
|
|
1391
|
+
import nodeSqlParser from "node-sql-parser";
|
|
1392
|
+
|
|
1393
|
+
// packages/text2sql/src/lib/sql-scope-error.ts
|
|
1394
|
+
function buildScopeParseErrorPayload(sql, parserDialect, error) {
|
|
1395
|
+
const parserError = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
1396
|
+
return {
|
|
1397
|
+
error: `SQL scope analysis failed before validation/execution: ${parserError}`,
|
|
1398
|
+
error_type: "SQL_SCOPE_PARSE_ERROR",
|
|
1399
|
+
suggestion: "Rewrite the query into simpler SQL that can be analyzed safely, or extend parser coverage for this dialect feature.",
|
|
1400
|
+
sql_attempted: sql,
|
|
1401
|
+
parser_dialect: parserDialect,
|
|
1402
|
+
parser_error: parserError
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// packages/text2sql/src/lib/adapters/parser-sql-policy.ts
|
|
1407
|
+
var READ_ONLY_MESSAGE = "only SELECT or WITH queries allowed";
|
|
1408
|
+
var { Parser } = nodeSqlParser;
|
|
1409
|
+
var parser = new Parser();
|
|
1410
|
+
var ParserSqlPolicyAnalyzer = class {
|
|
1411
|
+
readOnlyPolicy = {};
|
|
1412
|
+
async analyze(sql, context) {
|
|
1413
|
+
if (!this.#isReadOnly(sql)) {
|
|
1414
|
+
return { kind: "read-only", message: READ_ONLY_MESSAGE };
|
|
1415
|
+
}
|
|
1416
|
+
const scopeError = await this.#checkScope(sql, context);
|
|
1417
|
+
return scopeError ? { kind: "scope", payload: scopeError } : null;
|
|
1418
|
+
}
|
|
1419
|
+
#isReadOnly(sql) {
|
|
1420
|
+
const statements = this.#readParsedStatements(sql);
|
|
1421
|
+
if (statements) {
|
|
1422
|
+
return statements.length === 1 && statements[0]?.type === "select" && !containsBlockedOperation(statements[0], this.readOnlyPolicy);
|
|
1423
|
+
}
|
|
1424
|
+
const keyword = firstStatementKeyword(sql);
|
|
1425
|
+
return keyword === "SELECT" || keyword === "WITH";
|
|
1426
|
+
}
|
|
1427
|
+
#readParsedStatements(sql) {
|
|
1428
|
+
for (const dialect of this.dialects) {
|
|
1429
|
+
try {
|
|
1430
|
+
return parseStatements(sql, dialect);
|
|
1431
|
+
} catch {
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
async #checkScope(sql, context) {
|
|
1437
|
+
let references = null;
|
|
1438
|
+
let lastError;
|
|
1439
|
+
let lastDialect = this.dialects[0];
|
|
1440
|
+
for (const dialect of this.dialects) {
|
|
1441
|
+
try {
|
|
1442
|
+
references = extractBaseEntityReferences(sql, dialect);
|
|
1443
|
+
break;
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
lastDialect = dialect;
|
|
1446
|
+
lastError = error;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (references === null) {
|
|
1450
|
+
return buildScopeParseErrorPayload(sql, lastDialect, lastError);
|
|
1451
|
+
}
|
|
1452
|
+
if (references.length === 0) return null;
|
|
1453
|
+
const allowedEntities = await context.resolveAllowedEntities();
|
|
1454
|
+
const allowedQualified = new Set(
|
|
1455
|
+
allowedEntities.map((entity) => entity.toLowerCase())
|
|
1456
|
+
);
|
|
1457
|
+
const allowedUnqualified = /* @__PURE__ */ new Set();
|
|
1458
|
+
for (const entity of allowedEntities) {
|
|
1459
|
+
const dot = entity.lastIndexOf(".");
|
|
1460
|
+
allowedUnqualified.add(
|
|
1461
|
+
(dot === -1 ? entity : entity.slice(dot + 1)).toLowerCase()
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
const outOfScope = references.map(
|
|
1465
|
+
(reference) => reference.db ? `${reference.db}.${reference.table}` : reference.table
|
|
1466
|
+
).filter((name) => {
|
|
1467
|
+
const lower = name.toLowerCase();
|
|
1468
|
+
if (!name.includes(".")) {
|
|
1469
|
+
return !allowedQualified.has(lower) && !allowedUnqualified.has(lower);
|
|
1470
|
+
}
|
|
1471
|
+
if (allowedQualified.has(lower)) return false;
|
|
1472
|
+
const parts = lower.split(".");
|
|
1473
|
+
return parts.length < 3 || !allowedQualified.has(parts.slice(-2).join("."));
|
|
1474
|
+
});
|
|
1475
|
+
return outOfScope.length === 0 ? null : buildOutOfScopePayload(sql, outOfScope, [...allowedEntities]);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
function parseStatements(sql, dialect) {
|
|
1479
|
+
const ast = parser.astify(sql, { database: dialect });
|
|
1480
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
1481
|
+
return statements.filter(isAstLike);
|
|
1482
|
+
}
|
|
1483
|
+
function containsBlockedOperation(node, policy) {
|
|
1484
|
+
if (Array.isArray(node)) {
|
|
1485
|
+
return node.some((item) => containsBlockedOperation(item, policy));
|
|
1486
|
+
}
|
|
1487
|
+
if (!isAstLike(node)) return false;
|
|
1488
|
+
if (policy.blockSelectInto && node.type === "select" && isAstLike(node.into) && node.into.type === "into") {
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
1491
|
+
if (policy.blockLockingReads && node.locking_read != null) return true;
|
|
1492
|
+
if (policy.blockAssignments && node.type === "assign") return true;
|
|
1493
|
+
if (policy.blockServerQualifiedRelations && isTableReferenceNode(node) && typeof node.server === "string") {
|
|
1494
|
+
return true;
|
|
1495
|
+
}
|
|
1496
|
+
if (policy.blockTableHints && node.table_hint != null) return true;
|
|
1497
|
+
if (node.type === "function") {
|
|
1498
|
+
const functionIdentifier = readFunctionIdentifier(node);
|
|
1499
|
+
if (functionIdentifier && policy.blockQualifiedFunctions && functionIdentifier.parts.length > 1) {
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
if (functionIdentifier && policy.blockedFunctions?.some(
|
|
1503
|
+
(blocked) => blocked.toLowerCase() === functionIdentifier.parts.at(-1)
|
|
1504
|
+
)) {
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
if (functionIdentifier && policy.blockedFunctionPrefixes?.some(
|
|
1508
|
+
(prefix) => functionIdentifier.parts.at(-1)?.startsWith(prefix.toLowerCase())
|
|
1509
|
+
)) {
|
|
1510
|
+
return true;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return Object.values(node).some(
|
|
1514
|
+
(value) => containsBlockedOperation(value, policy)
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
function readFunctionIdentifier(node) {
|
|
1518
|
+
if (!isAstLike(node.name)) return null;
|
|
1519
|
+
const parts = [];
|
|
1520
|
+
if (isAstLike(node.name.schema) && typeof node.name.schema.value === "string") {
|
|
1521
|
+
appendIdentifierParts(parts, node.name.schema.value);
|
|
1522
|
+
}
|
|
1523
|
+
const names = Array.isArray(node.name.name) ? node.name.name : [];
|
|
1524
|
+
for (const name of names) {
|
|
1525
|
+
if (isAstLike(name) && typeof name.value === "string") {
|
|
1526
|
+
appendIdentifierParts(parts, name.value);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
return parts.length > 0 ? { parts } : null;
|
|
1530
|
+
}
|
|
1531
|
+
function appendIdentifierParts(parts, value) {
|
|
1532
|
+
parts.push(
|
|
1533
|
+
...value.toLowerCase().split(".").filter((part) => part.length > 0)
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
function extractBaseEntityReferences(sql, dialect) {
|
|
1537
|
+
const ast = parser.astify(sql, { database: dialect });
|
|
1538
|
+
const state = {
|
|
1539
|
+
cteNames: /* @__PURE__ */ new Set(),
|
|
1540
|
+
references: /* @__PURE__ */ new Map()
|
|
1541
|
+
};
|
|
1542
|
+
visitNode(ast, state);
|
|
1543
|
+
return Array.from(state.references.values());
|
|
1544
|
+
}
|
|
1545
|
+
function firstStatementKeyword(sql) {
|
|
1546
|
+
let offset = 0;
|
|
1547
|
+
while (offset < sql.length) {
|
|
1548
|
+
if (/\s/.test(sql[offset])) {
|
|
1549
|
+
offset++;
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
if (sql.startsWith("--", offset)) {
|
|
1553
|
+
offset += 2;
|
|
1554
|
+
while (offset < sql.length && sql[offset] !== "\n" && sql[offset] !== "\r") {
|
|
1555
|
+
offset++;
|
|
1556
|
+
}
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (sql.startsWith("/*", offset)) {
|
|
1560
|
+
const commentEnd = sql.indexOf("*/", offset + 2);
|
|
1561
|
+
if (commentEnd === -1) return null;
|
|
1562
|
+
offset = commentEnd + 2;
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
const keyword = /^[A-Za-z]+/.exec(sql.slice(offset));
|
|
1566
|
+
return keyword ? keyword[0].toUpperCase() : null;
|
|
1567
|
+
}
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
function buildOutOfScopePayload(sql, referencedEntities, allowedEntities) {
|
|
1571
|
+
return {
|
|
1572
|
+
error: `Query references entities outside grounded scope: ${referencedEntities.join(", ")}`,
|
|
1573
|
+
error_type: "OUT_OF_SCOPE",
|
|
1574
|
+
suggestion: "Restrict the query to grounded tables/views or expand grounding to include the referenced entities.",
|
|
1575
|
+
sql_attempted: sql,
|
|
1576
|
+
referenced_entities: referencedEntities,
|
|
1577
|
+
allowed_entities: allowedEntities
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function visitNode(node, state) {
|
|
1581
|
+
if (Array.isArray(node)) {
|
|
1582
|
+
for (const item of node) visitNode(item, state);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (!isAstLike(node)) return;
|
|
1586
|
+
if (isStatementNode(node)) {
|
|
1587
|
+
visitStatement(node, state);
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (isTableReferenceNode(node)) addReference(node, state);
|
|
1591
|
+
for (const value of Object.values(node)) visitNode(value, state);
|
|
1592
|
+
}
|
|
1593
|
+
function visitStatement(node, parentState) {
|
|
1594
|
+
const localState = {
|
|
1595
|
+
cteNames: new Set(parentState.cteNames),
|
|
1596
|
+
references: parentState.references
|
|
1597
|
+
};
|
|
1598
|
+
const withItems = Array.isArray(node.with) ? node.with : [];
|
|
1599
|
+
for (const item of withItems) {
|
|
1600
|
+
if (!isAstLike(item)) continue;
|
|
1601
|
+
const cteName = readCteName(item);
|
|
1602
|
+
if (cteName) localState.cteNames.add(caseFold(cteName));
|
|
1603
|
+
}
|
|
1604
|
+
for (const item of withItems) {
|
|
1605
|
+
if (isAstLike(item)) visitNode(item.stmt, localState);
|
|
1606
|
+
}
|
|
1607
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1608
|
+
if (key !== "with") visitNode(value, localState);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function addReference(node, state) {
|
|
1612
|
+
const table2 = typeof node.table === "string" ? node.table : null;
|
|
1613
|
+
if (!table2 || state.cteNames.has(caseFold(table2))) return;
|
|
1614
|
+
const db = typeof node.db === "string" ? node.db : null;
|
|
1615
|
+
const key = db ? `${db}.${table2}` : table2;
|
|
1616
|
+
if (!state.references.has(key)) state.references.set(key, { db, table: table2 });
|
|
1617
|
+
}
|
|
1618
|
+
function readCteName(node) {
|
|
1619
|
+
const name = node.name;
|
|
1620
|
+
if (typeof name === "string") return name;
|
|
1621
|
+
if (!isAstLike(name)) return void 0;
|
|
1622
|
+
return typeof name.value === "string" ? name.value : void 0;
|
|
1623
|
+
}
|
|
1624
|
+
function isStatementNode(node) {
|
|
1625
|
+
return typeof node.type === "string" && ["delete", "insert", "replace", "select", "update"].includes(node.type);
|
|
1626
|
+
}
|
|
1627
|
+
function isTableReferenceNode(node) {
|
|
1628
|
+
if (node.type === "column_ref" || typeof node.table !== "string")
|
|
1629
|
+
return false;
|
|
1630
|
+
return "addition" in node || "as" in node || "db" in node || "join" in node || "operator" in node || "surround" in node || "table_hint" in node || "temporal_table" in node;
|
|
1631
|
+
}
|
|
1632
|
+
function isAstLike(value) {
|
|
1633
|
+
return typeof value === "object" && value !== null;
|
|
1634
|
+
}
|
|
1635
|
+
function caseFold(value) {
|
|
1636
|
+
return value.toLowerCase();
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// packages/text2sql/src/lib/adapters/sqlserver/sqlserver.sql-policy.ts
|
|
1640
|
+
var SqlServerSqlPolicyAnalyzer = class extends ParserSqlPolicyAnalyzer {
|
|
1641
|
+
dialects = ["transactsql"];
|
|
1642
|
+
readOnlyPolicy = {
|
|
1643
|
+
blockSelectInto: true,
|
|
1644
|
+
blockServerQualifiedRelations: true,
|
|
1645
|
+
blockTableHints: true,
|
|
1646
|
+
blockedFunctions: [
|
|
1647
|
+
"FN_GET_AUDIT_FILE",
|
|
1648
|
+
"OPENDATASOURCE",
|
|
1649
|
+
"OPENQUERY",
|
|
1650
|
+
"OPENROWSET"
|
|
1651
|
+
]
|
|
1652
|
+
};
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1608
1655
|
// packages/text2sql/src/lib/adapters/sqlserver/sqlserver.ts
|
|
1609
1656
|
var SQL_SERVER_ERROR_MAP = {
|
|
1610
1657
|
"208": {
|
|
@@ -1666,7 +1713,7 @@ var SqlServer = class extends Adapter {
|
|
|
1666
1713
|
systemSchemas = ["INFORMATION_SCHEMA", "sys"];
|
|
1667
1714
|
formatterLanguage = "transactsql";
|
|
1668
1715
|
constructor(options) {
|
|
1669
|
-
super();
|
|
1716
|
+
super(new SqlServerSqlPolicyAnalyzer());
|
|
1670
1717
|
if (!options || typeof options.execute !== "function") {
|
|
1671
1718
|
throw new Error("SqlServer adapter requires an execute function.");
|
|
1672
1719
|
}
|
|
@@ -2226,6 +2273,9 @@ var ViewGrounding = class extends AbstractGrounding {
|
|
|
2226
2273
|
this.#columns = config.columns;
|
|
2227
2274
|
this.includeDefinition = config.includeDefinition ?? true;
|
|
2228
2275
|
}
|
|
2276
|
+
async contributeEntities(ctx) {
|
|
2277
|
+
await this.execute(ctx);
|
|
2278
|
+
}
|
|
2229
2279
|
/**
|
|
2230
2280
|
* Execute the grounding process.
|
|
2231
2281
|
* Writes discovered views to the context.
|
|
@@ -2370,6 +2420,7 @@ var sqlserver_default = {
|
|
|
2370
2420
|
};
|
|
2371
2421
|
export {
|
|
2372
2422
|
SqlServer,
|
|
2423
|
+
SqlServerSqlPolicyAnalyzer,
|
|
2373
2424
|
columnStats,
|
|
2374
2425
|
columnValues,
|
|
2375
2426
|
constraints,
|