@deepagents/text2sql 0.6.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1121 -453
- package/dist/index.js.map +4 -4
- package/dist/lib/adapters/mysql/column-stats.mysql.grounding.d.ts +14 -0
- package/dist/lib/adapters/mysql/column-stats.mysql.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/mysql/column-values.mysql.grounding.d.ts +22 -0
- package/dist/lib/adapters/mysql/column-values.mysql.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/mysql/constraint.mysql.grounding.d.ts +13 -0
- package/dist/lib/adapters/mysql/constraint.mysql.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/mysql/index.d.ts +44 -0
- package/dist/lib/adapters/mysql/index.d.ts.map +1 -0
- package/dist/lib/adapters/mysql/index.js +1597 -0
- package/dist/lib/adapters/mysql/index.js.map +7 -0
- package/dist/lib/adapters/mysql/indexes.mysql.grounding.d.ts +13 -0
- package/dist/lib/adapters/mysql/indexes.mysql.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/mysql/info.mysql.grounding.d.ts +13 -0
- package/dist/lib/adapters/mysql/info.mysql.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/mysql/mysql.d.ts +33 -0
- package/dist/lib/adapters/mysql/mysql.d.ts.map +1 -0
- package/dist/lib/adapters/mysql/row-count.mysql.grounding.d.ts +13 -0
- package/dist/lib/adapters/mysql/row-count.mysql.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/mysql/table.mysql.grounding.d.ts +21 -0
- package/dist/lib/adapters/mysql/table.mysql.grounding.d.ts.map +1 -0
- package/dist/lib/adapters/mysql/view.mysql.grounding.d.ts +18 -0
- package/dist/lib/adapters/mysql/view.mysql.grounding.d.ts.map +1 -0
- package/dist/lib/agents/bi.agent.d.ts +14 -0
- package/dist/lib/agents/bi.agent.d.ts.map +1 -0
- package/dist/lib/agents/chat1.agent.d.ts.map +1 -1
- package/dist/lib/agents/chat2.agent.d.ts.map +1 -1
- package/dist/lib/agents/developer.agent.d.ts +31 -0
- package/dist/lib/agents/developer.agent.d.ts.map +1 -0
- package/dist/lib/agents/question.agent.d.ts +1 -1
- package/dist/lib/agents/question.agent.d.ts.map +1 -1
- package/dist/lib/agents/sql.agent.d.ts +14 -32
- package/dist/lib/agents/sql.agent.d.ts.map +1 -1
- package/dist/lib/agents/text2sql.agent.d.ts.map +1 -1
- package/dist/lib/checkpoint.d.ts.map +1 -1
- package/dist/lib/sql.d.ts +42 -0
- package/dist/lib/sql.d.ts.map +1 -1
- package/dist/lib/synthesis/extractors/sql-extractor.d.ts.map +1 -1
- package/dist/lib/synthesis/index.js +267 -164
- package/dist/lib/synthesis/index.js.map +3 -3
- package/dist/lib/synthesis/synthesizers/breadth-evolver.d.ts.map +1 -1
- package/dist/lib/synthesis/synthesizers/depth-evolver.d.ts.map +1 -1
- package/dist/lib/synthesis/synthesizers/schema-synthesizer.d.ts.map +1 -1
- package/dist/lib/synthesis/synthesizers/styles.d.ts +2 -2
- package/dist/lib/synthesis/synthesizers/styles.d.ts.map +1 -1
- package/dist/lib/teach/teachings.d.ts.map +1 -1
- package/package.json +9 -3
|
@@ -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
|