@deepagents/text2sql 1.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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(
@@ -1442,7 +1489,7 @@ var AdapterIndexer = class {
1442
1489
  return configured;
1443
1490
  }
1444
1491
  if (this.#version) {
1445
- return `introspection-${this.#version}-${name}`;
1492
+ return `index-${this.#version}-${name}`;
1446
1493
  }
1447
1494
  return void 0;
1448
1495
  }
@@ -5478,8 +5525,48 @@ 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
+ buildSubcommandRepair as buildSubcommandRepair2,
5534
+ defineSubcommandGroup,
5535
+ repairQuotedArg as repairQuotedArg2,
5536
+ useBashMeta as useBashMeta2
5537
+ } from "@deepagents/context";
5538
+
5481
5539
  // packages/text2sql/src/lib/sql.ts
5482
5540
  import "@deepagents/agent";
5541
+ var text2SqlValidationMarker = Symbol("Text2SqlValidationError");
5542
+ var text2SqlUnknownAdapterMarker = Symbol("Text2SqlUnknownAdapterError");
5543
+ var Text2SqlValidationError = class _Text2SqlValidationError extends Error {
5544
+ [text2SqlValidationMarker];
5545
+ constructor(message) {
5546
+ super(message);
5547
+ this.name = "Text2SqlValidationError";
5548
+ this[text2SqlValidationMarker] = true;
5549
+ }
5550
+ static isInstance(error) {
5551
+ return error instanceof _Text2SqlValidationError && error[text2SqlValidationMarker] === true;
5552
+ }
5553
+ };
5554
+ var Text2SqlUnknownAdapterError = class _Text2SqlUnknownAdapterError extends Error {
5555
+ [text2SqlUnknownAdapterMarker];
5556
+ adapter;
5557
+ available;
5558
+ constructor(adapter, available) {
5559
+ const list = available.join(", ") || "(none configured)";
5560
+ super(`Unknown adapter "${adapter}". Available: ${list}`);
5561
+ this.name = "Text2SqlUnknownAdapterError";
5562
+ this.adapter = adapter;
5563
+ this.available = available;
5564
+ this[text2SqlUnknownAdapterMarker] = true;
5565
+ }
5566
+ static isInstance(error) {
5567
+ return error instanceof _Text2SqlUnknownAdapterError && error[text2SqlUnknownAdapterMarker] === true;
5568
+ }
5569
+ };
5483
5570
  var Text2Sql = class {
5484
5571
  #config;
5485
5572
  #indexer;
@@ -5498,19 +5585,59 @@ var Text2Sql = class {
5498
5585
  #requireAdapter(name) {
5499
5586
  const adapter = this.#config.adapters[name];
5500
5587
  if (!adapter) {
5501
- const available = Object.keys(this.#config.adapters).join(", ");
5502
- throw new Error(`Unknown adapter "${name}". Available: ${available}`);
5588
+ throw new Text2SqlUnknownAdapterError(name, this.adapterNames());
5503
5589
  }
5504
5590
  return adapter;
5505
5591
  }
5592
+ #requireModel(op) {
5593
+ const model = this.#config.model;
5594
+ if (!model) {
5595
+ throw new Error(`Text2Sql.${op}() requires a model in Text2SqlConfig`);
5596
+ }
5597
+ return model;
5598
+ }
5599
+ adapterNames() {
5600
+ return Object.keys(this.#config.adapters);
5601
+ }
5602
+ hasAdapter(name) {
5603
+ return name in this.#config.adapters;
5604
+ }
5605
+ async validate(name, sql) {
5606
+ const adapter = this.#requireAdapter(name);
5607
+ const result = this.#validateWith(adapter, sql);
5608
+ return result;
5609
+ }
5610
+ async run(name, sql) {
5611
+ const adapter = this.#requireAdapter(name);
5612
+ const formatted = await this.#validateWith(adapter, sql);
5613
+ const result = await adapter.execute(formatted);
5614
+ if (!Array.isArray(result)) {
5615
+ throw new Error("adapter.execute must return an array of rows");
5616
+ }
5617
+ const columns = result.length > 0 ? Object.keys(result[0]) : [];
5618
+ return { rows: result, columns };
5619
+ }
5620
+ async #validateWith(adapter, sql) {
5621
+ const formatted = adapter.format(sql);
5622
+ const error = await adapter.validate(formatted);
5623
+ if (error) throw new Text2SqlValidationError(error);
5624
+ return formatted;
5625
+ }
5626
+ async index(options = {}) {
5627
+ return this.#indexer.index({
5628
+ adapterNames: options.names,
5629
+ onProgress: options.onProgress
5630
+ });
5631
+ }
5506
5632
  async toSql(input, adapterName) {
5507
5633
  const adapter = this.#requireAdapter(adapterName);
5634
+ const model = this.#requireModel("toSql");
5508
5635
  const fragments = await this.#indexer.indexAdapter(adapterName);
5509
5636
  const result = await toSql({
5510
5637
  input,
5511
5638
  adapter,
5512
5639
  fragments,
5513
- model: this.#config.model
5640
+ model
5514
5641
  });
5515
5642
  return result.sql;
5516
5643
  }
@@ -5520,6 +5647,196 @@ var Text2Sql = class {
5520
5647
  return producer.toPairs();
5521
5648
  }
5522
5649
  };
5650
+
5651
+ // packages/text2sql/src/lib/sql-command.ts
5652
+ var SQL_VALIDATE_REMINDER2 = 'Always run `sql validate <db> "..."` before `sql run <db> "..."` to catch syntax errors early.';
5653
+ var SqlCommandError = class extends Error {
5654
+ exitCode;
5655
+ constructor(message, exitCode = 1) {
5656
+ super(message);
5657
+ this.name = "SqlCommandError";
5658
+ this.exitCode = exitCode;
5659
+ }
5660
+ };
5661
+ function createSqlCommand(text2Sql, options = {}) {
5662
+ const defaultOutputDir = options.outputDir ?? "/sql";
5663
+ const subcommands = {
5664
+ run: {
5665
+ usage: 'run <db> "SELECT ..."',
5666
+ description: "Execute query against <db> and store results",
5667
+ repair: repairDbNameAndQuotedArg,
5668
+ handler: (args, ctx) => handleRun(text2Sql, defaultOutputDir, args, ctx)
5669
+ },
5670
+ validate: {
5671
+ usage: 'validate <db> "SELECT ..."',
5672
+ description: "Validate query syntax against <db>",
5673
+ repair: repairDbNameAndQuotedArg,
5674
+ handler: (args) => handleValidate(text2Sql, args)
5675
+ },
5676
+ index: {
5677
+ usage: "index [adapter ...]",
5678
+ description: "Index adapter schemas and write context artifacts",
5679
+ handler: (args, ctx) => handleIndex(text2Sql, defaultOutputDir, args, ctx)
5680
+ }
5681
+ };
5682
+ return {
5683
+ command: defineSubcommandGroup("sql", subcommands),
5684
+ repair: buildSubcommandRepair2("sql", subcommands)
5685
+ };
5686
+ }
5687
+ function parseFlags(args) {
5688
+ let parsed;
5689
+ try {
5690
+ parsed = parseArgs({
5691
+ args,
5692
+ options: {
5693
+ "out-dir": { type: "string" },
5694
+ all: { type: "boolean" }
5695
+ },
5696
+ allowPositionals: true,
5697
+ strict: true
5698
+ });
5699
+ } catch (error) {
5700
+ const message = error instanceof Error ? error.message : String(error);
5701
+ throw new SqlCommandError(message, 1);
5702
+ }
5703
+ return {
5704
+ positional: parsed.positionals,
5705
+ outDir: parsed.values["out-dir"],
5706
+ all: parsed.values.all === true
5707
+ };
5708
+ }
5709
+ function resolveOutputDir(ctx, flags, fallback) {
5710
+ const envOutDir = ctx.env.get("TEXT2SQL_OUT_DIR");
5711
+ const candidate = flags.outDir ?? envOutDir ?? fallback;
5712
+ return posix5.isAbsolute(candidate) ? candidate : posix5.resolve(ctx.cwd, candidate);
5713
+ }
5714
+ async function handleRun(text2Sql, defaultOutputDir, args, ctx) {
5715
+ return runHandler("run", async () => {
5716
+ const flags = parseFlags(args);
5717
+ const meta = useBashMeta2();
5718
+ meta?.setReminder(SQL_VALIDATE_REMINDER2);
5719
+ const { name, sql } = takeDbAndSql(flags.positional, "run", text2Sql);
5720
+ const outputDir = resolveOutputDir(ctx, flags, defaultOutputDir);
5721
+ const { rows, columns } = await text2Sql.run(name, sql);
5722
+ const outPath = posix5.join(outputDir, `${v7()}.json`);
5723
+ await ctx.fs.mkdir(outputDir, { recursive: true });
5724
+ await ctx.fs.writeFile(outPath, JSON.stringify(rows, null, 2));
5725
+ return {
5726
+ stdout: `results stored in ${outPath}
5727
+ columns: ${columns.join(", ") || "(none)"}
5728
+ rows: ${rows.length}
5729
+ `,
5730
+ stderr: "",
5731
+ exitCode: 0
5732
+ };
5733
+ });
5734
+ }
5735
+ async function handleValidate(text2Sql, args) {
5736
+ return runHandler("validate", async () => {
5737
+ const flags = parseFlags(args);
5738
+ const { name, sql } = takeDbAndSql(flags.positional, "validate", text2Sql);
5739
+ await text2Sql.validate(name, sql);
5740
+ return { stdout: "valid\n", stderr: "", exitCode: 0 };
5741
+ });
5742
+ }
5743
+ async function handleIndex(text2Sql, defaultOutputDir, args, ctx) {
5744
+ return runHandler("index", async () => {
5745
+ const flags = parseFlags(args);
5746
+ const available = text2Sql.adapterNames();
5747
+ const requested = flags.all ? [] : dedupe(flags.positional);
5748
+ const names = requested.length === 0 ? available : requested;
5749
+ for (const name of names) {
5750
+ if (!text2Sql.hasAdapter(name)) {
5751
+ throw new SqlCommandError(
5752
+ `unknown adapter "${name}". Available: ${available.join(", ")}`,
5753
+ 1
5754
+ );
5755
+ }
5756
+ }
5757
+ const outputDir = resolveOutputDir(ctx, flags, defaultOutputDir);
5758
+ const fragments = await text2Sql.index({ names });
5759
+ const fragmentsPath = posix5.join(outputDir, `index-${v7()}.json`);
5760
+ await ctx.fs.mkdir(outputDir, { recursive: true });
5761
+ await ctx.fs.writeFile(fragmentsPath, JSON.stringify(fragments, null, 2));
5762
+ const manifest = {
5763
+ fragmentsPath,
5764
+ eventsPath: null,
5765
+ adapters: names,
5766
+ fragments: countSchemaFragments(fragments)
5767
+ };
5768
+ return {
5769
+ stdout: JSON.stringify(manifest, null, 2) + "\n",
5770
+ stderr: "",
5771
+ exitCode: 0
5772
+ };
5773
+ });
5774
+ }
5775
+ async function runHandler(name, fn) {
5776
+ try {
5777
+ return await fn();
5778
+ } catch (error) {
5779
+ if (error instanceof SqlCommandError) {
5780
+ return {
5781
+ stdout: "",
5782
+ stderr: `sql ${name}: ${error.message}
5783
+ `,
5784
+ exitCode: error.exitCode
5785
+ };
5786
+ }
5787
+ if (Text2SqlValidationError.isInstance(error) || Text2SqlUnknownAdapterError.isInstance(error)) {
5788
+ return {
5789
+ stdout: "",
5790
+ stderr: `sql ${name}: ${error.message}
5791
+ `,
5792
+ exitCode: 1
5793
+ };
5794
+ }
5795
+ const message = error instanceof Error ? error.message : String(error);
5796
+ return { stdout: "", stderr: `sql ${name}: ${message}
5797
+ `, exitCode: 1 };
5798
+ }
5799
+ }
5800
+ function takeDbAndSql(positional, cmd, text2Sql) {
5801
+ const [db, ...rest] = positional;
5802
+ const available = text2Sql.adapterNames().join(", ");
5803
+ if (!db) {
5804
+ throw new SqlCommandError(
5805
+ `missing database name. Usage: sql ${cmd} <db> "SELECT ...". Available: ${available}`,
5806
+ 1
5807
+ );
5808
+ }
5809
+ if (!text2Sql.hasAdapter(db)) {
5810
+ throw new SqlCommandError(
5811
+ `unknown database "${db}". Available: ${available}`,
5812
+ 1
5813
+ );
5814
+ }
5815
+ const sql = rest.join(" ").trim();
5816
+ if (!sql) {
5817
+ throw new SqlCommandError("no query provided", 1);
5818
+ }
5819
+ return { name: db, sql };
5820
+ }
5821
+ function repairDbNameAndQuotedArg(rawArgs) {
5822
+ const trimmed = rawArgs.trim();
5823
+ if (!trimmed) return null;
5824
+ const firstSpace = trimmed.search(/\s/);
5825
+ if (firstSpace === -1) return null;
5826
+ const dbName = trimmed.slice(0, firstSpace);
5827
+ const rest = trimmed.slice(firstSpace + 1);
5828
+ const repaired = repairQuotedArg2(rest);
5829
+ if (repaired == null) return null;
5830
+ return `${dbName} ${repaired}`;
5831
+ }
5832
+ function dedupe(values) {
5833
+ return [...new Set(values)];
5834
+ }
5835
+ function countSchemaFragments(fragments) {
5836
+ return fragments.reduce((count, adapterFragment) => {
5837
+ return Array.isArray(adapterFragment.data) ? count + adapterFragment.data.length : count + 1;
5838
+ }, 0);
5839
+ }
5523
5840
  export {
5524
5841
  Adapter,
5525
5842
  AdapterIndexer,
@@ -5536,10 +5853,13 @@ export {
5536
5853
  SqliteFs,
5537
5854
  TEXT2SQL_INDEX_PROGRESS_CHUNK,
5538
5855
  Text2Sql,
5856
+ Text2SqlUnknownAdapterError,
5857
+ Text2SqlValidationError,
5539
5858
  UnanswerableSQLError,
5540
5859
  applyTablesFilter,
5541
5860
  column,
5542
5861
  constraint,
5862
+ createSqlCommand,
5543
5863
  createSqlCommandHooks,
5544
5864
  createSqlMetaHook,
5545
5865
  dialectInfo,