@deepagents/text2sql 0.7.0 → 0.8.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.
@@ -0,0 +1,1597 @@
1
+ // packages/text2sql/src/lib/adapters/groundings/context.ts
2
+ function createGroundingContext() {
3
+ return {
4
+ tables: [],
5
+ views: [],
6
+ relationships: [],
7
+ info: void 0
8
+ };
9
+ }
10
+
11
+ // packages/text2sql/src/lib/adapters/adapter.ts
12
+ var Adapter = class {
13
+ async introspect(ctx = createGroundingContext()) {
14
+ const lines = [];
15
+ for (const fn of this.grounding) {
16
+ const grounding = fn(this);
17
+ lines.push({
18
+ tag: grounding.tag,
19
+ fn: await grounding.execute(ctx)
20
+ });
21
+ }
22
+ return lines.map(({ fn, tag }) => {
23
+ const description = fn();
24
+ if (description === null) {
25
+ return "";
26
+ }
27
+ return `<${tag}>
28
+ ${description}
29
+ </${tag}>`;
30
+ }).join("\n");
31
+ }
32
+ /**
33
+ * Convert unknown database value to number.
34
+ * Handles number, bigint, and string types.
35
+ */
36
+ toNumber(value) {
37
+ if (typeof value === "number" && Number.isFinite(value)) {
38
+ return value;
39
+ }
40
+ if (typeof value === "bigint") {
41
+ return Number(value);
42
+ }
43
+ if (typeof value === "string" && value.trim() !== "") {
44
+ const parsed = Number(value);
45
+ return Number.isFinite(parsed) ? parsed : void 0;
46
+ }
47
+ return void 0;
48
+ }
49
+ /**
50
+ * Parse a potentially qualified table name into schema and table parts.
51
+ */
52
+ parseTableName(name) {
53
+ if (name.includes(".")) {
54
+ const [schema, ...rest] = name.split(".");
55
+ return { schema, table: rest.join(".") };
56
+ }
57
+ return { schema: this.defaultSchema ?? "", table: name };
58
+ }
59
+ /**
60
+ * Escape a string value for use in SQL string literals (single quotes).
61
+ * Used in WHERE clauses like: WHERE name = '${escapeString(value)}'
62
+ */
63
+ escapeString(value) {
64
+ return value.replace(/'/g, "''");
65
+ }
66
+ /**
67
+ * Build a SQL filter clause to include/exclude schemas.
68
+ * @param columnName - The schema column name (e.g., 'TABLE_SCHEMA')
69
+ * @param allowedSchemas - If provided, filter to these schemas only
70
+ */
71
+ buildSchemaFilter(columnName, allowedSchemas) {
72
+ if (allowedSchemas && allowedSchemas.length > 0) {
73
+ const values = allowedSchemas.map((s) => `'${this.escapeString(s)}'`).join(", ");
74
+ return `AND ${columnName} IN (${values})`;
75
+ }
76
+ if (this.systemSchemas.length > 0) {
77
+ const values = this.systemSchemas.map((s) => `'${this.escapeString(s)}'`).join(", ");
78
+ return `AND ${columnName} NOT IN (${values})`;
79
+ }
80
+ return "";
81
+ }
82
+ };
83
+
84
+ // packages/text2sql/src/lib/adapters/groundings/abstract.grounding.ts
85
+ var AbstractGrounding = class {
86
+ tag;
87
+ constructor(tag) {
88
+ this.tag = tag;
89
+ }
90
+ };
91
+
92
+ // packages/text2sql/src/lib/adapters/groundings/column-stats.grounding.ts
93
+ var ColumnStatsGrounding = class extends AbstractGrounding {
94
+ constructor(config = {}) {
95
+ super("column_stats");
96
+ }
97
+ /**
98
+ * Execute the grounding process.
99
+ * Annotates columns in ctx.tables and ctx.views with statistics.
100
+ */
101
+ async execute(ctx) {
102
+ const allContainers = [...ctx.tables, ...ctx.views];
103
+ for (const container of allContainers) {
104
+ for (const column of container.columns) {
105
+ try {
106
+ const stats = await this.collectStats(container.name, column);
107
+ if (stats) {
108
+ column.stats = stats;
109
+ }
110
+ } catch (error) {
111
+ console.warn(
112
+ "Error collecting stats for",
113
+ container.name,
114
+ column.name,
115
+ error
116
+ );
117
+ }
118
+ }
119
+ }
120
+ return () => this.#describe();
121
+ }
122
+ #describe() {
123
+ return null;
124
+ }
125
+ };
126
+
127
+ // packages/text2sql/src/lib/adapters/groundings/column-values.grounding.ts
128
+ var ColumnValuesGrounding = class extends AbstractGrounding {
129
+ lowCardinalityLimit;
130
+ constructor(config = {}) {
131
+ super("column_values");
132
+ this.lowCardinalityLimit = config.lowCardinalityLimit ?? 20;
133
+ }
134
+ /**
135
+ * Get values for native ENUM type columns.
136
+ * Return undefined if column is not an ENUM type.
137
+ * Default implementation returns undefined (no native ENUM support).
138
+ */
139
+ async collectEnumValues(_tableName, _column) {
140
+ return void 0;
141
+ }
142
+ /**
143
+ * Parse CHECK constraint for enum-like IN clause.
144
+ * Extracts values from patterns like:
145
+ * - CHECK (status IN ('active', 'inactive'))
146
+ * - CHECK ((status)::text = ANY (ARRAY['a'::text, 'b'::text]))
147
+ * - CHECK (status = 'active' OR status = 'inactive')
148
+ */
149
+ parseCheckConstraint(constraint, columnName) {
150
+ if (constraint.type !== "CHECK" || !constraint.definition) {
151
+ return void 0;
152
+ }
153
+ if (constraint.columns && !constraint.columns.includes(columnName)) {
154
+ return void 0;
155
+ }
156
+ const def = constraint.definition;
157
+ const escapedCol = this.escapeRegex(columnName);
158
+ const colPattern = `(?:\\(?\\(?${escapedCol}\\)?(?:::(?:text|varchar|character varying))?\\)?)`;
159
+ const inMatch = def.match(
160
+ new RegExp(`${colPattern}\\s+IN\\s*\\(([^)]+)\\)`, "i")
161
+ );
162
+ if (inMatch) {
163
+ return this.extractStringValues(inMatch[1]);
164
+ }
165
+ const anyMatch = def.match(
166
+ new RegExp(
167
+ `${colPattern}\\s*=\\s*ANY\\s*\\(\\s*(?:ARRAY)?\\s*\\[([^\\]]+)\\]`,
168
+ "i"
169
+ )
170
+ );
171
+ if (anyMatch) {
172
+ return this.extractStringValues(anyMatch[1]);
173
+ }
174
+ const orPattern = new RegExp(
175
+ `\\b${this.escapeRegex(columnName)}\\b\\s*=\\s*'([^']*)'`,
176
+ "gi"
177
+ );
178
+ const orMatches = [...def.matchAll(orPattern)];
179
+ if (orMatches.length >= 2) {
180
+ return orMatches.map((m) => m[1]);
181
+ }
182
+ return void 0;
183
+ }
184
+ /**
185
+ * Extract string values from a comma-separated list.
186
+ */
187
+ extractStringValues(input) {
188
+ const values = [];
189
+ const matches = input.matchAll(/'([^']*)'/g);
190
+ for (const match of matches) {
191
+ values.push(match[1]);
192
+ }
193
+ return values.length > 0 ? values : void 0;
194
+ }
195
+ /**
196
+ * Escape special regex characters in a string.
197
+ */
198
+ escapeRegex(str) {
199
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
200
+ }
201
+ /**
202
+ * Get the table from context by name.
203
+ */
204
+ getTable(ctx, name) {
205
+ return ctx.tables.find((t) => t.name === name);
206
+ }
207
+ /**
208
+ * Execute the grounding process.
209
+ * Annotates columns in ctx.tables and ctx.views with values.
210
+ */
211
+ async execute(ctx) {
212
+ const allContainers = [...ctx.tables, ...ctx.views];
213
+ for (const container of allContainers) {
214
+ const table = this.getTable(ctx, container.name);
215
+ for (const column of container.columns) {
216
+ try {
217
+ const result = await this.resolveColumnValues(
218
+ container.name,
219
+ column,
220
+ table?.constraints
221
+ );
222
+ if (result) {
223
+ column.kind = result.kind;
224
+ column.values = result.values;
225
+ }
226
+ } catch (error) {
227
+ console.warn(
228
+ "Error collecting column values for",
229
+ container.name,
230
+ column.name,
231
+ error
232
+ );
233
+ }
234
+ }
235
+ }
236
+ return () => this.#describe();
237
+ }
238
+ /**
239
+ * Resolve column values from all sources in priority order.
240
+ */
241
+ async resolveColumnValues(tableName, column, constraints2) {
242
+ const enumValues = await this.collectEnumValues(tableName, column);
243
+ if (enumValues?.length) {
244
+ return { kind: "Enum", values: enumValues };
245
+ }
246
+ if (constraints2) {
247
+ for (const constraint of constraints2) {
248
+ const checkValues = this.parseCheckConstraint(constraint, column.name);
249
+ if (checkValues?.length) {
250
+ return { kind: "Enum", values: checkValues };
251
+ }
252
+ }
253
+ }
254
+ const lowCardValues = await this.collectLowCardinality(tableName, column);
255
+ if (lowCardValues?.length) {
256
+ return { kind: "LowCardinality", values: lowCardValues };
257
+ }
258
+ return void 0;
259
+ }
260
+ #describe() {
261
+ return null;
262
+ }
263
+ };
264
+
265
+ // packages/text2sql/src/lib/adapters/groundings/constraint.grounding.ts
266
+ var ConstraintGrounding = class extends AbstractGrounding {
267
+ constructor(config = {}) {
268
+ super("constraints");
269
+ }
270
+ /**
271
+ * Execute the grounding process.
272
+ * Annotates tables in ctx.tables with their constraints.
273
+ */
274
+ async execute(ctx) {
275
+ for (const table of ctx.tables) {
276
+ try {
277
+ table.constraints = await this.getConstraints(table.name);
278
+ } catch (error) {
279
+ console.warn("Error collecting constraints for", table.name, error);
280
+ }
281
+ }
282
+ return () => null;
283
+ }
284
+ };
285
+
286
+ // packages/text2sql/src/lib/adapters/groundings/indexes.grounding.ts
287
+ var IndexesGrounding = class extends AbstractGrounding {
288
+ constructor(config = {}) {
289
+ super("indexes");
290
+ }
291
+ /**
292
+ * Execute the grounding process.
293
+ * Annotates tables in ctx.tables with their indexes and marks indexed columns.
294
+ */
295
+ async execute(ctx) {
296
+ for (const table of ctx.tables) {
297
+ table.indexes = await this.getIndexes(table.name);
298
+ for (const index of table.indexes ?? []) {
299
+ for (const colName of index.columns) {
300
+ const column = table.columns.find((c) => c.name === colName);
301
+ if (column) {
302
+ column.isIndexed = true;
303
+ }
304
+ }
305
+ }
306
+ }
307
+ return () => null;
308
+ }
309
+ };
310
+
311
+ // packages/text2sql/src/lib/adapters/groundings/info.grounding.ts
312
+ var InfoGrounding = class extends AbstractGrounding {
313
+ constructor(config = {}) {
314
+ super("dialect_info");
315
+ }
316
+ /**
317
+ * Execute the grounding process.
318
+ * Writes database info to ctx.info.
319
+ */
320
+ async execute(ctx) {
321
+ ctx.info = await this.collectInfo();
322
+ const lines = [`Dialect: ${ctx.info.dialect ?? "unknown"}`];
323
+ if (ctx.info.version) {
324
+ lines.push(`Version: ${ctx.info.version}`);
325
+ }
326
+ if (ctx.info.database) {
327
+ lines.push(`Database: ${ctx.info.database}`);
328
+ }
329
+ if (ctx.info.details && Object.keys(ctx.info.details).length) {
330
+ lines.push(`Details: ${JSON.stringify(ctx.info.details)}`);
331
+ }
332
+ return () => lines.join("\n");
333
+ }
334
+ };
335
+
336
+ // packages/text2sql/src/lib/adapters/groundings/report.grounding.ts
337
+ import { groq } from "@ai-sdk/groq";
338
+ import { tool } from "ai";
339
+ import dedent from "dedent";
340
+ import z from "zod";
341
+ import {
342
+ agent,
343
+ generate,
344
+ toState,
345
+ user
346
+ } from "@deepagents/agent";
347
+ var ReportGrounding = class extends AbstractGrounding {
348
+ #adapter;
349
+ #model;
350
+ #cache;
351
+ #forceRefresh;
352
+ constructor(adapter, config = {}) {
353
+ super("business_context");
354
+ this.#adapter = adapter;
355
+ this.#model = config.model ?? groq("openai/gpt-oss-20b");
356
+ this.#cache = config.cache;
357
+ this.#forceRefresh = config.forceRefresh ?? false;
358
+ }
359
+ async execute(ctx) {
360
+ if (!this.#forceRefresh && this.#cache) {
361
+ const cached = await this.#cache.get();
362
+ if (cached) {
363
+ ctx.report = cached;
364
+ return () => cached;
365
+ }
366
+ }
367
+ const report2 = await this.#generateReport();
368
+ ctx.report = report2;
369
+ if (this.#cache) {
370
+ await this.#cache.set(report2);
371
+ }
372
+ return () => report2;
373
+ }
374
+ async #generateReport() {
375
+ const reportAgent = agent({
376
+ name: "db-report-agent",
377
+ model: this.#model,
378
+ prompt: () => dedent`
379
+ <identity>
380
+ You are a database analyst expert. Your job is to understand what
381
+ a database represents and provide business context about it.
382
+ You have READ-ONLY access to the database.
383
+ </identity>
384
+
385
+ <instructions>
386
+ Write a business context that helps another agent answer questions accurately.
387
+
388
+ For EACH table, do queries ONE AT A TIME:
389
+ 1. SELECT COUNT(*) to get row count
390
+ 2. SELECT * LIMIT 3 to see sample data
391
+
392
+ Then write a report with:
393
+ - What business this database is for
394
+ - For each table: purpose, row count, and example of what the data looks like
395
+
396
+ Include concrete examples like "Track prices are $0.99",
397
+ "Customer names like 'Luís Gonçalves'", etc.
398
+
399
+ Keep it 400-600 words, conversational style.
400
+ </instructions>
401
+ `,
402
+ tools: {
403
+ query_database: tool({
404
+ description: "Execute a SELECT query to explore the database and gather insights.",
405
+ inputSchema: z.object({
406
+ sql: z.string().describe("The SELECT query to execute"),
407
+ purpose: z.string().describe(
408
+ "What insight you are trying to gather with this query"
409
+ )
410
+ }),
411
+ execute: ({ sql }, options) => {
412
+ const state = toState(options);
413
+ return state.adapter.execute(sql);
414
+ }
415
+ })
416
+ }
417
+ });
418
+ const { text } = await generate(
419
+ reportAgent,
420
+ [
421
+ user(
422
+ "Please analyze the database and write a contextual report about what this database represents."
423
+ )
424
+ ],
425
+ { adapter: this.#adapter }
426
+ );
427
+ return text;
428
+ }
429
+ };
430
+
431
+ // packages/text2sql/src/lib/adapters/groundings/row-count.grounding.ts
432
+ var RowCountGrounding = class extends AbstractGrounding {
433
+ constructor(config = {}) {
434
+ super("row_counts");
435
+ }
436
+ /**
437
+ * Execute the grounding process.
438
+ * Annotates tables in ctx.tables with row counts and size hints.
439
+ */
440
+ async execute(ctx) {
441
+ for (const table of ctx.tables) {
442
+ const count = await this.getRowCount(table.name);
443
+ if (count != null) {
444
+ table.rowCount = count;
445
+ table.sizeHint = this.#classifyRowCount(count);
446
+ }
447
+ }
448
+ return () => null;
449
+ }
450
+ /**
451
+ * Classify row count into a size hint category.
452
+ */
453
+ #classifyRowCount(count) {
454
+ if (count < 100) return "tiny";
455
+ if (count < 1e3) return "small";
456
+ if (count < 1e4) return "medium";
457
+ if (count < 1e5) return "large";
458
+ return "huge";
459
+ }
460
+ };
461
+
462
+ // packages/text2sql/src/lib/adapters/groundings/table.grounding.ts
463
+ import pluralize from "pluralize";
464
+ var TableGrounding = class extends AbstractGrounding {
465
+ #filter;
466
+ #forward;
467
+ #backward;
468
+ constructor(config = {}) {
469
+ super("tables");
470
+ this.#filter = config.filter;
471
+ this.#forward = config.forward;
472
+ this.#backward = config.backward;
473
+ }
474
+ /**
475
+ * Execute the grounding process.
476
+ * Writes discovered tables and relationships to the context.
477
+ */
478
+ async execute(ctx) {
479
+ const seedTables = await this.applyFilter();
480
+ const forward = this.#forward;
481
+ const backward = this.#backward;
482
+ if (!forward && !backward) {
483
+ const tables3 = await Promise.all(
484
+ seedTables.map((name) => this.getTable(name))
485
+ );
486
+ ctx.tables.push(...tables3);
487
+ return () => this.#describeTables(tables3);
488
+ }
489
+ const tables2 = {};
490
+ const allRelationships = [];
491
+ const seenRelationships = /* @__PURE__ */ new Set();
492
+ const forwardQueue = [];
493
+ const backwardQueue = [];
494
+ const forwardVisited = /* @__PURE__ */ new Set();
495
+ const backwardVisited = /* @__PURE__ */ new Set();
496
+ for (const name of seedTables) {
497
+ if (forward) forwardQueue.push({ name, depth: 0 });
498
+ if (backward) backwardQueue.push({ name, depth: 0 });
499
+ }
500
+ const forwardLimit = forward === true ? Infinity : forward || 0;
501
+ while (forwardQueue.length > 0) {
502
+ const item = forwardQueue.shift();
503
+ if (!item) break;
504
+ const { name, depth } = item;
505
+ if (forwardVisited.has(name)) continue;
506
+ forwardVisited.add(name);
507
+ if (!tables2[name]) {
508
+ tables2[name] = await this.getTable(name);
509
+ }
510
+ if (depth < forwardLimit) {
511
+ const rels = await this.findOutgoingRelations(name);
512
+ for (const rel of rels) {
513
+ this.addRelationship(rel, allRelationships, seenRelationships);
514
+ if (!forwardVisited.has(rel.referenced_table)) {
515
+ forwardQueue.push({ name: rel.referenced_table, depth: depth + 1 });
516
+ }
517
+ }
518
+ }
519
+ }
520
+ const backwardLimit = backward === true ? Infinity : backward || 0;
521
+ while (backwardQueue.length > 0) {
522
+ const item = backwardQueue.shift();
523
+ if (!item) break;
524
+ const { name, depth } = item;
525
+ if (backwardVisited.has(name)) continue;
526
+ backwardVisited.add(name);
527
+ if (!tables2[name]) {
528
+ tables2[name] = await this.getTable(name);
529
+ }
530
+ if (depth < backwardLimit) {
531
+ const rels = await this.findIncomingRelations(name);
532
+ for (const rel of rels) {
533
+ this.addRelationship(rel, allRelationships, seenRelationships);
534
+ if (!backwardVisited.has(rel.table)) {
535
+ backwardQueue.push({ name: rel.table, depth: depth + 1 });
536
+ }
537
+ }
538
+ }
539
+ }
540
+ const tablesList = Object.values(tables2);
541
+ ctx.tables.push(...tablesList);
542
+ ctx.relationships.push(...allRelationships);
543
+ return () => this.#describeTables(tablesList);
544
+ }
545
+ /**
546
+ * Apply the filter to get seed table names.
547
+ * If filter is an explicit array, skip querying all table names.
548
+ */
549
+ async applyFilter() {
550
+ const filter = this.#filter;
551
+ if (Array.isArray(filter)) {
552
+ return filter;
553
+ }
554
+ const names = await this.getAllTableNames();
555
+ if (!filter) {
556
+ return names;
557
+ }
558
+ if (filter instanceof RegExp) {
559
+ return names.filter((name) => filter.test(name));
560
+ }
561
+ return names.filter(filter);
562
+ }
563
+ /**
564
+ * Add a relationship to the collection, deduplicating by key.
565
+ */
566
+ addRelationship(rel, all, seen) {
567
+ const key = `${rel.table}:${rel.from.join(",")}:${rel.referenced_table}:${rel.to.join(",")}`;
568
+ if (!seen.has(key)) {
569
+ seen.add(key);
570
+ all.push(rel);
571
+ }
572
+ }
573
+ #describeTables(tables2) {
574
+ if (!tables2.length) {
575
+ return "Schema unavailable.";
576
+ }
577
+ return tables2.map((table) => {
578
+ const rowCountInfo = table.rowCount != null ? ` [rows: ${table.rowCount}${table.sizeHint ? `, size: ${table.sizeHint}` : ""}]` : "";
579
+ const pkConstraint = table.constraints?.find(
580
+ (c) => c.type === "PRIMARY_KEY"
581
+ );
582
+ const pkColumns = new Set(pkConstraint?.columns ?? []);
583
+ const notNullColumns = new Set(
584
+ table.constraints?.filter((c) => c.type === "NOT_NULL").flatMap((c) => c.columns ?? []) ?? []
585
+ );
586
+ const defaultByColumn = /* @__PURE__ */ new Map();
587
+ for (const c of table.constraints?.filter(
588
+ (c2) => c2.type === "DEFAULT"
589
+ ) ?? []) {
590
+ for (const col of c.columns ?? []) {
591
+ if (c.defaultValue != null) {
592
+ defaultByColumn.set(col, c.defaultValue);
593
+ }
594
+ }
595
+ }
596
+ const uniqueColumns = new Set(
597
+ table.constraints?.filter((c) => c.type === "UNIQUE" && c.columns?.length === 1).flatMap((c) => c.columns ?? []) ?? []
598
+ );
599
+ const fkByColumn = /* @__PURE__ */ new Map();
600
+ for (const c of table.constraints?.filter(
601
+ (c2) => c2.type === "FOREIGN_KEY"
602
+ ) ?? []) {
603
+ const cols = c.columns ?? [];
604
+ const refCols = c.referencedColumns ?? [];
605
+ for (let i = 0; i < cols.length; i++) {
606
+ const refCol = refCols[i] ?? refCols[0] ?? cols[i];
607
+ fkByColumn.set(cols[i], `${c.referencedTable}.${refCol}`);
608
+ }
609
+ }
610
+ const columns = table.columns.map((column) => {
611
+ const annotations = [];
612
+ const isPrimaryKey = pkColumns.has(column.name);
613
+ if (isPrimaryKey) {
614
+ annotations.push("PK");
615
+ }
616
+ if (fkByColumn.has(column.name)) {
617
+ annotations.push(`FK -> ${fkByColumn.get(column.name)}`);
618
+ }
619
+ if (uniqueColumns.has(column.name)) {
620
+ annotations.push("UNIQUE");
621
+ }
622
+ if (notNullColumns.has(column.name)) {
623
+ annotations.push("NOT NULL");
624
+ }
625
+ if (defaultByColumn.has(column.name)) {
626
+ annotations.push(`DEFAULT: ${defaultByColumn.get(column.name)}`);
627
+ }
628
+ if (column.isIndexed && !isPrimaryKey) {
629
+ annotations.push("Indexed");
630
+ }
631
+ if (column.kind === "Enum" && column.values?.length) {
632
+ annotations.push(`Enum: ${column.values.join(", ")}`);
633
+ } else if (column.kind === "LowCardinality" && column.values?.length) {
634
+ annotations.push(`LowCardinality: ${column.values.join(", ")}`);
635
+ }
636
+ if (column.stats) {
637
+ const statParts = [];
638
+ if (column.stats.min != null || column.stats.max != null) {
639
+ const minText = column.stats.min ?? "n/a";
640
+ const maxText = column.stats.max ?? "n/a";
641
+ statParts.push(`range ${minText} \u2192 ${maxText}`);
642
+ }
643
+ if (column.stats.nullFraction != null && Number.isFinite(column.stats.nullFraction)) {
644
+ const percent = Math.round(column.stats.nullFraction * 1e3) / 10;
645
+ statParts.push(`null\u2248${percent}%`);
646
+ }
647
+ if (statParts.length) {
648
+ annotations.push(statParts.join(", "));
649
+ }
650
+ }
651
+ const annotationText = annotations.length ? ` [${annotations.join(", ")}]` : "";
652
+ return ` - ${column.name} (${column.type})${annotationText}`;
653
+ }).join("\n");
654
+ const indexes2 = table.indexes?.length ? `
655
+ Indexes:
656
+ ${table.indexes.map((index) => {
657
+ const props = [];
658
+ if (index.unique) {
659
+ props.push("UNIQUE");
660
+ }
661
+ if (index.type) {
662
+ props.push(index.type);
663
+ }
664
+ const propsText = props.length ? ` (${props.join(", ")})` : "";
665
+ const columnsText = index.columns?.length ? index.columns.join(", ") : "expression";
666
+ return ` - ${index.name}${propsText}: ${columnsText}`;
667
+ }).join("\n")}` : "";
668
+ const multiColumnUniques = table.constraints?.filter(
669
+ (c) => c.type === "UNIQUE" && (c.columns?.length ?? 0) > 1
670
+ ) ?? [];
671
+ const uniqueConstraints = multiColumnUniques.length ? `
672
+ Unique Constraints:
673
+ ${multiColumnUniques.map((c) => ` - ${c.name}: (${c.columns?.join(", ")})`).join("\n")}` : "";
674
+ const checkConstraints = table.constraints?.filter((c) => c.type === "CHECK") ?? [];
675
+ const checks = checkConstraints.length ? `
676
+ Check Constraints:
677
+ ${checkConstraints.map((c) => ` - ${c.name}: ${c.definition}`).join("\n")}` : "";
678
+ return `- Table: ${table.name}${rowCountInfo}
679
+ Columns:
680
+ ${columns}${indexes2}${uniqueConstraints}${checks}`;
681
+ }).join("\n\n");
682
+ }
683
+ #formatTableLabel = (tableName) => {
684
+ const base = tableName.split(".").pop() ?? tableName;
685
+ return base.replace(/_/g, " ");
686
+ };
687
+ #describeRelationships = (tables2, relationships) => {
688
+ if (!relationships.length) {
689
+ return "None detected";
690
+ }
691
+ const tableMap = new Map(tables2.map((table) => [table.name, table]));
692
+ return relationships.map((relationship) => {
693
+ const sourceLabel = this.#formatTableLabel(relationship.table);
694
+ const targetLabel = this.#formatTableLabel(
695
+ relationship.referenced_table
696
+ );
697
+ const singularSource = pluralize.singular(sourceLabel);
698
+ const pluralSource = pluralize.plural(sourceLabel);
699
+ const singularTarget = pluralize.singular(targetLabel);
700
+ const pluralTarget = pluralize.plural(targetLabel);
701
+ const sourceTable = tableMap.get(relationship.table);
702
+ const targetTable = tableMap.get(relationship.referenced_table);
703
+ const sourceCount = sourceTable?.rowCount;
704
+ const targetCount = targetTable?.rowCount;
705
+ const ratio = sourceCount != null && targetCount != null && targetCount > 0 ? sourceCount / targetCount : null;
706
+ let cardinality = "each";
707
+ if (ratio != null) {
708
+ if (ratio > 5) {
709
+ cardinality = `many-to-one (\u2248${sourceCount} vs ${targetCount})`;
710
+ } else if (ratio < 1.2 && ratio > 0.8) {
711
+ cardinality = `roughly 1:1 (${sourceCount} vs ${targetCount})`;
712
+ } else if (ratio < 0.2) {
713
+ cardinality = `one-to-many (${sourceCount} vs ${targetCount})`;
714
+ }
715
+ }
716
+ const mappings = relationship.from.map((fromCol, idx) => {
717
+ const targetCol = relationship.to[idx] ?? relationship.to[0] ?? fromCol;
718
+ return `${relationship.table}.${fromCol} -> ${relationship.referenced_table}.${targetCol}`;
719
+ }).join(", ");
720
+ return `- ${relationship.table} (${relationship.from.join(", ")}) -> ${relationship.referenced_table} (${relationship.to.join(", ")}) [${cardinality}]`;
721
+ }).join("\n");
722
+ };
723
+ };
724
+
725
+ // packages/text2sql/src/lib/adapters/mysql/column-stats.mysql.grounding.ts
726
+ var MysqlColumnStatsGrounding = class extends ColumnStatsGrounding {
727
+ #adapter;
728
+ constructor(adapter, config = {}) {
729
+ super(config);
730
+ this.#adapter = adapter;
731
+ }
732
+ async collectStats(tableName, column) {
733
+ if (!this.#shouldCollectStats(column.type)) {
734
+ return void 0;
735
+ }
736
+ const { schema, table } = this.#adapter.parseTableName(tableName);
737
+ const database = schema || await this.#getCurrentDatabase();
738
+ const tableIdentifier = `${this.#adapter.quoteIdentifier(database)}.${this.#adapter.quoteIdentifier(table)}`;
739
+ const columnIdentifier = this.#adapter.quoteIdentifier(column.name);
740
+ try {
741
+ const rows = await this.#adapter.runQuery(`
742
+ SELECT
743
+ CAST(MIN(${columnIdentifier}) AS CHAR) AS min_value,
744
+ CAST(MAX(${columnIdentifier}) AS CHAR) AS max_value,
745
+ AVG(CASE WHEN ${columnIdentifier} IS NULL THEN 1.0 ELSE 0.0 END) AS null_fraction
746
+ FROM ${tableIdentifier}
747
+ `);
748
+ if (!rows.length) {
749
+ return void 0;
750
+ }
751
+ const min = rows[0]?.min_value ?? void 0;
752
+ const max = rows[0]?.max_value ?? void 0;
753
+ const nullFraction = this.#toNumber(rows[0]?.null_fraction);
754
+ if (min == null && max == null && nullFraction == null) {
755
+ return void 0;
756
+ }
757
+ return {
758
+ min,
759
+ max,
760
+ nullFraction: nullFraction != null && Number.isFinite(nullFraction) ? Math.max(0, Math.min(1, nullFraction)) : void 0
761
+ };
762
+ } catch {
763
+ return void 0;
764
+ }
765
+ }
766
+ #shouldCollectStats(type) {
767
+ if (!type) {
768
+ return false;
769
+ }
770
+ const normalized = type.toLowerCase();
771
+ return /int|numeric|decimal|double|float|real|date|time|timestamp|datetime|bool|bit|year/.test(
772
+ normalized
773
+ );
774
+ }
775
+ #toNumber(value) {
776
+ if (typeof value === "number" && Number.isFinite(value)) {
777
+ return value;
778
+ }
779
+ if (typeof value === "bigint") {
780
+ return Number(value);
781
+ }
782
+ if (typeof value === "string" && value.trim() !== "") {
783
+ const parsed = Number(value);
784
+ return Number.isFinite(parsed) ? parsed : null;
785
+ }
786
+ return null;
787
+ }
788
+ async #getCurrentDatabase() {
789
+ const rows = await this.#adapter.runQuery(
790
+ "SELECT DATABASE() AS db"
791
+ );
792
+ return rows[0]?.db ?? "";
793
+ }
794
+ };
795
+
796
+ // packages/text2sql/src/lib/adapters/mysql/column-values.mysql.grounding.ts
797
+ var MysqlColumnValuesGrounding = class extends ColumnValuesGrounding {
798
+ #adapter;
799
+ constructor(adapter, config = {}) {
800
+ super(config);
801
+ this.#adapter = adapter;
802
+ }
803
+ /**
804
+ * Detect native MySQL ENUM types and extract their values.
805
+ */
806
+ async collectEnumValues(tableName, column) {
807
+ const { schema, table } = this.#adapter.parseTableName(tableName);
808
+ const database = schema || await this.#getCurrentDatabase();
809
+ const rows = await this.#adapter.runQuery(`
810
+ SELECT COLUMN_TYPE
811
+ FROM INFORMATION_SCHEMA.COLUMNS
812
+ WHERE TABLE_SCHEMA = '${this.#adapter.escapeString(database)}'
813
+ AND TABLE_NAME = '${this.#adapter.escapeString(table)}'
814
+ AND COLUMN_NAME = '${this.#adapter.escapeString(column.name)}'
815
+ `);
816
+ const columnType = rows[0]?.COLUMN_TYPE;
817
+ if (!columnType) return void 0;
818
+ const enumMatch = columnType.match(/^enum\((.+)\)$/i);
819
+ if (!enumMatch) return void 0;
820
+ return this.#parseEnumValues(enumMatch[1]);
821
+ }
822
+ /**
823
+ * Collect distinct values for low cardinality columns.
824
+ */
825
+ async collectLowCardinality(tableName, column) {
826
+ const { schema, table } = this.#adapter.parseTableName(tableName);
827
+ const database = schema || await this.#getCurrentDatabase();
828
+ const tableIdentifier = `${this.#adapter.quoteIdentifier(database)}.${this.#adapter.quoteIdentifier(table)}`;
829
+ const columnIdentifier = this.#adapter.quoteIdentifier(column.name);
830
+ const limit = this.lowCardinalityLimit + 1;
831
+ try {
832
+ const rows = await this.#adapter.runQuery(`
833
+ SELECT DISTINCT ${columnIdentifier} AS value
834
+ FROM ${tableIdentifier}
835
+ WHERE ${columnIdentifier} IS NOT NULL
836
+ LIMIT ${limit}
837
+ `);
838
+ if (!rows.length || rows.length > this.lowCardinalityLimit) {
839
+ return void 0;
840
+ }
841
+ const values = [];
842
+ for (const row of rows) {
843
+ const formatted = this.#normalizeValue(row.value);
844
+ if (formatted === null) {
845
+ return void 0;
846
+ }
847
+ values.push(formatted);
848
+ }
849
+ return values.length > 0 ? values : void 0;
850
+ } catch {
851
+ return void 0;
852
+ }
853
+ }
854
+ /**
855
+ * Parse ENUM values from the COLUMN_TYPE string.
856
+ * Input: "'val1','val2','val3'"
857
+ * Output: ['val1', 'val2', 'val3']
858
+ */
859
+ #parseEnumValues(enumString) {
860
+ const values = [];
861
+ const regex = /'((?:[^'\\]|\\.|'')*)'/g;
862
+ let match;
863
+ while ((match = regex.exec(enumString)) !== null) {
864
+ const value = match[1].replace(/''/g, "'").replace(/\\'/g, "'").replace(/\\\\/g, "\\");
865
+ values.push(value);
866
+ }
867
+ return values;
868
+ }
869
+ #normalizeValue(value) {
870
+ if (value === null || value === void 0) {
871
+ return null;
872
+ }
873
+ if (typeof value === "string") {
874
+ return value;
875
+ }
876
+ if (typeof value === "number" || typeof value === "bigint") {
877
+ return String(value);
878
+ }
879
+ if (typeof value === "boolean") {
880
+ return value ? "true" : "false";
881
+ }
882
+ if (value instanceof Date) {
883
+ return value.toISOString();
884
+ }
885
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
886
+ return value.toString("utf-8");
887
+ }
888
+ return null;
889
+ }
890
+ async #getCurrentDatabase() {
891
+ const rows = await this.#adapter.runQuery(
892
+ "SELECT DATABASE() AS db"
893
+ );
894
+ return rows[0]?.db ?? "";
895
+ }
896
+ };
897
+
898
+ // packages/text2sql/src/lib/adapters/mysql/constraint.mysql.grounding.ts
899
+ var MysqlConstraintGrounding = class extends ConstraintGrounding {
900
+ #adapter;
901
+ constructor(adapter, config = {}) {
902
+ super(config);
903
+ this.#adapter = adapter;
904
+ }
905
+ async getConstraints(tableName) {
906
+ const { schema, table } = this.#adapter.parseTableName(tableName);
907
+ const database = schema || await this.#getCurrentDatabase();
908
+ const constraints2 = [];
909
+ const constraintRows = await this.#adapter.runQuery(`
910
+ SELECT
911
+ tc.CONSTRAINT_NAME,
912
+ tc.CONSTRAINT_TYPE,
913
+ kcu.COLUMN_NAME,
914
+ kcu.REFERENCED_TABLE_SCHEMA,
915
+ kcu.REFERENCED_TABLE_NAME,
916
+ kcu.REFERENCED_COLUMN_NAME
917
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
918
+ LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
919
+ ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
920
+ AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA
921
+ AND tc.TABLE_NAME = kcu.TABLE_NAME
922
+ WHERE tc.TABLE_SCHEMA = '${this.#adapter.escapeString(database)}'
923
+ AND tc.TABLE_NAME = '${this.#adapter.escapeString(table)}'
924
+ AND tc.CONSTRAINT_TYPE IN ('PRIMARY KEY', 'UNIQUE', 'FOREIGN KEY')
925
+ ORDER BY tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION
926
+ `);
927
+ const constraintMap = /* @__PURE__ */ new Map();
928
+ for (const row of constraintRows) {
929
+ if (!row.CONSTRAINT_NAME || !row.COLUMN_NAME) continue;
930
+ const existing = constraintMap.get(row.CONSTRAINT_NAME);
931
+ if (existing) {
932
+ existing.columns.push(row.COLUMN_NAME);
933
+ if (row.REFERENCED_COLUMN_NAME) {
934
+ existing.referencedColumns = existing.referencedColumns ?? [];
935
+ existing.referencedColumns.push(row.REFERENCED_COLUMN_NAME);
936
+ }
937
+ } else {
938
+ const type = this.#mapConstraintType(row.CONSTRAINT_TYPE);
939
+ if (!type) continue;
940
+ const entry = {
941
+ type,
942
+ columns: [row.COLUMN_NAME]
943
+ };
944
+ if (type === "FOREIGN_KEY" && row.REFERENCED_TABLE_NAME) {
945
+ entry.referencedTable = row.REFERENCED_TABLE_SCHEMA ? `${row.REFERENCED_TABLE_SCHEMA}.${row.REFERENCED_TABLE_NAME}` : row.REFERENCED_TABLE_NAME;
946
+ if (row.REFERENCED_COLUMN_NAME) {
947
+ entry.referencedColumns = [row.REFERENCED_COLUMN_NAME];
948
+ }
949
+ }
950
+ constraintMap.set(row.CONSTRAINT_NAME, entry);
951
+ }
952
+ }
953
+ for (const [name, data] of constraintMap) {
954
+ constraints2.push({
955
+ name,
956
+ type: data.type,
957
+ columns: data.columns,
958
+ referencedTable: data.referencedTable,
959
+ referencedColumns: data.referencedColumns
960
+ });
961
+ }
962
+ const columnRows = await this.#adapter.runQuery(`
963
+ SELECT COLUMN_NAME, IS_NULLABLE, COLUMN_DEFAULT
964
+ FROM INFORMATION_SCHEMA.COLUMNS
965
+ WHERE TABLE_SCHEMA = '${this.#adapter.escapeString(database)}'
966
+ AND TABLE_NAME = '${this.#adapter.escapeString(table)}'
967
+ `);
968
+ const pkConstraint = constraints2.find((c) => c.type === "PRIMARY_KEY");
969
+ const pkColumns = new Set(pkConstraint?.columns ?? []);
970
+ for (const row of columnRows) {
971
+ if (!row.COLUMN_NAME) continue;
972
+ if (row.IS_NULLABLE === "NO" && !pkColumns.has(row.COLUMN_NAME)) {
973
+ constraints2.push({
974
+ name: `${row.COLUMN_NAME}_not_null`,
975
+ type: "NOT_NULL",
976
+ columns: [row.COLUMN_NAME]
977
+ });
978
+ }
979
+ if (row.COLUMN_DEFAULT !== null) {
980
+ constraints2.push({
981
+ name: `${row.COLUMN_NAME}_default`,
982
+ type: "DEFAULT",
983
+ columns: [row.COLUMN_NAME],
984
+ defaultValue: row.COLUMN_DEFAULT
985
+ });
986
+ }
987
+ }
988
+ try {
989
+ const checkRows = await this.#adapter.runQuery(`
990
+ SELECT CONSTRAINT_NAME, CHECK_CLAUSE
991
+ FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS
992
+ WHERE CONSTRAINT_SCHEMA = '${this.#adapter.escapeString(database)}'
993
+ `);
994
+ const checkTableRows = await this.#adapter.runQuery(`
995
+ SELECT CONSTRAINT_NAME, TABLE_NAME
996
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
997
+ WHERE TABLE_SCHEMA = '${this.#adapter.escapeString(database)}'
998
+ AND TABLE_NAME = '${this.#adapter.escapeString(table)}'
999
+ AND CONSTRAINT_TYPE = 'CHECK'
1000
+ `);
1001
+ const checkTableMap = new Map(
1002
+ checkTableRows.map((r) => [r.CONSTRAINT_NAME, r.TABLE_NAME])
1003
+ );
1004
+ for (const row of checkRows) {
1005
+ if (!row.CONSTRAINT_NAME) continue;
1006
+ if (checkTableMap.get(row.CONSTRAINT_NAME) !== table) continue;
1007
+ constraints2.push({
1008
+ name: row.CONSTRAINT_NAME,
1009
+ type: "CHECK",
1010
+ definition: row.CHECK_CLAUSE ?? void 0
1011
+ });
1012
+ }
1013
+ } catch {
1014
+ }
1015
+ return constraints2;
1016
+ }
1017
+ #mapConstraintType(type) {
1018
+ switch (type) {
1019
+ case "PRIMARY KEY":
1020
+ return "PRIMARY_KEY";
1021
+ case "UNIQUE":
1022
+ return "UNIQUE";
1023
+ case "FOREIGN KEY":
1024
+ return "FOREIGN_KEY";
1025
+ default:
1026
+ return null;
1027
+ }
1028
+ }
1029
+ async #getCurrentDatabase() {
1030
+ const rows = await this.#adapter.runQuery(
1031
+ "SELECT DATABASE() AS db"
1032
+ );
1033
+ return rows[0]?.db ?? "";
1034
+ }
1035
+ };
1036
+
1037
+ // packages/text2sql/src/lib/adapters/mysql/indexes.mysql.grounding.ts
1038
+ var MysqlIndexesGrounding = class extends IndexesGrounding {
1039
+ #adapter;
1040
+ constructor(adapter, config = {}) {
1041
+ super(config);
1042
+ this.#adapter = adapter;
1043
+ }
1044
+ async getIndexes(tableName) {
1045
+ const { schema, table } = this.#adapter.parseTableName(tableName);
1046
+ const database = schema || await this.#getCurrentDatabase();
1047
+ const rows = await this.#adapter.runQuery(`
1048
+ SELECT
1049
+ INDEX_NAME,
1050
+ COLUMN_NAME,
1051
+ NON_UNIQUE,
1052
+ INDEX_TYPE,
1053
+ SEQ_IN_INDEX
1054
+ FROM INFORMATION_SCHEMA.STATISTICS
1055
+ WHERE TABLE_SCHEMA = '${this.#adapter.escapeString(database)}'
1056
+ AND TABLE_NAME = '${this.#adapter.escapeString(table)}'
1057
+ ORDER BY INDEX_NAME, SEQ_IN_INDEX
1058
+ `);
1059
+ const indexMap = /* @__PURE__ */ new Map();
1060
+ for (const row of rows) {
1061
+ if (!row.INDEX_NAME) continue;
1062
+ let index = indexMap.get(row.INDEX_NAME);
1063
+ if (!index) {
1064
+ index = {
1065
+ name: row.INDEX_NAME,
1066
+ columns: [],
1067
+ unique: row.NON_UNIQUE === 0,
1068
+ type: row.INDEX_TYPE ?? void 0
1069
+ };
1070
+ indexMap.set(row.INDEX_NAME, index);
1071
+ }
1072
+ if (row.COLUMN_NAME) {
1073
+ index.columns.push(row.COLUMN_NAME);
1074
+ }
1075
+ }
1076
+ return Array.from(indexMap.values());
1077
+ }
1078
+ async #getCurrentDatabase() {
1079
+ const rows = await this.#adapter.runQuery(
1080
+ "SELECT DATABASE() AS db"
1081
+ );
1082
+ return rows[0]?.db ?? "";
1083
+ }
1084
+ };
1085
+
1086
+ // packages/text2sql/src/lib/adapters/mysql/info.mysql.grounding.ts
1087
+ var MysqlInfoGrounding = class extends InfoGrounding {
1088
+ #adapter;
1089
+ constructor(adapter, config = {}) {
1090
+ super(config);
1091
+ this.#adapter = adapter;
1092
+ }
1093
+ async collectInfo() {
1094
+ const [versionRows, dbRows] = await Promise.all([
1095
+ this.#adapter.runQuery(
1096
+ "SELECT VERSION() AS version"
1097
+ ),
1098
+ this.#adapter.runQuery("SELECT DATABASE() AS db")
1099
+ ]);
1100
+ const version = versionRows[0]?.version;
1101
+ const database = dbRows[0]?.db ?? void 0;
1102
+ const isMariadb = version?.toLowerCase().includes("mariadb") ?? false;
1103
+ const dialect = isMariadb ? "mariadb" : "mysql";
1104
+ return {
1105
+ dialect,
1106
+ version,
1107
+ database
1108
+ };
1109
+ }
1110
+ };
1111
+
1112
+ // packages/text2sql/src/lib/adapters/mysql/mysql.ts
1113
+ var MYSQL_ERROR_MAP = {
1114
+ "1146": {
1115
+ type: "MISSING_TABLE",
1116
+ hint: "Check the database for the correct table name. Include the database prefix if necessary."
1117
+ },
1118
+ "1054": {
1119
+ type: "INVALID_COLUMN",
1120
+ hint: "Verify the column exists on the referenced table and use table aliases to disambiguate."
1121
+ },
1122
+ "1064": {
1123
+ type: "SYNTAX_ERROR",
1124
+ hint: "There is a SQL syntax error. Review keywords, punctuation, and the overall query shape."
1125
+ },
1126
+ "1630": {
1127
+ type: "INVALID_FUNCTION",
1128
+ hint: "The function does not exist or the arguments are invalid."
1129
+ },
1130
+ "1305": {
1131
+ type: "INVALID_FUNCTION",
1132
+ hint: "The function or procedure you used is not recognized. Confirm its name and argument types."
1133
+ },
1134
+ "1109": {
1135
+ type: "MISSING_TABLE",
1136
+ hint: "Unknown table in the query. Verify table name and database."
1137
+ },
1138
+ "1051": {
1139
+ type: "MISSING_TABLE",
1140
+ hint: "Unknown table. Check if the table exists in the current database."
1141
+ }
1142
+ };
1143
+ function isMysqlError(error) {
1144
+ return typeof error === "object" && error !== null && ("errno" in error || "code" in error);
1145
+ }
1146
+ function formatMysqlError(sql, error) {
1147
+ const errorMessage = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error occurred";
1148
+ if (isMysqlError(error)) {
1149
+ const errorCode = error.errno?.toString() ?? error.code ?? "";
1150
+ const metadata = MYSQL_ERROR_MAP[errorCode];
1151
+ if (metadata) {
1152
+ return {
1153
+ error: errorMessage,
1154
+ error_type: metadata.type,
1155
+ suggestion: metadata.hint,
1156
+ sql_attempted: sql
1157
+ };
1158
+ }
1159
+ }
1160
+ return {
1161
+ error: errorMessage,
1162
+ error_type: "UNKNOWN_ERROR",
1163
+ suggestion: "Review the query and try again",
1164
+ sql_attempted: sql
1165
+ };
1166
+ }
1167
+ var Mysql = class extends Adapter {
1168
+ #options;
1169
+ grounding;
1170
+ defaultSchema = void 0;
1171
+ systemSchemas = [
1172
+ "mysql",
1173
+ "information_schema",
1174
+ "performance_schema",
1175
+ "sys"
1176
+ ];
1177
+ constructor(options) {
1178
+ super();
1179
+ if (!options || typeof options.execute !== "function") {
1180
+ throw new Error("Mysql adapter requires an execute function.");
1181
+ }
1182
+ this.#options = {
1183
+ ...options,
1184
+ databases: options.databases?.length ? options.databases : void 0
1185
+ };
1186
+ this.grounding = options.grounding;
1187
+ }
1188
+ async execute(sql) {
1189
+ return this.#options.execute(sql);
1190
+ }
1191
+ async validate(sql) {
1192
+ const validator = this.#options.validate ?? (async (text) => {
1193
+ await this.#options.execute(`EXPLAIN ${text}`);
1194
+ });
1195
+ try {
1196
+ return await validator(sql);
1197
+ } catch (error) {
1198
+ return JSON.stringify(formatMysqlError(sql, error));
1199
+ }
1200
+ }
1201
+ async runQuery(sql) {
1202
+ return this.#runIntrospectionQuery(sql);
1203
+ }
1204
+ quoteIdentifier(name) {
1205
+ return `\`${name.replace(/`/g, "``")}\``;
1206
+ }
1207
+ escape(value) {
1208
+ return value.replace(/`/g, "``");
1209
+ }
1210
+ buildSampleRowsQuery(tableName, columns, limit) {
1211
+ const { schema, table } = this.parseTableName(tableName);
1212
+ const tableIdentifier = schema ? `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}` : this.quoteIdentifier(table);
1213
+ const columnList = columns?.length ? columns.map((c) => this.quoteIdentifier(c)).join(", ") : "*";
1214
+ return `SELECT ${columnList} FROM ${tableIdentifier} LIMIT ${limit}`;
1215
+ }
1216
+ /**
1217
+ * Get the configured databases filter.
1218
+ */
1219
+ get databases() {
1220
+ return this.#options.databases;
1221
+ }
1222
+ async #runIntrospectionQuery(sql) {
1223
+ const result = await this.#options.execute(sql);
1224
+ if (Array.isArray(result)) {
1225
+ if (result.length >= 1 && Array.isArray(result[0]) && (result.length === 1 || result.length === 2 && !Array.isArray(result[1]?.[0]))) {
1226
+ return result[0];
1227
+ }
1228
+ return result;
1229
+ }
1230
+ if (result && typeof result === "object" && "rows" in result && Array.isArray(result.rows)) {
1231
+ return result.rows;
1232
+ }
1233
+ throw new Error(
1234
+ "Mysql adapter execute() must return an array of rows or an object with a rows array when introspecting."
1235
+ );
1236
+ }
1237
+ };
1238
+
1239
+ // packages/text2sql/src/lib/adapters/mysql/row-count.mysql.grounding.ts
1240
+ var MysqlRowCountGrounding = class extends RowCountGrounding {
1241
+ #adapter;
1242
+ constructor(adapter, config = {}) {
1243
+ super(config);
1244
+ this.#adapter = adapter;
1245
+ }
1246
+ async getRowCount(tableName) {
1247
+ const { schema, table } = this.#adapter.parseTableName(tableName);
1248
+ const database = schema || await this.#getCurrentDatabase();
1249
+ const tableIdentifier = `${this.#adapter.quoteIdentifier(database)}.${this.#adapter.quoteIdentifier(table)}`;
1250
+ try {
1251
+ const rows = await this.#adapter.runQuery(`SELECT COUNT(*) AS count FROM ${tableIdentifier}`);
1252
+ return this.#toNumber(rows[0]?.count);
1253
+ } catch {
1254
+ return void 0;
1255
+ }
1256
+ }
1257
+ #toNumber(value) {
1258
+ if (typeof value === "number" && Number.isFinite(value)) {
1259
+ return value;
1260
+ }
1261
+ if (typeof value === "bigint") {
1262
+ return Number(value);
1263
+ }
1264
+ if (typeof value === "string" && value.trim() !== "") {
1265
+ const parsed = Number(value);
1266
+ return Number.isFinite(parsed) ? parsed : void 0;
1267
+ }
1268
+ return void 0;
1269
+ }
1270
+ async #getCurrentDatabase() {
1271
+ const rows = await this.#adapter.runQuery(
1272
+ "SELECT DATABASE() AS db"
1273
+ );
1274
+ return rows[0]?.db ?? "";
1275
+ }
1276
+ };
1277
+
1278
+ // packages/text2sql/src/lib/adapters/mysql/table.mysql.grounding.ts
1279
+ var MysqlTableGrounding = class extends TableGrounding {
1280
+ #adapter;
1281
+ #databases;
1282
+ constructor(adapter, config = {}) {
1283
+ super(config);
1284
+ this.#adapter = adapter;
1285
+ this.#databases = config.databases ?? adapter.databases;
1286
+ }
1287
+ async getAllTableNames() {
1288
+ const rows = await this.#adapter.runQuery(`
1289
+ SELECT DISTINCT CONCAT(TABLE_SCHEMA, '.', TABLE_NAME) AS name
1290
+ FROM INFORMATION_SCHEMA.TABLES
1291
+ WHERE TABLE_TYPE = 'BASE TABLE'
1292
+ ${this.#buildDatabaseFilter("TABLE_SCHEMA")}
1293
+ ORDER BY name
1294
+ `);
1295
+ return rows.map((r) => r.name);
1296
+ }
1297
+ async getTable(tableName) {
1298
+ const { schema, table } = this.#adapter.parseTableName(tableName);
1299
+ const database = schema || await this.#getCurrentDatabase();
1300
+ const columns = await this.#adapter.runQuery(`
1301
+ SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE
1302
+ FROM INFORMATION_SCHEMA.COLUMNS
1303
+ WHERE TABLE_SCHEMA = '${this.#adapter.escapeString(database)}'
1304
+ AND TABLE_NAME = '${this.#adapter.escapeString(table)}'
1305
+ ORDER BY ORDINAL_POSITION
1306
+ `);
1307
+ return {
1308
+ name: tableName,
1309
+ schema: database,
1310
+ rawName: table,
1311
+ columns: columns.map((col) => ({
1312
+ name: col.COLUMN_NAME ?? "unknown",
1313
+ type: col.DATA_TYPE ?? "unknown"
1314
+ }))
1315
+ };
1316
+ }
1317
+ async findOutgoingRelations(tableName) {
1318
+ const { schema, table } = this.#adapter.parseTableName(tableName);
1319
+ const database = schema || await this.#getCurrentDatabase();
1320
+ const rows = await this.#adapter.runQuery(`
1321
+ SELECT
1322
+ kcu.CONSTRAINT_NAME,
1323
+ kcu.TABLE_SCHEMA,
1324
+ kcu.TABLE_NAME,
1325
+ kcu.COLUMN_NAME,
1326
+ kcu.REFERENCED_TABLE_SCHEMA,
1327
+ kcu.REFERENCED_TABLE_NAME,
1328
+ kcu.REFERENCED_COLUMN_NAME
1329
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
1330
+ WHERE kcu.TABLE_SCHEMA = '${this.#adapter.escapeString(database)}'
1331
+ AND kcu.TABLE_NAME = '${this.#adapter.escapeString(table)}'
1332
+ AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
1333
+ ORDER BY kcu.CONSTRAINT_NAME, kcu.ORDINAL_POSITION
1334
+ `);
1335
+ return this.#groupRelationships(rows);
1336
+ }
1337
+ async findIncomingRelations(tableName) {
1338
+ const { schema, table } = this.#adapter.parseTableName(tableName);
1339
+ const database = schema || await this.#getCurrentDatabase();
1340
+ const rows = await this.#adapter.runQuery(`
1341
+ SELECT
1342
+ kcu.CONSTRAINT_NAME,
1343
+ kcu.TABLE_SCHEMA,
1344
+ kcu.TABLE_NAME,
1345
+ kcu.COLUMN_NAME,
1346
+ kcu.REFERENCED_TABLE_SCHEMA,
1347
+ kcu.REFERENCED_TABLE_NAME,
1348
+ kcu.REFERENCED_COLUMN_NAME
1349
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
1350
+ WHERE kcu.REFERENCED_TABLE_SCHEMA = '${this.#adapter.escapeString(database)}'
1351
+ AND kcu.REFERENCED_TABLE_NAME = '${this.#adapter.escapeString(table)}'
1352
+ ORDER BY kcu.CONSTRAINT_NAME, kcu.ORDINAL_POSITION
1353
+ `);
1354
+ return this.#groupRelationships(rows);
1355
+ }
1356
+ #groupRelationships(rows) {
1357
+ const relationships = /* @__PURE__ */ new Map();
1358
+ for (const row of rows) {
1359
+ if (!row.TABLE_NAME || !row.REFERENCED_TABLE_NAME || !row.CONSTRAINT_NAME) {
1360
+ continue;
1361
+ }
1362
+ const schema = row.TABLE_SCHEMA ?? "";
1363
+ const referencedSchema = row.REFERENCED_TABLE_SCHEMA ?? "";
1364
+ const key = `${schema}.${row.TABLE_NAME}:${row.CONSTRAINT_NAME}`;
1365
+ const relationship = relationships.get(key) ?? {
1366
+ table: `${schema}.${row.TABLE_NAME}`,
1367
+ from: [],
1368
+ referenced_table: `${referencedSchema}.${row.REFERENCED_TABLE_NAME}`,
1369
+ to: []
1370
+ };
1371
+ relationship.from.push(row.COLUMN_NAME ?? "unknown");
1372
+ relationship.to.push(row.REFERENCED_COLUMN_NAME ?? "unknown");
1373
+ relationships.set(key, relationship);
1374
+ }
1375
+ return Array.from(relationships.values());
1376
+ }
1377
+ #buildDatabaseFilter(columnName) {
1378
+ if (this.#databases && this.#databases.length > 0) {
1379
+ const values = this.#databases.map((db) => `'${this.#adapter.escapeString(db)}'`).join(", ");
1380
+ return `AND ${columnName} IN (${values})`;
1381
+ }
1382
+ const systemDbs = this.#adapter.systemSchemas.map((db) => `'${this.#adapter.escapeString(db)}'`).join(", ");
1383
+ return `AND ${columnName} NOT IN (${systemDbs})`;
1384
+ }
1385
+ async #getCurrentDatabase() {
1386
+ const rows = await this.#adapter.runQuery(
1387
+ "SELECT DATABASE() AS db"
1388
+ );
1389
+ return rows[0]?.db ?? "";
1390
+ }
1391
+ };
1392
+
1393
+ // packages/text2sql/src/lib/adapters/groundings/view.grounding.ts
1394
+ var ViewGrounding = class extends AbstractGrounding {
1395
+ #filter;
1396
+ constructor(config = {}) {
1397
+ super("views");
1398
+ this.#filter = config.filter;
1399
+ }
1400
+ /**
1401
+ * Execute the grounding process.
1402
+ * Writes discovered views to the context.
1403
+ */
1404
+ async execute(ctx) {
1405
+ const viewNames = await this.applyFilter();
1406
+ const views2 = await Promise.all(
1407
+ viewNames.map((name) => this.getView(name))
1408
+ );
1409
+ ctx.views.push(...views2);
1410
+ return () => this.#describe(views2);
1411
+ }
1412
+ #describe(views2) {
1413
+ if (!views2.length) {
1414
+ return "No views available.";
1415
+ }
1416
+ return views2.map((view) => {
1417
+ const columns = view.columns.map((column) => {
1418
+ const annotations = [];
1419
+ if (column.kind === "LowCardinality" && column.values?.length) {
1420
+ annotations.push(`LowCardinality: ${column.values.join(", ")}`);
1421
+ }
1422
+ if (column.stats) {
1423
+ const statParts = [];
1424
+ if (column.stats.min != null || column.stats.max != null) {
1425
+ const minText = column.stats.min ?? "n/a";
1426
+ const maxText = column.stats.max ?? "n/a";
1427
+ statParts.push(`range ${minText} \u2192 ${maxText}`);
1428
+ }
1429
+ if (column.stats.nullFraction != null && Number.isFinite(column.stats.nullFraction)) {
1430
+ const percent = Math.round(column.stats.nullFraction * 1e3) / 10;
1431
+ statParts.push(`null\u2248${percent}%`);
1432
+ }
1433
+ if (statParts.length) {
1434
+ annotations.push(statParts.join(", "));
1435
+ }
1436
+ }
1437
+ const annotationText = annotations.length ? ` [${annotations.join(", ")}]` : "";
1438
+ return ` - ${column.name} (${column.type})${annotationText}`;
1439
+ }).join("\n");
1440
+ const definition = view.definition ? `
1441
+ Definition: ${view.definition.length > 200 ? view.definition.slice(0, 200) + "..." : view.definition}` : "";
1442
+ return `- View: ${view.name}${definition}
1443
+ Columns:
1444
+ ${columns}`;
1445
+ }).join("\n\n");
1446
+ }
1447
+ /**
1448
+ * Apply the filter to get view names.
1449
+ * If filter is an explicit array, skip querying all view names.
1450
+ */
1451
+ async applyFilter() {
1452
+ const filter = this.#filter;
1453
+ if (Array.isArray(filter)) {
1454
+ return filter;
1455
+ }
1456
+ const names = await this.getAllViewNames();
1457
+ if (!filter) {
1458
+ return names;
1459
+ }
1460
+ if (filter instanceof RegExp) {
1461
+ return names.filter((name) => filter.test(name));
1462
+ }
1463
+ return names.filter(filter);
1464
+ }
1465
+ };
1466
+
1467
+ // packages/text2sql/src/lib/adapters/mysql/view.mysql.grounding.ts
1468
+ var MysqlViewGrounding = class extends ViewGrounding {
1469
+ #adapter;
1470
+ #databases;
1471
+ constructor(adapter, config = {}) {
1472
+ super(config);
1473
+ this.#adapter = adapter;
1474
+ this.#databases = config.databases ?? adapter.databases;
1475
+ }
1476
+ async getAllViewNames() {
1477
+ const rows = await this.#adapter.runQuery(`
1478
+ SELECT DISTINCT CONCAT(TABLE_SCHEMA, '.', TABLE_NAME) AS name
1479
+ FROM INFORMATION_SCHEMA.VIEWS
1480
+ WHERE 1=1
1481
+ ${this.#buildDatabaseFilter("TABLE_SCHEMA")}
1482
+ ORDER BY name
1483
+ `);
1484
+ return rows.map((r) => r.name);
1485
+ }
1486
+ async getView(viewName) {
1487
+ const { schema, table } = this.#adapter.parseTableName(viewName);
1488
+ const database = schema || await this.#getCurrentDatabase();
1489
+ const columns = await this.#adapter.runQuery(`
1490
+ SELECT COLUMN_NAME, DATA_TYPE
1491
+ FROM INFORMATION_SCHEMA.COLUMNS
1492
+ WHERE TABLE_SCHEMA = '${this.#adapter.escapeString(database)}'
1493
+ AND TABLE_NAME = '${this.#adapter.escapeString(table)}'
1494
+ ORDER BY ORDINAL_POSITION
1495
+ `);
1496
+ const viewRows = await this.#adapter.runQuery(`
1497
+ SELECT VIEW_DEFINITION
1498
+ FROM INFORMATION_SCHEMA.VIEWS
1499
+ WHERE TABLE_SCHEMA = '${this.#adapter.escapeString(database)}'
1500
+ AND TABLE_NAME = '${this.#adapter.escapeString(table)}'
1501
+ `);
1502
+ return {
1503
+ name: viewName,
1504
+ schema: database,
1505
+ rawName: table,
1506
+ definition: viewRows[0]?.VIEW_DEFINITION ?? void 0,
1507
+ columns: columns.map((col) => ({
1508
+ name: col.COLUMN_NAME ?? "unknown",
1509
+ type: col.DATA_TYPE ?? "unknown"
1510
+ }))
1511
+ };
1512
+ }
1513
+ #buildDatabaseFilter(columnName) {
1514
+ if (this.#databases && this.#databases.length > 0) {
1515
+ const values = this.#databases.map((db) => `'${this.#adapter.escapeString(db)}'`).join(", ");
1516
+ return `AND ${columnName} IN (${values})`;
1517
+ }
1518
+ const systemDbs = this.#adapter.systemSchemas.map((db) => `'${this.#adapter.escapeString(db)}'`).join(", ");
1519
+ return `AND ${columnName} NOT IN (${systemDbs})`;
1520
+ }
1521
+ async #getCurrentDatabase() {
1522
+ const rows = await this.#adapter.runQuery(
1523
+ "SELECT DATABASE() AS db"
1524
+ );
1525
+ return rows[0]?.db ?? "";
1526
+ }
1527
+ };
1528
+
1529
+ // packages/text2sql/src/lib/adapters/mysql/index.ts
1530
+ function tables(config = {}) {
1531
+ return (adapter) => new MysqlTableGrounding(adapter, config);
1532
+ }
1533
+ function info(config = {}) {
1534
+ return (adapter) => new MysqlInfoGrounding(adapter, config);
1535
+ }
1536
+ function views(config = {}) {
1537
+ return (adapter) => {
1538
+ return new MysqlViewGrounding(adapter, config);
1539
+ };
1540
+ }
1541
+ function columnStats(config = {}) {
1542
+ return (adapter) => {
1543
+ return new MysqlColumnStatsGrounding(adapter, config);
1544
+ };
1545
+ }
1546
+ function columnValues(config = {}) {
1547
+ return (adapter) => {
1548
+ return new MysqlColumnValuesGrounding(adapter, config);
1549
+ };
1550
+ }
1551
+ function indexes(config = {}) {
1552
+ return (adapter) => {
1553
+ return new MysqlIndexesGrounding(adapter, config);
1554
+ };
1555
+ }
1556
+ function rowCount(config = {}) {
1557
+ return (adapter) => {
1558
+ return new MysqlRowCountGrounding(adapter, config);
1559
+ };
1560
+ }
1561
+ function constraints(config = {}) {
1562
+ return (adapter) => {
1563
+ return new MysqlConstraintGrounding(adapter, config);
1564
+ };
1565
+ }
1566
+ function report(config = {}) {
1567
+ return (adapter) => new ReportGrounding(adapter, config);
1568
+ }
1569
+ var mysql_default = {
1570
+ tables,
1571
+ info,
1572
+ views,
1573
+ columnStats,
1574
+ columnValues,
1575
+ indexes,
1576
+ rowCount,
1577
+ constraints,
1578
+ report,
1579
+ Mysql,
1580
+ Mariadb: Mysql
1581
+ };
1582
+ export {
1583
+ Mysql as Mariadb,
1584
+ Mysql,
1585
+ columnStats,
1586
+ columnValues,
1587
+ constraints,
1588
+ mysql_default as default,
1589
+ formatMysqlError,
1590
+ indexes,
1591
+ info,
1592
+ report,
1593
+ rowCount,
1594
+ tables,
1595
+ views
1596
+ };
1597
+ //# sourceMappingURL=index.js.map