@deepagents/text2sql 1.0.0 → 2.2.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 +83 -15
- package/dist/bin/sql.js +823 -169
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +345 -25
- package/dist/index.js.map +4 -4
- package/dist/lib/adapters/adapter.d.ts.map +1 -1
- package/dist/lib/adapters/bigquery/index.js +68 -21
- package/dist/lib/adapters/bigquery/index.js.map +2 -2
- package/dist/lib/adapters/mysql/index.js +68 -21
- package/dist/lib/adapters/mysql/index.js.map +2 -2
- package/dist/lib/adapters/postgres/index.js +68 -21
- package/dist/lib/adapters/postgres/index.js.map +2 -2
- package/dist/lib/adapters/runtime-scope.d.ts +2 -0
- package/dist/lib/adapters/runtime-scope.d.ts.map +1 -1
- package/dist/lib/adapters/spreadsheet/index.js +68 -21
- package/dist/lib/adapters/spreadsheet/index.js.map +2 -2
- package/dist/lib/adapters/sqlite/index.js +68 -21
- package/dist/lib/adapters/sqlite/index.js.map +2 -2
- package/dist/lib/adapters/sqlserver/index.js +68 -21
- package/dist/lib/adapters/sqlserver/index.js.map +2 -2
- package/dist/lib/sql-command.d.ts +32 -0
- package/dist/lib/sql-command.d.ts.map +1 -0
- package/dist/lib/sql.d.ts +32 -8
- package/dist/lib/sql.d.ts.map +1 -1
- package/dist/lib/synthesis/index.js.map +2 -2
- package/package.json +5 -5
package/dist/bin/sql.js
CHANGED
|
@@ -3,80 +3,8 @@
|
|
|
3
3
|
// packages/text2sql/src/bin/sql.ts
|
|
4
4
|
import { cac } from "cac";
|
|
5
5
|
|
|
6
|
-
// packages/text2sql/src/
|
|
7
|
-
|
|
8
|
-
command;
|
|
9
|
-
exitCode;
|
|
10
|
-
constructor(command, message, exitCode = 1) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = "CommandError";
|
|
13
|
-
this.command = command;
|
|
14
|
-
this.exitCode = exitCode;
|
|
15
|
-
}
|
|
16
|
-
toResult() {
|
|
17
|
-
return {
|
|
18
|
-
stdout: "",
|
|
19
|
-
stderr: `sql ${this.command}: ${this.message}
|
|
20
|
-
`,
|
|
21
|
-
exitCode: this.exitCode
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
function errorMessage(error) {
|
|
26
|
-
return error instanceof Error ? error.message : String(error);
|
|
27
|
-
}
|
|
28
|
-
var SqlCommand = class {
|
|
29
|
-
options = [];
|
|
30
|
-
helpDisplay;
|
|
31
|
-
fail(message, exitCode = 1) {
|
|
32
|
-
throw new CommandError(this.name, message, exitCode);
|
|
33
|
-
}
|
|
34
|
-
requireAdapter(adapters, db) {
|
|
35
|
-
const available = Object.keys(adapters).join(", ") || "(none configured)";
|
|
36
|
-
if (!db) {
|
|
37
|
-
this.fail(
|
|
38
|
-
`missing database name. Usage: sql ${this.name} ${this.usage}. Available: ${available}`
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
const adapter = adapters[db];
|
|
42
|
-
if (!adapter) {
|
|
43
|
-
this.fail(`unknown database "${db}". Available: ${available}`);
|
|
44
|
-
}
|
|
45
|
-
return adapter;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
var SqlQueryCommand = class extends SqlCommand {
|
|
49
|
-
args = "<db> <...sql>";
|
|
50
|
-
async execute(ctx, args, options) {
|
|
51
|
-
const [db, sqlParts] = args;
|
|
52
|
-
const adapter = this.requireAdapter(ctx.adapters, db);
|
|
53
|
-
const sql = (sqlParts ?? []).join(" ").trim();
|
|
54
|
-
if (!sql) this.fail("no query provided");
|
|
55
|
-
const formatted = await this.formatAndValidate(adapter, sql);
|
|
56
|
-
return this.afterValidation(ctx, adapter, formatted, options);
|
|
57
|
-
}
|
|
58
|
-
async formatAndValidate(adapter, sql) {
|
|
59
|
-
let formatted;
|
|
60
|
-
try {
|
|
61
|
-
formatted = adapter.format(sql);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
this.fail(errorMessage(error));
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
const syntaxError = await adapter.validate(formatted);
|
|
67
|
-
if (syntaxError) this.fail(syntaxError);
|
|
68
|
-
} catch (error) {
|
|
69
|
-
this.fail(errorMessage(error));
|
|
70
|
-
}
|
|
71
|
-
return formatted;
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// packages/text2sql/src/bin/commands/index.ts
|
|
76
|
-
import { appendFileSync } from "node:fs";
|
|
77
|
-
import { mkdir, writeFile as writeFile2 } from "node:fs/promises";
|
|
78
|
-
import * as path2 from "node:path";
|
|
79
|
-
import { v7 } from "uuid";
|
|
6
|
+
// packages/text2sql/src/lib/sql.ts
|
|
7
|
+
import "@deepagents/agent";
|
|
80
8
|
|
|
81
9
|
// packages/text2sql/src/lib/adapter-index.ts
|
|
82
10
|
import { fragment } from "@deepagents/context";
|
|
@@ -180,6 +108,17 @@ function createGroundingContext(options = {}) {
|
|
|
180
108
|
import nodeSqlParser from "node-sql-parser";
|
|
181
109
|
var { Parser } = nodeSqlParser;
|
|
182
110
|
var parser = new Parser();
|
|
111
|
+
function buildScopeParseErrorPayload(sql, dialect, error) {
|
|
112
|
+
const parserError = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
113
|
+
return {
|
|
114
|
+
error: `SQL scope analysis failed before validation/execution: ${parserError}`,
|
|
115
|
+
error_type: "SQL_SCOPE_PARSE_ERROR",
|
|
116
|
+
suggestion: "Rewrite the query into simpler SQL that can be analyzed safely, or extend parser coverage for this dialect feature.",
|
|
117
|
+
sql_attempted: sql,
|
|
118
|
+
parser_dialect: dialect,
|
|
119
|
+
parser_error: parserError
|
|
120
|
+
};
|
|
121
|
+
}
|
|
183
122
|
|
|
184
123
|
// packages/text2sql/src/lib/file-cache.ts
|
|
185
124
|
import { createHash } from "node:crypto";
|
|
@@ -340,7 +279,7 @@ var AdapterIndexer = class {
|
|
|
340
279
|
});
|
|
341
280
|
return fragments;
|
|
342
281
|
} catch (error) {
|
|
343
|
-
const reason =
|
|
282
|
+
const reason = errorMessage(error);
|
|
344
283
|
progress({
|
|
345
284
|
type: "adapter:error",
|
|
346
285
|
adapter: name,
|
|
@@ -369,7 +308,7 @@ var AdapterIndexer = class {
|
|
|
369
308
|
return configured;
|
|
370
309
|
}
|
|
371
310
|
if (this.#version) {
|
|
372
|
-
return `
|
|
311
|
+
return `index-${this.#version}-${name}`;
|
|
373
312
|
}
|
|
374
313
|
return void 0;
|
|
375
314
|
}
|
|
@@ -387,72 +326,811 @@ function adapterProgressEvent(adapter, progress) {
|
|
|
387
326
|
timestampMs: progress.timestampMs
|
|
388
327
|
};
|
|
389
328
|
}
|
|
329
|
+
function errorMessage(error) {
|
|
330
|
+
return error instanceof Error ? error.message : String(error);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// packages/text2sql/src/lib/agents/sql.agent.ts
|
|
334
|
+
import { groq } from "@ai-sdk/groq";
|
|
335
|
+
import {
|
|
336
|
+
APICallError,
|
|
337
|
+
JSONParseError,
|
|
338
|
+
NoContentGeneratedError,
|
|
339
|
+
NoObjectGeneratedError,
|
|
340
|
+
NoOutputGeneratedError,
|
|
341
|
+
TypeValidationError,
|
|
342
|
+
defaultSettingsMiddleware,
|
|
343
|
+
wrapLanguageModel
|
|
344
|
+
} from "ai";
|
|
345
|
+
import dedent from "dedent";
|
|
346
|
+
import pRetry from "p-retry";
|
|
347
|
+
import z from "zod";
|
|
348
|
+
import "@deepagents/agent";
|
|
349
|
+
import {
|
|
350
|
+
ContextEngine,
|
|
351
|
+
InMemoryContextStore,
|
|
352
|
+
example,
|
|
353
|
+
fragment as fragment2,
|
|
354
|
+
guardrail,
|
|
355
|
+
hint,
|
|
356
|
+
persona,
|
|
357
|
+
policy,
|
|
358
|
+
structuredOutput,
|
|
359
|
+
user,
|
|
360
|
+
workflow
|
|
361
|
+
} from "@deepagents/context";
|
|
362
|
+
var RETRY_TEMPERATURES = [0, 0.2, 0.3];
|
|
363
|
+
var SQL_AGENT_ROLE = "Expert SQL query generator.";
|
|
364
|
+
var SQL_AGENT_OBJECTIVE = "Generate precise SQL grounded in provided schema.";
|
|
365
|
+
var FORMATTER_SCOPE_DIALECTS = /* @__PURE__ */ new Map([
|
|
366
|
+
["sqlite", "sqlite"],
|
|
367
|
+
["postgresql", "postgresql"],
|
|
368
|
+
["bigquery", "bigquery"],
|
|
369
|
+
["transactsql", "transactsql"],
|
|
370
|
+
["mysql", "mysql"]
|
|
371
|
+
]);
|
|
372
|
+
var SQL_AGENT_POLICIES = [
|
|
373
|
+
fragment2(
|
|
374
|
+
"schema_mapping",
|
|
375
|
+
policy({
|
|
376
|
+
rule: "Translate natural language into precise SQL grounded in available schema entities."
|
|
377
|
+
}),
|
|
378
|
+
hint("Preserve schema spelling exactly, including typos in column names.")
|
|
379
|
+
),
|
|
380
|
+
fragment2(
|
|
381
|
+
"projection_minimality",
|
|
382
|
+
policy({
|
|
383
|
+
rule: "Return only columns requested by the question; do not add helper columns unless explicitly requested."
|
|
384
|
+
}),
|
|
385
|
+
policy({
|
|
386
|
+
rule: 'For requests of the form "X sorted/ordered by Y", project X only unless Y is explicitly requested as an output field.'
|
|
387
|
+
}),
|
|
388
|
+
policy({
|
|
389
|
+
rule: "Prefer selecting schema columns directly without derived expressions when direct selection answers the request."
|
|
390
|
+
}),
|
|
391
|
+
hint(
|
|
392
|
+
"Do not include ORDER BY, GROUP BY, or JOIN helper columns in SELECT output unless the question explicitly asks for them."
|
|
393
|
+
),
|
|
394
|
+
policy({
|
|
395
|
+
rule: "Use DISTINCT only when uniqueness is explicitly requested (for example distinct/unique/different/no duplicates)."
|
|
396
|
+
}),
|
|
397
|
+
hint(
|
|
398
|
+
'Do not infer DISTINCT from generic wording such as "some", plural nouns, or entity-set phrasing; for transactional/attendance-style tables, default to raw rows unless uniqueness is explicitly requested.'
|
|
399
|
+
)
|
|
400
|
+
),
|
|
401
|
+
fragment2(
|
|
402
|
+
"date_transform_safety",
|
|
403
|
+
policy({
|
|
404
|
+
rule: "Do not assume VARCHAR/TEXT values are parseable dates. Avoid date extraction functions on text columns by default."
|
|
405
|
+
}),
|
|
406
|
+
policy({
|
|
407
|
+
rule: "Use date-part extraction only when both conditions hold: the question explicitly asks for transformation and schema values require transformation to produce that unit."
|
|
408
|
+
}),
|
|
409
|
+
hint(
|
|
410
|
+
"Do not apply SUBSTR, STRFTIME, DATE_PART, YEAR, or similar extraction functions unless the question explicitly asks for transformation and schema values require it."
|
|
411
|
+
),
|
|
412
|
+
hint(
|
|
413
|
+
"If a column already represents the requested concept (for example a stored year-like value), use the column as-is."
|
|
414
|
+
)
|
|
415
|
+
),
|
|
416
|
+
fragment2(
|
|
417
|
+
"sql_minimality",
|
|
418
|
+
guardrail({
|
|
419
|
+
rule: "Never hallucinate tables or columns.",
|
|
420
|
+
reason: "Schema fidelity is required.",
|
|
421
|
+
action: "Use only available schema entities."
|
|
422
|
+
}),
|
|
423
|
+
guardrail({
|
|
424
|
+
rule: "Avoid unnecessary transformations and derived projections.",
|
|
425
|
+
reason: "Extra transformations frequently change semantics and reduce correctness.",
|
|
426
|
+
action: "Do not add date parsing, substring extraction, or derived columns unless explicitly required by the question or schema."
|
|
427
|
+
})
|
|
428
|
+
),
|
|
429
|
+
fragment2(
|
|
430
|
+
"preflight_checklist",
|
|
431
|
+
workflow({
|
|
432
|
+
task: "Final SQL preflight before returning output",
|
|
433
|
+
steps: [
|
|
434
|
+
"Verify selected columns match the question and remove unrequested helper projections.",
|
|
435
|
+
"If aggregate values are used only for ranking/filtering, keep them out of SELECT unless explicitly requested.",
|
|
436
|
+
"Prefer raw schema columns over derived expressions when raw columns already satisfy the request.",
|
|
437
|
+
"If a candidate query uses STRFTIME, SUBSTR, DATE_PART, YEAR, or similar extraction on text-like columns, remove that transformation unless explicitly required by the question.",
|
|
438
|
+
"Return only schema-grounded SQL using existing tables and columns."
|
|
439
|
+
]
|
|
440
|
+
})
|
|
441
|
+
),
|
|
442
|
+
fragment2(
|
|
443
|
+
"set_semantics",
|
|
444
|
+
policy({
|
|
445
|
+
rule: "For questions asking where both condition A and condition B hold over an attribute, compute the intersection of qualifying sets for that attribute."
|
|
446
|
+
}),
|
|
447
|
+
policy({
|
|
448
|
+
rule: "Do not force the same entity instance to satisfy both conditions unless the question explicitly requests the same person/row/entity."
|
|
449
|
+
}),
|
|
450
|
+
hint(
|
|
451
|
+
"Prefer INTERSECT (or logically equivalent set-based shape) over requiring the same physical row/entity to satisfy both conditions unless explicitly requested."
|
|
452
|
+
),
|
|
453
|
+
hint(
|
|
454
|
+
"When two conditions describe different row groups whose shared attribute is requested, build each group separately and intersect the attribute values."
|
|
455
|
+
),
|
|
456
|
+
hint(
|
|
457
|
+
"Do not collapse cross-group conditions into a single-row AND predicate when the intent is shared values across groups."
|
|
458
|
+
),
|
|
459
|
+
policy({
|
|
460
|
+
rule: "If two predicates on the same field cannot both be true for one row, do not combine them with AND; use set operations across separate filtered subsets when shared values are requested."
|
|
461
|
+
})
|
|
462
|
+
),
|
|
463
|
+
fragment2(
|
|
464
|
+
"predicate_column_alignment",
|
|
465
|
+
policy({
|
|
466
|
+
rule: "Match literal values to semantically compatible columns. Do not compare descriptive names to identifier columns."
|
|
467
|
+
}),
|
|
468
|
+
hint(
|
|
469
|
+
"When a filter value is a descriptive label (for example a department name), join through the lookup table and filter on its name/title column, not on *_id columns."
|
|
470
|
+
),
|
|
471
|
+
hint(
|
|
472
|
+
"When relation roles are explicit in wording (for example host/home/source/destination), prefer foreign keys with matching role qualifiers over generic similarly named columns."
|
|
473
|
+
),
|
|
474
|
+
policy({
|
|
475
|
+
rule: "When multiple foreign-key candidates exist, select the column whose qualifier best matches the relationship described in the question."
|
|
476
|
+
}),
|
|
477
|
+
policy({
|
|
478
|
+
rule: "For hosting/held semantics, prefer host_* relationship columns when available over generic *_id alternatives."
|
|
479
|
+
}),
|
|
480
|
+
hint(
|
|
481
|
+
'Interpret wording like "held/hosted a competition or event" as a hosting relationship and map to host_* foreign keys when present.'
|
|
482
|
+
),
|
|
483
|
+
policy({
|
|
484
|
+
rule: "Do not compare descriptive labels or names to *_id columns; join to the table containing the descriptive field and filter there."
|
|
485
|
+
}),
|
|
486
|
+
policy({
|
|
487
|
+
rule: "Keep numeric identifiers unquoted when used as numeric equality filters unless schema indicates text identifiers."
|
|
488
|
+
}),
|
|
489
|
+
policy({
|
|
490
|
+
rule: "When filtering by a descriptive label value and a related table exposes a corresponding *_name or title column, join to that table and filter on the descriptive column."
|
|
491
|
+
})
|
|
492
|
+
),
|
|
493
|
+
fragment2(
|
|
494
|
+
"ordering_semantics",
|
|
495
|
+
policy({
|
|
496
|
+
rule: "Respect explicit sort direction terms. If direction is not specified, use ascending order unless a superlative intent (most/least/highest/lowest) implies direction."
|
|
497
|
+
}),
|
|
498
|
+
policy({
|
|
499
|
+
rule: "When ranking categories by frequency, use COUNT for ordering but keep output focused on requested category fields unless counts are explicitly requested."
|
|
500
|
+
}),
|
|
501
|
+
policy({
|
|
502
|
+
rule: "Do not use DESC unless descending direction is explicit or a superlative intent requires descending ranking."
|
|
503
|
+
}),
|
|
504
|
+
policy({
|
|
505
|
+
rule: 'For "most common/frequent <attribute>" requests, return the attribute value(s) only; use counts only for ordering/filtering unless the question explicitly asks to return counts.'
|
|
506
|
+
}),
|
|
507
|
+
hint(
|
|
508
|
+
'Use DESC with LIMIT 1 for "most/highest/largest"; use ASC with LIMIT 1 for "least/lowest/smallest".'
|
|
509
|
+
)
|
|
510
|
+
),
|
|
511
|
+
fragment2(
|
|
512
|
+
"negative_membership_queries",
|
|
513
|
+
policy({
|
|
514
|
+
rule: "For requests asking entities that did not participate/host/appear in related records, prefer NOT IN or NOT EXISTS against the related foreign-key set."
|
|
515
|
+
}),
|
|
516
|
+
hint(
|
|
517
|
+
"Map role-bearing relationship columns carefully (for example host_* foreign keys for hosting relationships) instead of generic IDs when role wording is explicit."
|
|
518
|
+
),
|
|
519
|
+
hint(
|
|
520
|
+
'For "never had/never exceeded" conditions over history tables, exclude entities via NOT IN/NOT EXISTS against the disqualifying entity-id set (often built with GROUP BY/HAVING MAX(...)).'
|
|
521
|
+
)
|
|
522
|
+
),
|
|
523
|
+
fragment2(
|
|
524
|
+
"join_completeness",
|
|
525
|
+
policy({
|
|
526
|
+
rule: "Preserve entity-restricting joins implied by the question. Do not widen results by querying only a broader attribute table when a subset entity table is available."
|
|
527
|
+
}),
|
|
528
|
+
policy({
|
|
529
|
+
rule: "If an entity term in the question maps to a table, keep that table in query scope and join to attribute tables rather than dropping the entity table."
|
|
530
|
+
}),
|
|
531
|
+
hint(
|
|
532
|
+
"If the question targets a specific entity group, include that entity table and its join conditions even when selected columns come from a related table."
|
|
533
|
+
),
|
|
534
|
+
hint(
|
|
535
|
+
"When the question names an entity type and a relation table links to that entity via *_id, include the entity table in scope instead of counting only relation rows."
|
|
536
|
+
),
|
|
537
|
+
hint(
|
|
538
|
+
"Prefer INNER JOIN by default; use LEFT JOIN only when the question explicitly requests including unmatched rows or zero-related entities."
|
|
539
|
+
)
|
|
540
|
+
),
|
|
541
|
+
fragment2(
|
|
542
|
+
"aggregation_exactness",
|
|
543
|
+
policy({
|
|
544
|
+
rule: "Preserve requested aggregation semantics exactly: use COUNT(*) by default for total rows, use COUNT(DISTINCT ...) only when uniqueness is explicitly requested, and group by stable entity keys when computing per-entity aggregates."
|
|
545
|
+
}),
|
|
546
|
+
policy({
|
|
547
|
+
rule: "For questions asking which entity has lowest/highest average of a metric, compute AVG(metric) per entity (GROUP BY entity) and rank those aggregates."
|
|
548
|
+
}),
|
|
549
|
+
hint(
|
|
550
|
+
'For "how many <entities>" questions over relation records, default to COUNT(*) on qualifying rows unless explicit uniqueness language is present.'
|
|
551
|
+
)
|
|
552
|
+
),
|
|
553
|
+
fragment2(
|
|
554
|
+
"query_shape_examples",
|
|
555
|
+
example({
|
|
556
|
+
question: "List categories ordered by how many records belong to each category.",
|
|
557
|
+
answer: "SELECT category FROM records GROUP BY category ORDER BY COUNT(*)"
|
|
558
|
+
}),
|
|
559
|
+
example({
|
|
560
|
+
question: "Show labels shared by rows with metric > 100 and rows with metric < 10.",
|
|
561
|
+
answer: "SELECT label FROM records WHERE metric > 100 INTERSECT SELECT label FROM records WHERE metric < 10"
|
|
562
|
+
}),
|
|
563
|
+
example({
|
|
564
|
+
question: "List locations that have not hosted any event.",
|
|
565
|
+
answer: "SELECT location_name FROM locations WHERE location_id NOT IN (SELECT host_location_id FROM events)"
|
|
566
|
+
}),
|
|
567
|
+
example({
|
|
568
|
+
question: "List the most common category across records.",
|
|
569
|
+
answer: "SELECT category FROM records GROUP BY category ORDER BY COUNT(*) DESC LIMIT 1"
|
|
570
|
+
})
|
|
571
|
+
)
|
|
572
|
+
];
|
|
573
|
+
function extractSql(output) {
|
|
574
|
+
const match = output.match(/```sql\n?([\s\S]*?)```/);
|
|
575
|
+
return match ? match[1].trim() : output.trim();
|
|
576
|
+
}
|
|
577
|
+
function scopeDialectFor(adapter) {
|
|
578
|
+
const dialect = FORMATTER_SCOPE_DIALECTS.get(adapter.formatterLanguage);
|
|
579
|
+
if (!dialect) {
|
|
580
|
+
throw new TypeError(
|
|
581
|
+
`No scope dialect mapping for formatter language "${adapter.formatterLanguage}".`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
return dialect;
|
|
585
|
+
}
|
|
586
|
+
function fencedMarkdownParseError(sql, adapter) {
|
|
587
|
+
return new SQLValidationError(
|
|
588
|
+
JSON.stringify(
|
|
589
|
+
buildScopeParseErrorPayload(
|
|
590
|
+
sql,
|
|
591
|
+
scopeDialectFor(adapter),
|
|
592
|
+
new Error("SQL response was fenced markdown, not executable SQL.")
|
|
593
|
+
)
|
|
594
|
+
)
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
async function toSql(options) {
|
|
598
|
+
const { maxRetries = 3 } = options;
|
|
599
|
+
return withRetry(
|
|
600
|
+
async (attemptNumber, errors, attempts) => {
|
|
601
|
+
const context = new ContextEngine({
|
|
602
|
+
store: new InMemoryContextStore(),
|
|
603
|
+
chatId: `sql-gen-${crypto.randomUUID()}`,
|
|
604
|
+
userId: "system"
|
|
605
|
+
});
|
|
606
|
+
context.set(
|
|
607
|
+
persona({
|
|
608
|
+
name: "Freya",
|
|
609
|
+
role: SQL_AGENT_ROLE,
|
|
610
|
+
objective: SQL_AGENT_OBJECTIVE
|
|
611
|
+
// role: `You are a data science expert that provides well-reasoned and detailed responses.`,
|
|
612
|
+
// objective: `Your task is to understand the schema and generate a valid SQL query to answer the question. You first think about the reasoning process as an internal monologue and then provide the user with the answer.`,
|
|
613
|
+
}),
|
|
614
|
+
...SQL_AGENT_POLICIES,
|
|
615
|
+
...options.fragments
|
|
616
|
+
);
|
|
617
|
+
if (errors.length) {
|
|
618
|
+
const lastError = errors.at(-1);
|
|
619
|
+
context.set(
|
|
620
|
+
user(dedent`
|
|
621
|
+
Answer the following question with the SQL code. Use the piece of evidence and base your answer on the database schema.
|
|
622
|
+
Given the question, the evidence and the database schema, return the SQL script that addresses the question.
|
|
623
|
+
|
|
624
|
+
Question: ${options.input}
|
|
625
|
+
`),
|
|
626
|
+
UnanswerableSQLError.isInstance(lastError) ? user(
|
|
627
|
+
`<retry_instruction>Your previous response marked the task as unanswerable. Re-evaluate using best-effort schema mapping. If the core intent is answerable with existing tables/columns, return SQL. Return error only when required core intent cannot be mapped without inventing schema elements.</retry_instruction>`
|
|
628
|
+
) : user(
|
|
629
|
+
`<validation_error>Your previous SQL query had the following error: ${lastError?.message}. Please fix the query.</validation_error>`
|
|
630
|
+
)
|
|
631
|
+
);
|
|
632
|
+
} else {
|
|
633
|
+
context.set(
|
|
634
|
+
user(dedent`
|
|
635
|
+
Answer the following question with the SQL code. Use the piece of evidence and base your answer on the database schema.
|
|
636
|
+
Given the question, the evidence and the database schema, return the SQL script that addresses the question.
|
|
637
|
+
|
|
638
|
+
Question: ${options.input}
|
|
639
|
+
`)
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
const temperature = RETRY_TEMPERATURES[attemptNumber - 1] ?? RETRY_TEMPERATURES[RETRY_TEMPERATURES.length - 1];
|
|
643
|
+
const baseModel = options.model ?? groq("openai/gpt-oss-20b");
|
|
644
|
+
const model = wrapLanguageModel({
|
|
645
|
+
model: baseModel,
|
|
646
|
+
middleware: defaultSettingsMiddleware({ settings: { temperature } })
|
|
647
|
+
});
|
|
648
|
+
const sqlOutput = structuredOutput({
|
|
649
|
+
model,
|
|
650
|
+
context,
|
|
651
|
+
schema: z.object({
|
|
652
|
+
result: z.union([
|
|
653
|
+
z.object({
|
|
654
|
+
sql: z.string().describe("The SQL query that answers the question"),
|
|
655
|
+
reasoning: z.string().describe("The reasoning steps taken to generate the SQL")
|
|
656
|
+
}),
|
|
657
|
+
z.object({
|
|
658
|
+
error: z.string().describe(
|
|
659
|
+
"Error message explaining why the question cannot be answered with the given schema"
|
|
660
|
+
)
|
|
661
|
+
})
|
|
662
|
+
])
|
|
663
|
+
})
|
|
664
|
+
});
|
|
665
|
+
const { result: output } = await sqlOutput.generate();
|
|
666
|
+
const finalizeSql = async (rawSql) => {
|
|
667
|
+
const extractedSql = extractSql(rawSql);
|
|
668
|
+
const errorsOrUndefined = errors.length ? errors.map(formatErrorMessage) : void 0;
|
|
669
|
+
if (extractedSql === "") {
|
|
670
|
+
return {
|
|
671
|
+
attempts,
|
|
672
|
+
sql: "",
|
|
673
|
+
errors: errorsOrUndefined
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
const sql = options.adapter.format(extractedSql);
|
|
677
|
+
if (rawSql.trimStart().startsWith("```") && extractedSql === rawSql.trim()) {
|
|
678
|
+
throw fencedMarkdownParseError(sql, options.adapter);
|
|
679
|
+
}
|
|
680
|
+
const validationError = await options.adapter.validate(sql);
|
|
681
|
+
if (validationError) {
|
|
682
|
+
throw new SQLValidationError(validationError);
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
attempts,
|
|
686
|
+
sql,
|
|
687
|
+
errors: errorsOrUndefined
|
|
688
|
+
};
|
|
689
|
+
};
|
|
690
|
+
if ("error" in output) {
|
|
691
|
+
context.set(
|
|
692
|
+
user(
|
|
693
|
+
"<best_effort_fallback>Do not return unanswerable. Produce the best valid SQL query that answers the core intent using only available schema entities.</best_effort_fallback>"
|
|
694
|
+
)
|
|
695
|
+
);
|
|
696
|
+
const forcedSqlOutput = structuredOutput({
|
|
697
|
+
model,
|
|
698
|
+
context,
|
|
699
|
+
schema: z.object({
|
|
700
|
+
sql: z.string().describe(
|
|
701
|
+
"Best-effort SQL query that answers the core intent using only available schema entities."
|
|
702
|
+
),
|
|
703
|
+
reasoning: z.string().describe("Reasoning steps for best-effort schema mapping.")
|
|
704
|
+
})
|
|
705
|
+
});
|
|
706
|
+
try {
|
|
707
|
+
const forced = await forcedSqlOutput.generate();
|
|
708
|
+
return await finalizeSql(forced.sql);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
if (SQLValidationError.isInstance(error) || APICallError.isInstance(error) || JSONParseError.isInstance(error) || TypeValidationError.isInstance(error) || NoObjectGeneratedError.isInstance(error) || NoOutputGeneratedError.isInstance(error) || NoContentGeneratedError.isInstance(error)) {
|
|
711
|
+
throw error;
|
|
712
|
+
}
|
|
713
|
+
throw new UnanswerableSQLError(output.error);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return await finalizeSql(output.sql);
|
|
717
|
+
},
|
|
718
|
+
{ retries: maxRetries - 1 }
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
function formatErrorMessage(error) {
|
|
722
|
+
if (APICallError.isInstance(error)) {
|
|
723
|
+
if (error.message.startsWith("Failed to validate JSON")) {
|
|
724
|
+
return `Schema validation failed: ${error.message}`;
|
|
725
|
+
}
|
|
726
|
+
return error.message;
|
|
727
|
+
}
|
|
728
|
+
if (SQLValidationError.isInstance(error)) {
|
|
729
|
+
return `SQL Validation Error: ${error.message}`;
|
|
730
|
+
}
|
|
731
|
+
return error.message;
|
|
732
|
+
}
|
|
733
|
+
function isModelUnavailableError(error) {
|
|
734
|
+
if (!APICallError.isInstance(error)) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
const message = error.message.toLowerCase();
|
|
738
|
+
const responseBody = (error.responseBody ?? "").toLowerCase();
|
|
739
|
+
const is404ModelError = error.statusCode === 404 && (message.includes("model") || responseBody.includes("model_not_found"));
|
|
740
|
+
const errorCode = typeof error.data === "object" && error.data !== null && "error" in error.data && typeof error.data.error === "object" && error.data.error !== null && "code" in error.data.error && typeof error.data.error.code === "string" ? error.data.error.code.toLowerCase() : void 0;
|
|
741
|
+
return is404ModelError || errorCode === "model_not_found" || responseBody.includes('"code":"model_not_found"') || message.includes("model") && message.includes("does not exist or you do not have access to it");
|
|
742
|
+
}
|
|
743
|
+
async function withRetry(computation, options = { retries: 3 }) {
|
|
744
|
+
const errors = [];
|
|
745
|
+
let attempts = 0;
|
|
746
|
+
return pRetry(
|
|
747
|
+
(attemptNumber) => {
|
|
748
|
+
return computation(attemptNumber, errors, ++attempts);
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
retries: options.retries,
|
|
752
|
+
shouldRetry: (context) => {
|
|
753
|
+
if (UnanswerableSQLError.isInstance(context.error)) {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
if (isModelUnavailableError(context.error)) {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
if (SQLValidationError.isInstance(context.error)) {
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
console.log({
|
|
763
|
+
NoObjectGeneratedError: NoObjectGeneratedError.isInstance(
|
|
764
|
+
context.error
|
|
765
|
+
),
|
|
766
|
+
NoOutputGeneratedError: NoOutputGeneratedError.isInstance(
|
|
767
|
+
context.error
|
|
768
|
+
),
|
|
769
|
+
APICallError: APICallError.isInstance(context.error),
|
|
770
|
+
JSONParseError: JSONParseError.isInstance(context.error),
|
|
771
|
+
TypeValidationError: TypeValidationError.isInstance(context.error),
|
|
772
|
+
NoContentGeneratedError: NoContentGeneratedError.isInstance(
|
|
773
|
+
context.error
|
|
774
|
+
)
|
|
775
|
+
});
|
|
776
|
+
return APICallError.isInstance(context.error) || JSONParseError.isInstance(context.error) || TypeValidationError.isInstance(context.error) || NoObjectGeneratedError.isInstance(context.error) || NoOutputGeneratedError.isInstance(context.error) || NoContentGeneratedError.isInstance(context.error);
|
|
777
|
+
},
|
|
778
|
+
onFailedAttempt(context) {
|
|
779
|
+
errors.push(context.error);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// packages/text2sql/src/lib/sql.ts
|
|
786
|
+
var text2SqlValidationMarker = Symbol("Text2SqlValidationError");
|
|
787
|
+
var text2SqlUnknownAdapterMarker = Symbol("Text2SqlUnknownAdapterError");
|
|
788
|
+
var Text2SqlValidationError = class _Text2SqlValidationError extends Error {
|
|
789
|
+
[text2SqlValidationMarker];
|
|
790
|
+
constructor(message) {
|
|
791
|
+
super(message);
|
|
792
|
+
this.name = "Text2SqlValidationError";
|
|
793
|
+
this[text2SqlValidationMarker] = true;
|
|
794
|
+
}
|
|
795
|
+
static isInstance(error) {
|
|
796
|
+
return error instanceof _Text2SqlValidationError && error[text2SqlValidationMarker] === true;
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
var Text2SqlUnknownAdapterError = class _Text2SqlUnknownAdapterError extends Error {
|
|
800
|
+
[text2SqlUnknownAdapterMarker];
|
|
801
|
+
adapter;
|
|
802
|
+
available;
|
|
803
|
+
constructor(adapter, available) {
|
|
804
|
+
const list = available.join(", ") || "(none configured)";
|
|
805
|
+
super(`Unknown adapter "${adapter}". Available: ${list}`);
|
|
806
|
+
this.name = "Text2SqlUnknownAdapterError";
|
|
807
|
+
this.adapter = adapter;
|
|
808
|
+
this.available = available;
|
|
809
|
+
this[text2SqlUnknownAdapterMarker] = true;
|
|
810
|
+
}
|
|
811
|
+
static isInstance(error) {
|
|
812
|
+
return error instanceof _Text2SqlUnknownAdapterError && error[text2SqlUnknownAdapterMarker] === true;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
var Text2Sql = class {
|
|
816
|
+
#config;
|
|
817
|
+
#indexer;
|
|
818
|
+
constructor(config) {
|
|
819
|
+
const adapterNames = Object.keys(config.adapters);
|
|
820
|
+
if (adapterNames.length === 0) {
|
|
821
|
+
throw new Error("Text2Sql requires at least one adapter");
|
|
822
|
+
}
|
|
823
|
+
validateAdapterNames(adapterNames);
|
|
824
|
+
this.#config = config;
|
|
825
|
+
this.#indexer = new AdapterIndexer({
|
|
826
|
+
adapters: config.adapters,
|
|
827
|
+
version: config.version
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
#requireAdapter(name) {
|
|
831
|
+
const adapter = this.#config.adapters[name];
|
|
832
|
+
if (!adapter) {
|
|
833
|
+
throw new Text2SqlUnknownAdapterError(name, this.adapterNames());
|
|
834
|
+
}
|
|
835
|
+
return adapter;
|
|
836
|
+
}
|
|
837
|
+
#requireModel(op) {
|
|
838
|
+
const model = this.#config.model;
|
|
839
|
+
if (!model) {
|
|
840
|
+
throw new Error(`Text2Sql.${op}() requires a model in Text2SqlConfig`);
|
|
841
|
+
}
|
|
842
|
+
return model;
|
|
843
|
+
}
|
|
844
|
+
adapterNames() {
|
|
845
|
+
return Object.keys(this.#config.adapters);
|
|
846
|
+
}
|
|
847
|
+
hasAdapter(name) {
|
|
848
|
+
return name in this.#config.adapters;
|
|
849
|
+
}
|
|
850
|
+
async validate(name, sql) {
|
|
851
|
+
const adapter = this.#requireAdapter(name);
|
|
852
|
+
const result = this.#validateWith(adapter, sql);
|
|
853
|
+
return result;
|
|
854
|
+
}
|
|
855
|
+
async run(name, sql) {
|
|
856
|
+
const adapter = this.#requireAdapter(name);
|
|
857
|
+
const formatted = await this.#validateWith(adapter, sql);
|
|
858
|
+
const result = await adapter.execute(formatted);
|
|
859
|
+
if (!Array.isArray(result)) {
|
|
860
|
+
throw new Error("adapter.execute must return an array of rows");
|
|
861
|
+
}
|
|
862
|
+
const columns = result.length > 0 ? Object.keys(result[0]) : [];
|
|
863
|
+
return { rows: result, columns };
|
|
864
|
+
}
|
|
865
|
+
async #validateWith(adapter, sql) {
|
|
866
|
+
const formatted = adapter.format(sql);
|
|
867
|
+
const error = await adapter.validate(formatted);
|
|
868
|
+
if (error) throw new Text2SqlValidationError(error);
|
|
869
|
+
return formatted;
|
|
870
|
+
}
|
|
871
|
+
async index(options = {}) {
|
|
872
|
+
return this.#indexer.index({
|
|
873
|
+
adapterNames: options.names,
|
|
874
|
+
onProgress: options.onProgress
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
async toSql(input, adapterName) {
|
|
878
|
+
const adapter = this.#requireAdapter(adapterName);
|
|
879
|
+
const model = this.#requireModel("toSql");
|
|
880
|
+
const fragments = await this.#indexer.indexAdapter(adapterName);
|
|
881
|
+
const result = await toSql({
|
|
882
|
+
input,
|
|
883
|
+
adapter,
|
|
884
|
+
fragments,
|
|
885
|
+
model
|
|
886
|
+
});
|
|
887
|
+
return result.sql;
|
|
888
|
+
}
|
|
889
|
+
async toPairs(adapterName, factory) {
|
|
890
|
+
const adapter = this.#requireAdapter(adapterName);
|
|
891
|
+
const producer = factory(adapter);
|
|
892
|
+
return producer.toPairs();
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
// packages/text2sql/src/bin/command.ts
|
|
897
|
+
import * as path2 from "node:path";
|
|
898
|
+
var OUT_DIR_OPTION = {
|
|
899
|
+
flag: "--out-dir <path>",
|
|
900
|
+
description: "Output directory (default: $TEXT2SQL_OUT_DIR or ./sql)"
|
|
901
|
+
};
|
|
902
|
+
function resolveOutputDir(ctx, options) {
|
|
903
|
+
const explicit = typeof options.outDir === "string" ? options.outDir : void 0;
|
|
904
|
+
return path2.resolve(ctx.cwd, explicit ?? ctx.env.TEXT2SQL_OUT_DIR ?? "./sql");
|
|
905
|
+
}
|
|
906
|
+
var CommandError = class extends Error {
|
|
907
|
+
command;
|
|
908
|
+
exitCode;
|
|
909
|
+
constructor(command, message, exitCode = 1) {
|
|
910
|
+
super(message);
|
|
911
|
+
this.name = "CommandError";
|
|
912
|
+
this.command = command;
|
|
913
|
+
this.exitCode = exitCode;
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
function renderCommandError(error, stderr) {
|
|
917
|
+
stderr.write(`sql ${error.command}: ${error.message}
|
|
918
|
+
`);
|
|
919
|
+
}
|
|
390
920
|
function errorMessage2(error) {
|
|
391
921
|
return error instanceof Error ? error.message : String(error);
|
|
392
922
|
}
|
|
923
|
+
var SqlCommand = class {
|
|
924
|
+
options = [];
|
|
925
|
+
helpDisplay;
|
|
926
|
+
fail(message, exitCode = 1) {
|
|
927
|
+
throw new CommandError(this.name, message, exitCode);
|
|
928
|
+
}
|
|
929
|
+
requireAdapterName(text2Sql, db) {
|
|
930
|
+
const available = text2Sql.adapterNames().join(", ") || "(none configured)";
|
|
931
|
+
if (!db) {
|
|
932
|
+
this.fail(
|
|
933
|
+
`missing database name. Usage: sql ${this.name} ${this.usage}. Available: ${available}`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
if (!text2Sql.hasAdapter(db)) {
|
|
937
|
+
this.fail(`unknown database "${db}". Available: ${available}`);
|
|
938
|
+
}
|
|
939
|
+
return db;
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
var SqlQueryCommand = class extends SqlCommand {
|
|
943
|
+
args = "<db> <...sql>";
|
|
944
|
+
async execute(ctx, args, options) {
|
|
945
|
+
const [db, sqlParts] = args;
|
|
946
|
+
const name = this.requireAdapterName(ctx.text2Sql, db);
|
|
947
|
+
const sql = (sqlParts ?? []).join(" ").trim();
|
|
948
|
+
if (!sql) this.fail("no query provided");
|
|
949
|
+
try {
|
|
950
|
+
return await this.runQuery(ctx, name, sql, options);
|
|
951
|
+
} catch (error) {
|
|
952
|
+
if (error instanceof CommandError) throw error;
|
|
953
|
+
if (Text2SqlValidationError.isInstance(error)) this.fail(error.message);
|
|
954
|
+
this.fail(errorMessage2(error));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
// packages/text2sql/src/bin/commands/index.ts
|
|
960
|
+
import { constants, createWriteStream } from "node:fs";
|
|
961
|
+
import {
|
|
962
|
+
mkdir,
|
|
963
|
+
open,
|
|
964
|
+
stat,
|
|
965
|
+
writeFile as writeFile2
|
|
966
|
+
} from "node:fs/promises";
|
|
967
|
+
import * as path3 from "node:path";
|
|
968
|
+
import { finished } from "node:stream/promises";
|
|
969
|
+
import { v7 } from "uuid";
|
|
970
|
+
|
|
971
|
+
// packages/text2sql/src/bin/commands/indexing-formatter.ts
|
|
972
|
+
var REDUNDANT_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
973
|
+
"adapter:start",
|
|
974
|
+
"adapter:end"
|
|
975
|
+
]);
|
|
976
|
+
function formatPretty(event) {
|
|
977
|
+
const head = event.adapter ? `${event.type} ${event.adapter}` : event.type;
|
|
978
|
+
const parts = [`[${head}]`];
|
|
979
|
+
if (event.phase) parts.push(`phase=${event.phase}`);
|
|
980
|
+
if (event.table) parts.push(`table=${event.table}`);
|
|
981
|
+
if (typeof event.current === "number" && typeof event.total === "number" && !event.type.endsWith(":start")) {
|
|
982
|
+
parts.push(`${event.current}/${event.total}`);
|
|
983
|
+
}
|
|
984
|
+
if (event.cached) parts.push("cached");
|
|
985
|
+
if (!REDUNDANT_MESSAGE_TYPES.has(event.type)) parts.push(event.message);
|
|
986
|
+
return parts.join(" ");
|
|
987
|
+
}
|
|
393
988
|
|
|
394
989
|
// packages/text2sql/src/bin/commands/index.ts
|
|
395
990
|
var IndexCommand = class extends SqlCommand {
|
|
396
991
|
name = "index";
|
|
397
992
|
description = "Index adapter schemas and write context artifacts";
|
|
398
993
|
args = "[...adapters]";
|
|
399
|
-
usage = "[--all] [adapter ...]";
|
|
400
|
-
helpDisplay = "[--all] [adapter ...]";
|
|
994
|
+
usage = "[--all] [--verbose [pretty|json]] [adapter ...]";
|
|
995
|
+
helpDisplay = "[--all] [--verbose] [adapter ...]";
|
|
401
996
|
options = [
|
|
402
|
-
{ flag: "--all", description: "Index all adapters (default)" }
|
|
997
|
+
{ flag: "--all", description: "Index all adapters (default)" },
|
|
998
|
+
{
|
|
999
|
+
flag: "-v, --verbose [format]",
|
|
1000
|
+
description: "Mirror progress events to stderr (format: pretty (default) | json)"
|
|
1001
|
+
},
|
|
1002
|
+
OUT_DIR_OPTION
|
|
403
1003
|
];
|
|
404
1004
|
async execute(ctx, args, options) {
|
|
405
1005
|
const adapterNames = args[0] ?? [];
|
|
406
1006
|
const requested = options.all ? [] : adapterNames;
|
|
407
|
-
const names = this.resolveNames(ctx.
|
|
408
|
-
const
|
|
1007
|
+
const names = this.resolveNames(ctx.text2Sql, requested);
|
|
1008
|
+
const verbose = this.resolveVerbose(options.verbose);
|
|
1009
|
+
const outputDir = resolveOutputDir(ctx, options);
|
|
409
1010
|
const id = v7();
|
|
410
|
-
const fragmentsPath =
|
|
411
|
-
const
|
|
1011
|
+
const fragmentsPath = path3.join(outputDir, `index-${id}.json`);
|
|
1012
|
+
const explicitEventsPath = resolveExplicitEventsPath(ctx);
|
|
1013
|
+
const eventsPath = explicitEventsPath ?? path3.join(outputDir, `index-${id}.events.ndjson`);
|
|
412
1014
|
try {
|
|
413
1015
|
await mkdir(outputDir, { recursive: true });
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
onProgress: (event) => appendProgressEvent(eventsPath, event)
|
|
419
|
-
});
|
|
420
|
-
await writeFile2(fragmentsPath, JSON.stringify(fragments, null, 2));
|
|
421
|
-
const manifest = {
|
|
422
|
-
fragmentsPath,
|
|
1016
|
+
if (explicitEventsPath) {
|
|
1017
|
+
await mkdir(path3.dirname(explicitEventsPath), { recursive: true });
|
|
1018
|
+
}
|
|
1019
|
+
const eventsStream = await openEventsWriter(
|
|
423
1020
|
eventsPath,
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
1021
|
+
explicitEventsPath,
|
|
1022
|
+
ctx.stderr
|
|
1023
|
+
);
|
|
1024
|
+
try {
|
|
1025
|
+
const fragments = await ctx.text2Sql.index({
|
|
1026
|
+
names,
|
|
1027
|
+
onProgress: createProgressHandler(eventsStream, verbose, ctx.stderr)
|
|
1028
|
+
});
|
|
1029
|
+
await writeFile2(fragmentsPath, JSON.stringify(fragments, null, 2));
|
|
1030
|
+
const manifest = {
|
|
1031
|
+
fragmentsPath,
|
|
1032
|
+
eventsPath,
|
|
1033
|
+
adapters: names,
|
|
1034
|
+
fragments: countSchemaFragments(fragments)
|
|
1035
|
+
};
|
|
1036
|
+
ctx.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
|
|
1037
|
+
return 0;
|
|
1038
|
+
} finally {
|
|
1039
|
+
eventsStream.end();
|
|
1040
|
+
await finished(eventsStream).catch(() => {
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
432
1043
|
} catch (error) {
|
|
433
|
-
this.fail(
|
|
1044
|
+
this.fail(errorMessage2(error));
|
|
434
1045
|
}
|
|
435
1046
|
}
|
|
436
|
-
resolveNames(
|
|
437
|
-
const availableNames =
|
|
1047
|
+
resolveNames(text2Sql, requested) {
|
|
1048
|
+
const availableNames = text2Sql.adapterNames();
|
|
438
1049
|
const available = availableNames.join(", ") || "(none configured)";
|
|
439
1050
|
const names = requested.length === 0 ? availableNames : dedupe(requested);
|
|
440
1051
|
for (const name of names) {
|
|
441
|
-
if (!
|
|
1052
|
+
if (!text2Sql.hasAdapter(name)) {
|
|
442
1053
|
this.fail(`unknown adapter "${name}". Available: ${available}`);
|
|
443
1054
|
}
|
|
444
1055
|
}
|
|
445
1056
|
return names;
|
|
446
1057
|
}
|
|
1058
|
+
resolveVerbose(value) {
|
|
1059
|
+
if (value === void 0 || value === false) return null;
|
|
1060
|
+
if (value === true || value === "" || value === "pretty") return "pretty";
|
|
1061
|
+
if (value === "json") return "json";
|
|
1062
|
+
this.fail(
|
|
1063
|
+
`invalid --verbose value "${String(value)}". Expected "pretty" or "json".`
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
447
1066
|
};
|
|
448
|
-
function
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
JSON.stringify(
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
1067
|
+
function createProgressHandler(eventsStream, verbose, stderr) {
|
|
1068
|
+
return (event) => {
|
|
1069
|
+
const stamped = { ...event, timestampMs: event.timestampMs ?? Date.now() };
|
|
1070
|
+
const json = JSON.stringify(stamped) + "\n";
|
|
1071
|
+
eventsStream.write(json);
|
|
1072
|
+
if (verbose === "pretty") stderr.write(formatPretty(stamped) + "\n");
|
|
1073
|
+
else if (verbose === "json") stderr.write(json);
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function resolveExplicitEventsPath(ctx) {
|
|
1077
|
+
const explicit = ctx.env.TEXT2SQL_INDEX_EVENTS_PATH?.trim();
|
|
1078
|
+
return explicit ? path3.resolve(ctx.cwd, explicit) : null;
|
|
1079
|
+
}
|
|
1080
|
+
async function openEventsWriter(eventsPath, explicitEventsPath, stderr) {
|
|
1081
|
+
const probe = explicitEventsPath ? await probeFifoWriter(explicitEventsPath, stderr) : null;
|
|
1082
|
+
const stream = createWriteStream(eventsPath, {
|
|
1083
|
+
flags: "a",
|
|
1084
|
+
encoding: "utf-8"
|
|
1085
|
+
});
|
|
1086
|
+
try {
|
|
1087
|
+
await waitForOpen(stream);
|
|
1088
|
+
return stream;
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
stream.destroy();
|
|
1091
|
+
throw err;
|
|
1092
|
+
} finally {
|
|
1093
|
+
await probe?.close().catch(() => {
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
async function probeFifoWriter(eventsPath, stderr) {
|
|
1098
|
+
let stats;
|
|
1099
|
+
try {
|
|
1100
|
+
stats = await stat(eventsPath);
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
if (err.code === "ENOENT") return null;
|
|
1103
|
+
throw err;
|
|
1104
|
+
}
|
|
1105
|
+
if (!stats.isFIFO()) return null;
|
|
1106
|
+
try {
|
|
1107
|
+
return await open(eventsPath, constants.O_WRONLY | constants.O_NONBLOCK);
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
if (err.code === "ENXIO") {
|
|
1110
|
+
stderr.write(`sql index: waiting for reader on FIFO ${eventsPath}...
|
|
1111
|
+
`);
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
throw err;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function waitForOpen(stream) {
|
|
1118
|
+
return new Promise((resolve4, reject) => {
|
|
1119
|
+
if (!stream.pending) {
|
|
1120
|
+
resolve4();
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const onOpen = () => {
|
|
1124
|
+
stream.off("error", onError);
|
|
1125
|
+
resolve4();
|
|
1126
|
+
};
|
|
1127
|
+
const onError = (err) => {
|
|
1128
|
+
stream.off("open", onOpen);
|
|
1129
|
+
reject(err);
|
|
1130
|
+
};
|
|
1131
|
+
stream.once("open", onOpen);
|
|
1132
|
+
stream.once("error", onError);
|
|
1133
|
+
});
|
|
456
1134
|
}
|
|
457
1135
|
function countSchemaFragments(fragments) {
|
|
458
1136
|
return fragments.reduce((count, adapterFragment) => {
|
|
@@ -468,54 +1146,31 @@ function dedupe(values) {
|
|
|
468
1146
|
|
|
469
1147
|
// packages/text2sql/src/bin/commands/run.ts
|
|
470
1148
|
import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
|
|
471
|
-
import * as
|
|
1149
|
+
import * as path4 from "node:path";
|
|
472
1150
|
import { v7 as v72 } from "uuid";
|
|
473
1151
|
var RunCommand = class extends SqlQueryCommand {
|
|
474
1152
|
name = "run";
|
|
475
1153
|
description = "Execute query against <db> and store results";
|
|
476
1154
|
usage = '<db> "SELECT ..." [--out-dir <path>]';
|
|
477
1155
|
helpDisplay = '<db> "SELECT ..."';
|
|
478
|
-
options = [
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
];
|
|
484
|
-
async afterValidation(ctx, adapter, sql, options) {
|
|
485
|
-
const outputDir = this.resolveOutputDir(ctx, options);
|
|
486
|
-
let rows;
|
|
487
|
-
try {
|
|
488
|
-
rows = await adapter.execute(sql);
|
|
489
|
-
} catch (error) {
|
|
490
|
-
this.fail(errorMessage(error));
|
|
491
|
-
}
|
|
492
|
-
if (!Array.isArray(rows)) {
|
|
493
|
-
this.fail("adapter.execute must return an array of rows");
|
|
494
|
-
}
|
|
495
|
-
const outPath = path3.join(outputDir, `${v72()}.json`);
|
|
1156
|
+
options = [OUT_DIR_OPTION];
|
|
1157
|
+
async runQuery(ctx, name, sql, options) {
|
|
1158
|
+
const outputDir = resolveOutputDir(ctx, options);
|
|
1159
|
+
const { rows, columns } = await ctx.text2Sql.run(name, sql);
|
|
1160
|
+
const outPath = path4.join(outputDir, `${v72()}.json`);
|
|
496
1161
|
try {
|
|
497
1162
|
await mkdir2(outputDir, { recursive: true });
|
|
498
1163
|
await writeFile3(outPath, JSON.stringify(rows, null, 2));
|
|
499
1164
|
} catch (error) {
|
|
500
|
-
this.fail(
|
|
1165
|
+
this.fail(errorMessage2(error));
|
|
501
1166
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
stderr: "",
|
|
510
|
-
exitCode: 0
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
resolveOutputDir(ctx, options) {
|
|
514
|
-
const explicit = typeof options.outDir === "string" ? options.outDir : void 0;
|
|
515
|
-
return path3.resolve(
|
|
516
|
-
ctx.cwd,
|
|
517
|
-
explicit ?? ctx.env.TEXT2SQL_OUT_DIR ?? "./sql"
|
|
518
|
-
);
|
|
1167
|
+
ctx.stdout.write(`results stored in ${outPath}
|
|
1168
|
+
`);
|
|
1169
|
+
ctx.stdout.write(`columns: ${columns.join(", ") || "(none)"}
|
|
1170
|
+
`);
|
|
1171
|
+
ctx.stdout.write(`rows: ${rows.length}
|
|
1172
|
+
`);
|
|
1173
|
+
return 0;
|
|
519
1174
|
}
|
|
520
1175
|
};
|
|
521
1176
|
|
|
@@ -525,8 +1180,10 @@ var ValidateCommand = class extends SqlQueryCommand {
|
|
|
525
1180
|
description = "Validate query syntax against <db>";
|
|
526
1181
|
usage = '<db> "SELECT ..."';
|
|
527
1182
|
helpDisplay = '<db> "SELECT ..."';
|
|
528
|
-
async
|
|
529
|
-
|
|
1183
|
+
async runQuery(ctx, name, sql) {
|
|
1184
|
+
await ctx.text2Sql.validate(name, sql);
|
|
1185
|
+
ctx.stdout.write("valid\n");
|
|
1186
|
+
return 0;
|
|
530
1187
|
}
|
|
531
1188
|
};
|
|
532
1189
|
|
|
@@ -553,7 +1210,7 @@ async function loadAdapters() {
|
|
|
553
1210
|
mod = await import(specifier);
|
|
554
1211
|
} catch (cause) {
|
|
555
1212
|
throw new Error(
|
|
556
|
-
`TEXT2SQL_ADAPTERS=${target}: failed to import module - ${
|
|
1213
|
+
`TEXT2SQL_ADAPTERS=${target}: failed to import module - ${errorMessage2(cause)}`
|
|
557
1214
|
);
|
|
558
1215
|
}
|
|
559
1216
|
const exported = mod.default;
|
|
@@ -578,7 +1235,7 @@ async function loadAdapters() {
|
|
|
578
1235
|
try {
|
|
579
1236
|
validateAdapterNames(entries.map(([name]) => name));
|
|
580
1237
|
} catch (cause) {
|
|
581
|
-
throw new Error(`TEXT2SQL_ADAPTERS=${target}: ${
|
|
1238
|
+
throw new Error(`TEXT2SQL_ADAPTERS=${target}: ${errorMessage2(cause)}`);
|
|
582
1239
|
}
|
|
583
1240
|
return exported;
|
|
584
1241
|
}
|
|
@@ -603,7 +1260,7 @@ cli.usage("<run|validate|index> ...").help(
|
|
|
603
1260
|
for (const command of commands) {
|
|
604
1261
|
registerCommand(cli, command);
|
|
605
1262
|
}
|
|
606
|
-
process.
|
|
1263
|
+
process.exitCode = await runCli();
|
|
607
1264
|
async function runCli() {
|
|
608
1265
|
try {
|
|
609
1266
|
cli.parse(process.argv, { run: false });
|
|
@@ -642,23 +1299,27 @@ function registerCommand(cli2, command) {
|
|
|
642
1299
|
async function runCommand(command, positional, options) {
|
|
643
1300
|
let ctx;
|
|
644
1301
|
try {
|
|
645
|
-
|
|
1302
|
+
const text2Sql = new Text2Sql({
|
|
646
1303
|
adapters: await loadAdapters(),
|
|
1304
|
+
version: process.env.TEXT2SQL_INDEX_VERSION
|
|
1305
|
+
});
|
|
1306
|
+
ctx = {
|
|
1307
|
+
text2Sql,
|
|
647
1308
|
cwd: process.cwd(),
|
|
648
|
-
env: process.env
|
|
1309
|
+
env: process.env,
|
|
1310
|
+
stdout: process.stdout,
|
|
1311
|
+
stderr: process.stderr
|
|
649
1312
|
};
|
|
650
1313
|
} catch (error) {
|
|
651
|
-
process.stderr.write(`${
|
|
1314
|
+
process.stderr.write(`${errorMessage2(error)}
|
|
652
1315
|
`);
|
|
653
1316
|
return 2;
|
|
654
1317
|
}
|
|
655
1318
|
try {
|
|
656
|
-
|
|
657
|
-
writeCommandResult(result);
|
|
658
|
-
return result.exitCode;
|
|
1319
|
+
return await command.execute(ctx, positional, options);
|
|
659
1320
|
} catch (error) {
|
|
660
1321
|
if (error instanceof CommandError) {
|
|
661
|
-
|
|
1322
|
+
renderCommandError(error, process.stderr);
|
|
662
1323
|
return error.exitCode;
|
|
663
1324
|
}
|
|
664
1325
|
throw error;
|
|
@@ -673,13 +1334,6 @@ function rewriteCommandsHelp(body) {
|
|
|
673
1334
|
);
|
|
674
1335
|
}, body);
|
|
675
1336
|
}
|
|
676
|
-
function writeCommandResult(result) {
|
|
677
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
678
|
-
if (result.stderr) {
|
|
679
|
-
const out = result.stderr.endsWith("\n") ? result.stderr : result.stderr + "\n";
|
|
680
|
-
process.stderr.write(out);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
1337
|
function writeCliError(error) {
|
|
684
1338
|
if (error instanceof Error && error.name === "CACError") {
|
|
685
1339
|
process.stderr.write(`sql: ${error.message}
|
|
@@ -688,7 +1342,7 @@ function writeCliError(error) {
|
|
|
688
1342
|
return 2;
|
|
689
1343
|
}
|
|
690
1344
|
if (error instanceof CommandError) {
|
|
691
|
-
|
|
1345
|
+
renderCommandError(error, process.stderr);
|
|
692
1346
|
return error.exitCode;
|
|
693
1347
|
}
|
|
694
1348
|
process.stderr.write(
|