@deepagents/text2sql 2.1.0 → 2.3.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 CHANGED
@@ -10,5 +10,6 @@ export * from './lib/fragments/schema.ts';
10
10
  export * from './lib/fs/index.ts';
11
11
  export * from './lib/instructions.ts';
12
12
  export * from './lib/sql-hooks.ts';
13
+ export { type CreateSqlCommandOptions, type CreateSqlCommandResult, createSqlCommand, } from './lib/sql-command.ts';
13
14
  export * from './lib/sql.ts';
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oCAAoC,CAAC;AACnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oCAAoC,CAAC;AACnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,cAAc,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -189,6 +189,13 @@ function createGroundingContext(options = {}) {
189
189
  import nodeSqlParser from "node-sql-parser";
190
190
  var { Parser } = nodeSqlParser;
191
191
  var parser = new Parser();
192
+ function parseStatementTypes(sql, dialect) {
193
+ const ast = parser.astify(sql, { database: dialect });
194
+ const statements = Array.isArray(ast) ? ast : [ast];
195
+ return statements.flatMap(
196
+ (statement) => isAstLike(statement) && typeof statement.type === "string" ? [statement.type] : []
197
+ );
198
+ }
192
199
  function extractBaseEntityReferences(sql, dialect) {
193
200
  const ast = parser.astify(sql, { database: dialect });
194
201
  const state = {
@@ -314,6 +321,22 @@ function caseFold(value) {
314
321
  }
315
322
 
316
323
  // packages/text2sql/src/lib/adapters/adapter.ts
324
+ var PARSER_DIALECTS_BY_FORMATTER_LANGUAGE = {
325
+ sqlite: ["sqlite", "mysql"],
326
+ postgresql: ["postgresql"],
327
+ bigquery: ["bigquery"],
328
+ transactsql: ["transactsql"],
329
+ mysql: ["mysql"]
330
+ };
331
+ function parserDialectsFor(formatterLanguage) {
332
+ const candidates = PARSER_DIALECTS_BY_FORMATTER_LANGUAGE[formatterLanguage];
333
+ if (!candidates) {
334
+ throw new TypeError(
335
+ `No scope dialect mapping for formatter language "${formatterLanguage}". Add it to PARSER_DIALECTS_BY_FORMATTER_LANGUAGE in adapter.ts.`
336
+ );
337
+ }
338
+ return candidates;
339
+ }
317
340
  var Adapter = class {
318
341
  /**
319
342
  * Introspect the database schema and return context fragments.
@@ -535,10 +558,49 @@ var Adapter = class {
535
558
  return sql.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\([().*])/g, "$1").replace(/\\(?=$)/gm, "");
536
559
  }
537
560
  #checkReadOnly(sql) {
538
- const upper = sql.trimStart().toUpperCase();
539
- if (!upper.startsWith("SELECT") && !upper.startsWith("WITH")) {
561
+ const statementTypes = this.#readParsedStatementTypes(sql);
562
+ if (statementTypes) {
563
+ if (statementTypes.length === 1 && statementTypes[0] === "select") {
564
+ return null;
565
+ }
540
566
  return "only SELECT or WITH queries allowed";
541
567
  }
568
+ const keyword = this.#firstStatementKeyword(sql);
569
+ if (keyword === "SELECT" || keyword === "WITH") return null;
570
+ return "only SELECT or WITH queries allowed";
571
+ }
572
+ #readParsedStatementTypes(sql) {
573
+ for (const dialect of parserDialectsFor(this.formatterLanguage)) {
574
+ try {
575
+ return parseStatementTypes(sql, dialect);
576
+ } catch {
577
+ }
578
+ }
579
+ return null;
580
+ }
581
+ #firstStatementKeyword(sql) {
582
+ let offset = 0;
583
+ while (offset < sql.length) {
584
+ if (/\s/.test(sql[offset])) {
585
+ offset++;
586
+ continue;
587
+ }
588
+ if (sql.startsWith("--", offset)) {
589
+ offset += 2;
590
+ while (offset < sql.length && sql[offset] !== "\n" && sql[offset] !== "\r") {
591
+ offset++;
592
+ }
593
+ continue;
594
+ }
595
+ if (sql.startsWith("/*", offset)) {
596
+ const commentEnd = sql.indexOf("*/", offset + 2);
597
+ if (commentEnd === -1) return null;
598
+ offset = commentEnd + 2;
599
+ continue;
600
+ }
601
+ const keyword = /^[A-Za-z]+/.exec(sql.slice(offset));
602
+ return keyword ? keyword[0].toUpperCase() : null;
603
+ }
542
604
  return null;
543
605
  }
544
606
  #cachedAllowedEntities = null;
@@ -548,36 +610,21 @@ var Adapter = class {
548
610
  return this.#cachedAllowedEntities;
549
611
  }
550
612
  async #checkScope(sql, allowedEntities) {
551
- const dialect = this.formatterLanguage;
552
- const scopeDialects = {
553
- sqlite: ["sqlite", "mysql"],
554
- postgresql: ["postgresql"],
555
- bigquery: ["bigquery"],
556
- transactsql: ["transactsql"],
557
- mysql: ["mysql"]
558
- };
559
- const candidates = scopeDialects[dialect];
560
- if (!candidates) {
561
- throw new TypeError(
562
- `No scope dialect mapping for formatter language "${dialect}". Add it to the scopeDialects map in Adapter.#checkScope.`
563
- );
564
- }
613
+ const candidates = parserDialectsFor(this.formatterLanguage);
565
614
  let references = null;
566
615
  let lastError;
616
+ let lastDialect = candidates[0];
567
617
  for (const candidate of candidates) {
568
618
  try {
569
619
  references = extractBaseEntityReferences(sql, candidate);
570
620
  break;
571
621
  } catch (error) {
622
+ lastDialect = candidate;
572
623
  lastError = error;
573
624
  }
574
625
  }
575
626
  if (references === null) {
576
- return buildScopeParseErrorPayload(
577
- sql,
578
- dialect,
579
- lastError
580
- );
627
+ return buildScopeParseErrorPayload(sql, lastDialect, lastError);
581
628
  }
582
629
  if (references.length === 0) return null;
583
630
  const allowedQualified = new Set(
@@ -5478,8 +5525,49 @@ function createSqlCommandHooks({
5478
5525
  };
5479
5526
  }
5480
5527
 
5528
+ // packages/text2sql/src/lib/sql-command.ts
5529
+ import { posix as posix5 } from "node:path";
5530
+ import { parseArgs } from "node:util";
5531
+ import { v7 } from "uuid";
5532
+ import {
5533
+ BashException as BashException2,
5534
+ buildSubcommandRepair as buildSubcommandRepair2,
5535
+ defineSubcommandGroup,
5536
+ repairQuotedArg as repairQuotedArg2,
5537
+ useBashMeta as useBashMeta2
5538
+ } from "@deepagents/context";
5539
+
5481
5540
  // packages/text2sql/src/lib/sql.ts
5482
5541
  import "@deepagents/agent";
5542
+ var text2SqlValidationMarker = Symbol("Text2SqlValidationError");
5543
+ var text2SqlUnknownAdapterMarker = Symbol("Text2SqlUnknownAdapterError");
5544
+ var Text2SqlValidationError = class _Text2SqlValidationError extends Error {
5545
+ [text2SqlValidationMarker];
5546
+ constructor(message) {
5547
+ super(message);
5548
+ this.name = "Text2SqlValidationError";
5549
+ this[text2SqlValidationMarker] = true;
5550
+ }
5551
+ static isInstance(error) {
5552
+ return error instanceof _Text2SqlValidationError && error[text2SqlValidationMarker] === true;
5553
+ }
5554
+ };
5555
+ var Text2SqlUnknownAdapterError = class _Text2SqlUnknownAdapterError extends Error {
5556
+ [text2SqlUnknownAdapterMarker];
5557
+ adapter;
5558
+ available;
5559
+ constructor(adapter, available) {
5560
+ const list = available.join(", ") || "(none configured)";
5561
+ super(`Unknown adapter "${adapter}". Available: ${list}`);
5562
+ this.name = "Text2SqlUnknownAdapterError";
5563
+ this.adapter = adapter;
5564
+ this.available = available;
5565
+ this[text2SqlUnknownAdapterMarker] = true;
5566
+ }
5567
+ static isInstance(error) {
5568
+ return error instanceof _Text2SqlUnknownAdapterError && error[text2SqlUnknownAdapterMarker] === true;
5569
+ }
5570
+ };
5483
5571
  var Text2Sql = class {
5484
5572
  #config;
5485
5573
  #indexer;
@@ -5498,19 +5586,59 @@ var Text2Sql = class {
5498
5586
  #requireAdapter(name) {
5499
5587
  const adapter = this.#config.adapters[name];
5500
5588
  if (!adapter) {
5501
- const available = Object.keys(this.#config.adapters).join(", ");
5502
- throw new Error(`Unknown adapter "${name}". Available: ${available}`);
5589
+ throw new Text2SqlUnknownAdapterError(name, this.adapterNames());
5503
5590
  }
5504
5591
  return adapter;
5505
5592
  }
5593
+ #requireModel(op) {
5594
+ const model = this.#config.model;
5595
+ if (!model) {
5596
+ throw new Error(`Text2Sql.${op}() requires a model in Text2SqlConfig`);
5597
+ }
5598
+ return model;
5599
+ }
5600
+ adapterNames() {
5601
+ return Object.keys(this.#config.adapters);
5602
+ }
5603
+ hasAdapter(name) {
5604
+ return name in this.#config.adapters;
5605
+ }
5606
+ async validate(name, sql) {
5607
+ const adapter = this.#requireAdapter(name);
5608
+ const result = this.#validateWith(adapter, sql);
5609
+ return result;
5610
+ }
5611
+ async run(name, sql) {
5612
+ const adapter = this.#requireAdapter(name);
5613
+ const formatted = await this.#validateWith(adapter, sql);
5614
+ const result = await adapter.execute(formatted);
5615
+ if (!Array.isArray(result)) {
5616
+ throw new Error("adapter.execute must return an array of rows");
5617
+ }
5618
+ const columns = result.length > 0 ? Object.keys(result[0]) : [];
5619
+ return { rows: result, columns };
5620
+ }
5621
+ async #validateWith(adapter, sql) {
5622
+ const formatted = adapter.format(sql);
5623
+ const error = await adapter.validate(formatted);
5624
+ if (error) throw new Text2SqlValidationError(error);
5625
+ return formatted;
5626
+ }
5627
+ async index(options = {}) {
5628
+ return this.#indexer.index({
5629
+ adapterNames: options.names,
5630
+ onProgress: options.onProgress
5631
+ });
5632
+ }
5506
5633
  async toSql(input, adapterName) {
5507
5634
  const adapter = this.#requireAdapter(adapterName);
5635
+ const model = this.#requireModel("toSql");
5508
5636
  const fragments = await this.#indexer.indexAdapter(adapterName);
5509
5637
  const result = await toSql({
5510
5638
  input,
5511
5639
  adapter,
5512
5640
  fragments,
5513
- model: this.#config.model
5641
+ model
5514
5642
  });
5515
5643
  return result.sql;
5516
5644
  }
@@ -5520,6 +5648,143 @@ var Text2Sql = class {
5520
5648
  return producer.toPairs();
5521
5649
  }
5522
5650
  };
5651
+
5652
+ // packages/text2sql/src/lib/sql-command.ts
5653
+ var SQL_VALIDATE_REMINDER2 = 'Always run `sql validate <db> "..."` before `sql run <db> "..."` to catch syntax errors early.';
5654
+ var SqlCommandError = class extends BashException2 {
5655
+ #subcommand;
5656
+ #exitCode;
5657
+ constructor(subcommand, message, exitCode = 1) {
5658
+ super(message);
5659
+ this.name = "SqlCommandError";
5660
+ this.#subcommand = subcommand;
5661
+ this.#exitCode = exitCode;
5662
+ }
5663
+ format() {
5664
+ return {
5665
+ stdout: "",
5666
+ stderr: `sql ${this.#subcommand}: ${this.message}
5667
+ `,
5668
+ exitCode: this.#exitCode
5669
+ };
5670
+ }
5671
+ };
5672
+ function createSqlCommand(text2Sql, options = {}) {
5673
+ const defaultOutputDir = options.outputDir ?? "/sql";
5674
+ const subcommands = {
5675
+ run: {
5676
+ usage: 'run <db> "SELECT ..."',
5677
+ description: "Execute query against <db> and store results",
5678
+ repair: repairDbNameAndQuotedArg,
5679
+ handler: (args, ctx) => handleRun(text2Sql, defaultOutputDir, args, ctx)
5680
+ },
5681
+ validate: {
5682
+ usage: 'validate <db> "SELECT ..."',
5683
+ description: "Validate query syntax against <db>",
5684
+ repair: repairDbNameAndQuotedArg,
5685
+ handler: (args) => handleValidate(text2Sql, args)
5686
+ }
5687
+ };
5688
+ return {
5689
+ command: defineSubcommandGroup("sql", subcommands),
5690
+ repair: buildSubcommandRepair2("sql", subcommands)
5691
+ };
5692
+ }
5693
+ function parseFlags(subcommand, args) {
5694
+ try {
5695
+ const parsed = parseArgs({
5696
+ args,
5697
+ options: { "out-dir": { type: "string" } },
5698
+ allowPositionals: true,
5699
+ strict: true
5700
+ });
5701
+ return {
5702
+ positional: parsed.positionals,
5703
+ outDir: parsed.values["out-dir"]
5704
+ };
5705
+ } catch (error) {
5706
+ const message = error instanceof Error ? error.message : String(error);
5707
+ throw new SqlCommandError(subcommand, message);
5708
+ }
5709
+ }
5710
+ function resolveOutputDir(ctx, flags, fallback) {
5711
+ const envOutDir = ctx.env.get("TEXT2SQL_OUT_DIR");
5712
+ const candidate = flags.outDir ?? envOutDir ?? fallback;
5713
+ return posix5.isAbsolute(candidate) ? candidate : posix5.resolve(ctx.cwd, candidate);
5714
+ }
5715
+ async function handleRun(text2Sql, defaultOutputDir, args, ctx) {
5716
+ return runHandler("run", async () => {
5717
+ const flags = parseFlags("run", args);
5718
+ const meta = useBashMeta2();
5719
+ meta?.setReminder(SQL_VALIDATE_REMINDER2);
5720
+ const { name, sql } = takeDbAndSql("run", flags.positional, text2Sql);
5721
+ const outputDir = resolveOutputDir(ctx, flags, defaultOutputDir);
5722
+ const { rows, columns } = await text2Sql.run(name, sql);
5723
+ const outPath = posix5.join(outputDir, `${v7()}.json`);
5724
+ await ctx.fs.mkdir(outputDir, { recursive: true });
5725
+ await ctx.fs.writeFile(outPath, JSON.stringify(rows, null, 2));
5726
+ return {
5727
+ stdout: `results stored in ${outPath}
5728
+ columns: ${columns.join(", ") || "(none)"}
5729
+ rows: ${rows.length}
5730
+ `,
5731
+ stderr: "",
5732
+ exitCode: 0
5733
+ };
5734
+ });
5735
+ }
5736
+ async function handleValidate(text2Sql, args) {
5737
+ return runHandler("validate", async () => {
5738
+ const flags = parseFlags("validate", args);
5739
+ const { name, sql } = takeDbAndSql("validate", flags.positional, text2Sql);
5740
+ await text2Sql.validate(name, sql);
5741
+ return { stdout: "valid\n", stderr: "", exitCode: 0 };
5742
+ });
5743
+ }
5744
+ async function runHandler(subcommand, fn) {
5745
+ try {
5746
+ return await fn();
5747
+ } catch (error) {
5748
+ if (error instanceof BashException2) return error.format();
5749
+ if (Text2SqlValidationError.isInstance(error) || Text2SqlUnknownAdapterError.isInstance(error)) {
5750
+ return new SqlCommandError(subcommand, error.message).format();
5751
+ }
5752
+ const message = error instanceof Error ? error.message : String(error);
5753
+ return new SqlCommandError(subcommand, message).format();
5754
+ }
5755
+ }
5756
+ function takeDbAndSql(subcommand, positional, text2Sql) {
5757
+ const [db, ...rest] = positional;
5758
+ const available = text2Sql.adapterNames().join(", ");
5759
+ if (!db) {
5760
+ throw new SqlCommandError(
5761
+ subcommand,
5762
+ `missing database name. Usage: sql ${subcommand} <db> "SELECT ...". Available: ${available}`
5763
+ );
5764
+ }
5765
+ if (!text2Sql.hasAdapter(db)) {
5766
+ throw new SqlCommandError(
5767
+ subcommand,
5768
+ `unknown database "${db}". Available: ${available}`
5769
+ );
5770
+ }
5771
+ const sql = rest.join(" ").trim();
5772
+ if (!sql) {
5773
+ throw new SqlCommandError(subcommand, "no query provided");
5774
+ }
5775
+ return { name: db, sql };
5776
+ }
5777
+ function repairDbNameAndQuotedArg(rawArgs) {
5778
+ const trimmed = rawArgs.trim();
5779
+ if (!trimmed) return null;
5780
+ const firstSpace = trimmed.search(/\s/);
5781
+ if (firstSpace === -1) return null;
5782
+ const dbName = trimmed.slice(0, firstSpace);
5783
+ const rest = trimmed.slice(firstSpace + 1);
5784
+ const repaired = repairQuotedArg2(rest);
5785
+ if (repaired == null) return null;
5786
+ return `${dbName} ${repaired}`;
5787
+ }
5523
5788
  export {
5524
5789
  Adapter,
5525
5790
  AdapterIndexer,
@@ -5536,10 +5801,13 @@ export {
5536
5801
  SqliteFs,
5537
5802
  TEXT2SQL_INDEX_PROGRESS_CHUNK,
5538
5803
  Text2Sql,
5804
+ Text2SqlUnknownAdapterError,
5805
+ Text2SqlValidationError,
5539
5806
  UnanswerableSQLError,
5540
5807
  applyTablesFilter,
5541
5808
  column,
5542
5809
  constraint,
5810
+ createSqlCommand,
5543
5811
  createSqlCommandHooks,
5544
5812
  createSqlMetaHook,
5545
5813
  dialectInfo,