@deepagents/text2sql 7.0.2 → 7.1.1

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.
Files changed (184) hide show
  1. package/README.md +63 -75
  2. package/dist/bin/sql.js +82 -599
  3. package/dist/eval/src/evals/models.d.ts +12 -0
  4. package/dist/eval/src/evals/models.d.ts.map +1 -0
  5. package/dist/eval/src/evals/sql-create-context/sql-create-context.eval.d.ts +2 -0
  6. package/dist/eval/src/evals/sql-create-context/sql-create-context.eval.d.ts.map +1 -0
  7. package/dist/index.d.ts +0 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +920 -706
  10. package/dist/index.js.map +4 -4
  11. package/dist/lib/adapter-index.d.ts +0 -2
  12. package/dist/lib/adapter-index.d.ts.map +1 -1
  13. package/dist/lib/adapters/adapter.d.ts +23 -1
  14. package/dist/lib/adapters/adapter.d.ts.map +1 -1
  15. package/dist/lib/adapters/bigquery/bigquery-fk.d.ts +0 -11
  16. package/dist/lib/adapters/bigquery/bigquery-fk.d.ts.map +1 -1
  17. package/dist/lib/adapters/bigquery/bigquery-rows.d.ts +22 -0
  18. package/dist/lib/adapters/bigquery/bigquery-rows.d.ts.map +1 -0
  19. package/dist/lib/adapters/bigquery/bigquery.d.ts +1 -1
  20. package/dist/lib/adapters/bigquery/bigquery.d.ts.map +1 -1
  21. package/dist/lib/adapters/bigquery/constraint.bigquery.grounding.d.ts.map +1 -1
  22. package/dist/lib/adapters/bigquery/index.d.ts +6 -13
  23. package/dist/lib/adapters/bigquery/index.d.ts.map +1 -1
  24. package/dist/lib/adapters/bigquery/index.js +244 -51
  25. package/dist/lib/adapters/bigquery/index.js.map +4 -4
  26. package/dist/lib/adapters/bigquery/indexes.bigquery.grounding.d.ts.map +1 -1
  27. package/dist/lib/adapters/bigquery/row-count.bigquery.grounding.d.ts.map +1 -1
  28. package/dist/lib/adapters/bigquery/table.bigquery.grounding.d.ts.map +1 -1
  29. package/dist/lib/adapters/bigquery/view.bigquery.grounding.d.ts.map +1 -1
  30. package/dist/lib/adapters/clickhouse/clickhouse-rows.d.ts +7 -0
  31. package/dist/lib/adapters/clickhouse/clickhouse-rows.d.ts.map +1 -0
  32. package/dist/lib/adapters/clickhouse/clickhouse.d.ts +1 -1
  33. package/dist/lib/adapters/clickhouse/clickhouse.d.ts.map +1 -1
  34. package/dist/lib/adapters/clickhouse/constraint.clickhouse.grounding.d.ts.map +1 -1
  35. package/dist/lib/adapters/clickhouse/index.js +133 -44
  36. package/dist/lib/adapters/clickhouse/index.js.map +4 -4
  37. package/dist/lib/adapters/clickhouse/indexes.clickhouse.grounding.d.ts.map +1 -1
  38. package/dist/lib/adapters/clickhouse/info.clickhouse.grounding.d.ts.map +1 -1
  39. package/dist/lib/adapters/clickhouse/row-count.clickhouse.grounding.d.ts.map +1 -1
  40. package/dist/lib/adapters/clickhouse/table.clickhouse.grounding.d.ts.map +1 -1
  41. package/dist/lib/adapters/clickhouse/view.clickhouse.grounding.d.ts.map +1 -1
  42. package/dist/lib/adapters/duckdb/duckdb.d.ts +1 -1
  43. package/dist/lib/adapters/duckdb/duckdb.d.ts.map +1 -1
  44. package/dist/lib/adapters/duckdb/duckdb.groundings.d.ts.map +1 -1
  45. package/dist/lib/adapters/duckdb/index.d.ts +8 -10
  46. package/dist/lib/adapters/duckdb/index.d.ts.map +1 -1
  47. package/dist/lib/adapters/duckdb/index.js +1549 -1412
  48. package/dist/lib/adapters/duckdb/index.js.map +4 -4
  49. package/dist/lib/adapters/groundings/index.js.map +1 -1
  50. package/dist/lib/adapters/groundings/rows.d.ts +32 -0
  51. package/dist/lib/adapters/groundings/rows.d.ts.map +1 -0
  52. package/dist/lib/adapters/mysql/column-stats.mysql.grounding.d.ts.map +1 -1
  53. package/dist/lib/adapters/mysql/column-values.mysql.grounding.d.ts.map +1 -1
  54. package/dist/lib/adapters/mysql/constraint.mysql.grounding.d.ts.map +1 -1
  55. package/dist/lib/adapters/mysql/index.d.ts +2 -4
  56. package/dist/lib/adapters/mysql/index.d.ts.map +1 -1
  57. package/dist/lib/adapters/mysql/index.js +230 -76
  58. package/dist/lib/adapters/mysql/index.js.map +4 -4
  59. package/dist/lib/adapters/mysql/indexes.mysql.grounding.d.ts.map +1 -1
  60. package/dist/lib/adapters/mysql/info.mysql.grounding.d.ts.map +1 -1
  61. package/dist/lib/adapters/mysql/mysql-rows.d.ts +11 -0
  62. package/dist/lib/adapters/mysql/mysql-rows.d.ts.map +1 -0
  63. package/dist/lib/adapters/mysql/mysql.d.ts +1 -1
  64. package/dist/lib/adapters/mysql/mysql.d.ts.map +1 -1
  65. package/dist/lib/adapters/mysql/row-count.mysql.grounding.d.ts.map +1 -1
  66. package/dist/lib/adapters/mysql/table.mysql.grounding.d.ts +3 -2
  67. package/dist/lib/adapters/mysql/table.mysql.grounding.d.ts.map +1 -1
  68. package/dist/lib/adapters/mysql/view.mysql.grounding.d.ts +2 -2
  69. package/dist/lib/adapters/mysql/view.mysql.grounding.d.ts.map +1 -1
  70. package/dist/lib/adapters/postgres/column-stats.postgres.grounding.d.ts.map +1 -1
  71. package/dist/lib/adapters/postgres/column-values.postgres.grounding.d.ts.map +1 -1
  72. package/dist/lib/adapters/postgres/constraint.postgres.grounding.d.ts.map +1 -1
  73. package/dist/lib/adapters/postgres/index.d.ts +2 -4
  74. package/dist/lib/adapters/postgres/index.d.ts.map +1 -1
  75. package/dist/lib/adapters/postgres/index.js +266 -487
  76. package/dist/lib/adapters/postgres/index.js.map +4 -4
  77. package/dist/lib/adapters/postgres/indexes.postgres.grounding.d.ts +1 -5
  78. package/dist/lib/adapters/postgres/indexes.postgres.grounding.d.ts.map +1 -1
  79. package/dist/lib/adapters/postgres/info.postgres.grounding.d.ts.map +1 -1
  80. package/dist/lib/adapters/postgres/postgres-rows.d.ts +7 -0
  81. package/dist/lib/adapters/postgres/postgres-rows.d.ts.map +1 -0
  82. package/dist/lib/adapters/postgres/postgres.d.ts +6 -1
  83. package/dist/lib/adapters/postgres/postgres.d.ts.map +1 -1
  84. package/dist/lib/adapters/postgres/row-count.postgres.grounding.d.ts.map +1 -1
  85. package/dist/lib/adapters/postgres/table.postgres.grounding.d.ts +5 -4
  86. package/dist/lib/adapters/postgres/table.postgres.grounding.d.ts.map +1 -1
  87. package/dist/lib/adapters/postgres/view.postgres.grounding.d.ts +3 -3
  88. package/dist/lib/adapters/postgres/view.postgres.grounding.d.ts.map +1 -1
  89. package/dist/lib/adapters/posthog/definitions.posthog.grounding.d.ts.map +1 -1
  90. package/dist/lib/adapters/posthog/index.d.ts +4 -5
  91. package/dist/lib/adapters/posthog/index.d.ts.map +1 -1
  92. package/dist/lib/adapters/posthog/index.js +349 -287
  93. package/dist/lib/adapters/posthog/index.js.map +4 -4
  94. package/dist/lib/adapters/posthog/posthog.d.ts +2 -2
  95. package/dist/lib/adapters/posthog/posthog.d.ts.map +1 -1
  96. package/dist/lib/adapters/posthog/posthog.sql-policy.d.ts.map +1 -1
  97. package/dist/lib/adapters/posthog/schema.posthog.grounding.d.ts.map +1 -1
  98. package/dist/lib/adapters/posthog/transport.d.ts.map +1 -1
  99. package/dist/lib/adapters/posthog/types.d.ts +16 -6
  100. package/dist/lib/adapters/posthog/types.d.ts.map +1 -1
  101. package/dist/lib/adapters/spreadsheet/index.js +117 -38
  102. package/dist/lib/adapters/spreadsheet/index.js.map +4 -4
  103. package/dist/lib/adapters/sqlite/column-stats.sqlite.grounding.d.ts.map +1 -1
  104. package/dist/lib/adapters/sqlite/column-values.sqlite.grounding.d.ts.map +1 -1
  105. package/dist/lib/adapters/sqlite/constraint.sqlite.grounding.d.ts.map +1 -1
  106. package/dist/lib/adapters/sqlite/index.js +136 -52
  107. package/dist/lib/adapters/sqlite/index.js.map +4 -4
  108. package/dist/lib/adapters/sqlite/indexes.sqlite.grounding.d.ts.map +1 -1
  109. package/dist/lib/adapters/sqlite/info.sqlite.grounding.d.ts.map +1 -1
  110. package/dist/lib/adapters/sqlite/row-count.sqlite.grounding.d.ts.map +1 -1
  111. package/dist/lib/adapters/sqlite/sqlite-rows.d.ts +41 -0
  112. package/dist/lib/adapters/sqlite/sqlite-rows.d.ts.map +1 -0
  113. package/dist/lib/adapters/sqlite/sqlite.d.ts +1 -1
  114. package/dist/lib/adapters/sqlite/sqlite.d.ts.map +1 -1
  115. package/dist/lib/adapters/sqlite/table.sqlite.grounding.d.ts.map +1 -1
  116. package/dist/lib/adapters/sqlite/view.sqlite.grounding.d.ts.map +1 -1
  117. package/dist/lib/adapters/sqlserver/column-stats.sqlserver.grounding.d.ts.map +1 -1
  118. package/dist/lib/adapters/sqlserver/column-values.sqlserver.grounding.d.ts.map +1 -1
  119. package/dist/lib/adapters/sqlserver/constraint.sqlserver.grounding.d.ts.map +1 -1
  120. package/dist/lib/adapters/sqlserver/index.d.ts +2 -4
  121. package/dist/lib/adapters/sqlserver/index.d.ts.map +1 -1
  122. package/dist/lib/adapters/sqlserver/index.js +255 -503
  123. package/dist/lib/adapters/sqlserver/index.js.map +4 -4
  124. package/dist/lib/adapters/sqlserver/indexes.sqlserver.grounding.d.ts +1 -5
  125. package/dist/lib/adapters/sqlserver/indexes.sqlserver.grounding.d.ts.map +1 -1
  126. package/dist/lib/adapters/sqlserver/info.sqlserver.grounding.d.ts.map +1 -1
  127. package/dist/lib/adapters/sqlserver/row-count.sqlserver.grounding.d.ts.map +1 -1
  128. package/dist/lib/adapters/sqlserver/sqlserver-rows.d.ts +7 -0
  129. package/dist/lib/adapters/sqlserver/sqlserver-rows.d.ts.map +1 -0
  130. package/dist/lib/adapters/sqlserver/sqlserver.d.ts +6 -1
  131. package/dist/lib/adapters/sqlserver/sqlserver.d.ts.map +1 -1
  132. package/dist/lib/adapters/sqlserver/table.sqlserver.grounding.d.ts +5 -4
  133. package/dist/lib/adapters/sqlserver/table.sqlserver.grounding.d.ts.map +1 -1
  134. package/dist/lib/adapters/sqlserver/view.sqlserver.grounding.d.ts +3 -3
  135. package/dist/lib/adapters/sqlserver/view.sqlserver.grounding.d.ts.map +1 -1
  136. package/dist/lib/agents/question.agent.d.ts.map +1 -1
  137. package/dist/lib/agents/sql-transform-plugins.d.ts.map +1 -1
  138. package/dist/lib/agents/suggestions.agents.d.ts.map +1 -1
  139. package/dist/lib/checkpoint.d.ts +34 -21
  140. package/dist/lib/checkpoint.d.ts.map +1 -1
  141. package/dist/lib/file-cache.d.ts +6 -1
  142. package/dist/lib/file-cache.d.ts.map +1 -1
  143. package/dist/lib/fs/columns.d.ts +14 -0
  144. package/dist/lib/fs/columns.d.ts.map +1 -0
  145. package/dist/lib/fs/mssql/mssql-fs.d.ts +4 -4
  146. package/dist/lib/fs/mssql/mssql-fs.d.ts.map +1 -1
  147. package/dist/lib/fs/postgres/postgres-fs.d.ts +4 -4
  148. package/dist/lib/fs/postgres/postgres-fs.d.ts.map +1 -1
  149. package/dist/lib/fs/sqlite/sqlite-fs.d.ts +4 -4
  150. package/dist/lib/fs/sqlite/sqlite-fs.d.ts.map +1 -1
  151. package/dist/lib/index-cache.d.ts +6 -6
  152. package/dist/lib/index-cache.d.ts.map +1 -1
  153. package/dist/lib/sql-command.d.ts +1 -4
  154. package/dist/lib/sql-command.d.ts.map +1 -1
  155. package/dist/lib/sql.d.ts +0 -5
  156. package/dist/lib/sql.d.ts.map +1 -1
  157. package/dist/lib/synthesis/decorators/deduplicated-producer.d.ts +1 -2
  158. package/dist/lib/synthesis/decorators/deduplicated-producer.d.ts.map +1 -1
  159. package/dist/lib/synthesis/decorators/filtered-producer.d.ts +0 -2
  160. package/dist/lib/synthesis/decorators/filtered-producer.d.ts.map +1 -1
  161. package/dist/lib/synthesis/decorators/validated-producer.d.ts +1 -3
  162. package/dist/lib/synthesis/decorators/validated-producer.d.ts.map +1 -1
  163. package/dist/lib/synthesis/extractors/base-contextual-extractor.d.ts.map +1 -1
  164. package/dist/lib/synthesis/extractors/message-extractor.d.ts.map +1 -1
  165. package/dist/lib/synthesis/extractors/segmented-context-extractor.d.ts.map +1 -1
  166. package/dist/lib/synthesis/extractors/sql-extractor.d.ts.map +1 -1
  167. package/dist/lib/synthesis/extractors/tool-input.d.ts +6 -0
  168. package/dist/lib/synthesis/extractors/tool-input.d.ts.map +1 -0
  169. package/dist/lib/synthesis/index.js +124 -114
  170. package/dist/lib/synthesis/index.js.map +4 -4
  171. package/dist/lib/synthesis/synthesizers/breadth-evolver.d.ts +0 -2
  172. package/dist/lib/synthesis/synthesizers/breadth-evolver.d.ts.map +1 -1
  173. package/dist/lib/synthesis/synthesizers/depth-evolver.d.ts +0 -3
  174. package/dist/lib/synthesis/synthesizers/depth-evolver.d.ts.map +1 -1
  175. package/dist/lib/synthesis/synthesizers/persona-generator.d.ts.map +1 -1
  176. package/dist/lib/synthesis/synthesizers/schema-synthesizer.d.ts +0 -2
  177. package/dist/lib/synthesis/synthesizers/schema-synthesizer.d.ts.map +1 -1
  178. package/dist/scripts/scripts/bigquery-test.d.ts +2 -0
  179. package/dist/scripts/scripts/bigquery-test.d.ts.map +1 -0
  180. package/package.json +4 -6
  181. package/dist/lib/index-lock.d.ts +0 -60
  182. package/dist/lib/index-lock.d.ts.map +0 -1
  183. package/dist/lib/prompt.d.ts +0 -2
  184. package/dist/lib/prompt.d.ts.map +0 -1
@@ -1,1554 +1,1674 @@
1
- // packages/text2sql/src/lib/adapters/groundings/abstract.grounding.ts
2
- function filterColumns(columns, filter) {
3
- if (Array.isArray(filter)) {
4
- return columns.filter((col) => filter.includes(col.name));
1
+ // packages/text2sql/src/lib/adapters/adapter.ts
2
+ import { format as formatSql } from "sql-formatter";
3
+ import { z } from "zod";
4
+
5
+ // packages/text2sql/src/lib/agents/exceptions.ts
6
+ var sqlValidationMarker = /* @__PURE__ */ Symbol("SQLValidationError");
7
+ var unanswerableSqlMarker = /* @__PURE__ */ Symbol("UnanswerableSQLError");
8
+ var sqlScopeMarker = /* @__PURE__ */ Symbol("SQLScopeError");
9
+ var sqlReadOnlyMarker = /* @__PURE__ */ Symbol("SQLReadOnlyError");
10
+ var SQLValidationError = class _SQLValidationError extends Error {
11
+ [sqlValidationMarker];
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "SQLValidationError";
15
+ this[sqlValidationMarker] = true;
5
16
  }
6
- if (filter instanceof RegExp) {
7
- return columns.filter((col) => filter.test(col.name));
17
+ static isInstance(error) {
18
+ return error instanceof _SQLValidationError && error[sqlValidationMarker] === true;
8
19
  }
9
- return columns.filter((col) => filter(col.name));
10
- }
11
- function applyColumnFilter(entity, columnsConfig) {
12
- if (!columnsConfig) return entity;
13
- const filter = columnsConfig[entity.name];
14
- if (!filter) return entity;
15
- return { ...entity, columns: filterColumns(entity.columns, filter) };
16
- }
17
- var AbstractGrounding = class {
18
- /**
19
- * Grounding identifier for debugging/logging.
20
- */
21
- name;
22
- phase;
23
- constructor(name, phase) {
24
- this.name = name;
25
- this.phase = phase;
20
+ };
21
+ var UnanswerableSQLError = class _UnanswerableSQLError extends Error {
22
+ [unanswerableSqlMarker];
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "UnanswerableSQLError";
26
+ this[unanswerableSqlMarker] = true;
26
27
  }
27
- /**
28
- * Populate ctx with the tables/views this grounding makes available for scope
29
- * resolution. Annotators decorate entities other groundings produced and add
30
- * no membership, so the default is a no-op; entity-producing groundings
31
- * override it. This lets scope resolution skip the annotators' metadata scans.
32
- *
33
- * @param _ctx - Shared context for accumulating entity membership
34
- */
35
- async contributeEntities(_ctx) {
28
+ static isInstance(error) {
29
+ return error instanceof _UnanswerableSQLError && error[unanswerableSqlMarker] === true;
36
30
  }
37
31
  };
38
-
39
- // packages/text2sql/src/lib/adapters/groundings/column-stats.grounding.ts
40
- var ColumnStatsGrounding = class extends AbstractGrounding {
41
- constructor(config = {}) {
42
- super("columnStats", "column_stats");
32
+ var SQLScopeError = class _SQLScopeError extends Error {
33
+ [sqlScopeMarker];
34
+ payload;
35
+ errorType;
36
+ constructor(payload) {
37
+ super(JSON.stringify(payload));
38
+ this.name = "SQLScopeError";
39
+ this.payload = payload;
40
+ this.errorType = payload.error_type;
41
+ this[sqlScopeMarker] = true;
43
42
  }
44
- /**
45
- * Execute the grounding process.
46
- * Annotates columns in ctx.tables and ctx.views with statistics.
47
- */
48
- async execute(ctx) {
49
- const allContainers = [...ctx.tables, ...ctx.views];
50
- const total = allContainers.length;
51
- for (let i = 0; i < allContainers.length; i++) {
52
- const container = allContainers[i];
53
- ctx.onProgress({
54
- type: "phase:progress",
55
- phase: "column_stats",
56
- table: container.name,
57
- message: `Collecting stats for ${container.name}...`,
58
- current: i + 1,
59
- total
60
- });
61
- for (const column2 of container.columns) {
62
- try {
63
- const stats = await this.collectStats(container.name, column2);
64
- if (stats) {
65
- column2.stats = stats;
66
- }
67
- } catch (error) {
68
- console.warn(
69
- "Error collecting stats for",
70
- container.name,
71
- column2.name,
72
- error
73
- );
74
- }
75
- }
76
- }
43
+ static isInstance(error) {
44
+ return error instanceof _SQLScopeError && error[sqlScopeMarker] === true;
77
45
  }
78
46
  };
79
-
80
- // packages/text2sql/src/lib/adapters/groundings/column-values.grounding.ts
81
- var ColumnValuesGrounding = class extends AbstractGrounding {
82
- lowCardinalityLimit;
83
- maxValueLength;
84
- constructor(config = {}) {
85
- super("columnValues", "column_values");
86
- this.lowCardinalityLimit = config.lowCardinalityLimit ?? 20;
87
- this.maxValueLength = config.maxValueLength ?? 100;
47
+ var SQLReadOnlyError = class _SQLReadOnlyError extends Error {
48
+ [sqlReadOnlyMarker];
49
+ constructor(message) {
50
+ super(message);
51
+ this.name = "SQLReadOnlyError";
52
+ this[sqlReadOnlyMarker] = true;
88
53
  }
89
- /**
90
- * Get values for native ENUM type columns.
91
- * Return undefined if column is not an ENUM type.
92
- * Default implementation returns undefined (no native ENUM support).
93
- */
94
- async collectEnumValues(_tableName, _column) {
95
- return void 0;
54
+ static isInstance(error) {
55
+ return error instanceof _SQLReadOnlyError && error[sqlReadOnlyMarker] === true;
96
56
  }
97
- /**
98
- * Parse CHECK constraint for enum-like IN clause.
99
- * Extracts values from patterns like:
100
- * - CHECK (status IN ('active', 'inactive'))
101
- * - CHECK ((status)::text = ANY (ARRAY['a'::text, 'b'::text]))
102
- * - CHECK (status = 'active' OR status = 'inactive')
103
- */
104
- parseCheckConstraint(constraint2, columnName) {
105
- if (constraint2.type !== "CHECK" || !constraint2.definition) {
106
- return void 0;
57
+ };
58
+
59
+ // packages/text2sql/src/lib/fragments/schema.ts
60
+ function dialectInfo(input) {
61
+ return {
62
+ name: "dialectInfo",
63
+ data: {
64
+ dialect: input.dialect,
65
+ ...input.version && { version: input.version },
66
+ ...input.database && { database: input.database },
67
+ ...input.details && { details: input.details }
107
68
  }
108
- if (constraint2.columns && !constraint2.columns.includes(columnName)) {
109
- return void 0;
69
+ };
70
+ }
71
+ function table(input) {
72
+ return {
73
+ name: "table",
74
+ data: {
75
+ name: input.name,
76
+ ...input.schema && { schema: input.schema },
77
+ ...input.rowCount != null && { rowCount: input.rowCount },
78
+ ...input.sizeHint && { sizeHint: input.sizeHint },
79
+ columns: input.columns,
80
+ ...input.indexes?.length && { indexes: input.indexes },
81
+ ...input.constraints?.length && { constraints: input.constraints }
110
82
  }
111
- const def = constraint2.definition;
112
- const escapedCol = this.escapeRegex(columnName);
113
- const colPattern = `(?:\\(?\\(?${escapedCol}\\)?(?:::(?:text|varchar|character varying))?\\)?)`;
114
- const inMatch = def.match(
115
- new RegExp(`${colPattern}\\s+IN\\s*\\(([^)]+)\\)`, "i")
116
- );
117
- if (inMatch) {
118
- return this.extractStringValues(inMatch[1]);
83
+ };
84
+ }
85
+ function column(input) {
86
+ return {
87
+ name: "column",
88
+ data: {
89
+ name: input.name,
90
+ type: input.type,
91
+ ...input.pk && { pk: true },
92
+ ...input.fk && { fk: input.fk },
93
+ ...input.unique && { unique: true },
94
+ ...input.notNull && { notNull: true },
95
+ ...input.default && { default: input.default },
96
+ ...input.indexed && { indexed: true },
97
+ ...input.values?.length && { values: input.values },
98
+ ...input.stats && { stats: input.stats }
119
99
  }
120
- const anyMatch = def.match(
121
- new RegExp(
122
- `${colPattern}\\s*=\\s*ANY\\s*\\(\\s*(?:ARRAY)?\\s*\\[([^\\]]+)\\]`,
123
- "i"
124
- )
125
- );
126
- if (anyMatch) {
127
- return this.extractStringValues(anyMatch[1]);
100
+ };
101
+ }
102
+ function index(input) {
103
+ return {
104
+ name: "index",
105
+ data: {
106
+ name: input.name,
107
+ columns: input.columns,
108
+ ...input.unique && { unique: true },
109
+ ...input.type && { type: input.type }
128
110
  }
129
- const orPattern = new RegExp(
130
- `\\b${this.escapeRegex(columnName)}\\b\\s*=\\s*'([^']*)'`,
131
- "gi"
132
- );
133
- const orMatches = [...def.matchAll(orPattern)];
134
- if (orMatches.length >= 2) {
135
- return orMatches.map((m) => m[1]);
111
+ };
112
+ }
113
+ function constraint(input) {
114
+ return {
115
+ name: "constraint",
116
+ data: {
117
+ name: input.name,
118
+ type: input.type,
119
+ ...input.columns?.length && { columns: input.columns },
120
+ ...input.definition && { definition: input.definition },
121
+ ...input.defaultValue && { defaultValue: input.defaultValue },
122
+ ...input.referencedTable && { referencedTable: input.referencedTable },
123
+ ...input.referencedColumns?.length && {
124
+ referencedColumns: input.referencedColumns
125
+ }
136
126
  }
137
- return void 0;
138
- }
139
- /**
140
- * Extract string values from a comma-separated list.
141
- */
142
- extractStringValues(input) {
143
- const values = [];
144
- const matches = input.matchAll(/'([^']*)'/g);
145
- for (const match of matches) {
146
- values.push(match[1]);
127
+ };
128
+ }
129
+ function view(input) {
130
+ return {
131
+ name: "view",
132
+ data: {
133
+ name: input.name,
134
+ ...input.schema && { schema: input.schema },
135
+ columns: input.columns,
136
+ ...input.definition && { definition: input.definition }
147
137
  }
148
- return values.length > 0 ? values : void 0;
149
- }
150
- /**
151
- * Escape special regex characters in a string.
152
- */
153
- escapeRegex(str) {
154
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
155
- }
138
+ };
139
+ }
140
+ function relationship(input) {
141
+ return {
142
+ name: "relationship",
143
+ data: {
144
+ from: input.from,
145
+ to: input.to,
146
+ ...input.cardinality && { cardinality: input.cardinality }
147
+ }
148
+ };
149
+ }
150
+
151
+ // packages/text2sql/src/lib/adapters/groundings/context.ts
152
+ var noopProgress = () => {
153
+ };
154
+ function timestampProgress(onProgress) {
155
+ if (!onProgress) return noopProgress;
156
+ return (progress) => {
157
+ onProgress({
158
+ ...progress,
159
+ timestampMs: progress.timestampMs ?? Date.now()
160
+ });
161
+ };
162
+ }
163
+ function createGroundingContext(options = {}) {
164
+ return {
165
+ tables: [],
166
+ views: [],
167
+ relationships: [],
168
+ info: void 0,
169
+ fragments: [],
170
+ cache: /* @__PURE__ */ new Map(),
171
+ onProgress: timestampProgress(options.onProgress)
172
+ };
173
+ }
174
+
175
+ // packages/text2sql/src/lib/adapters/adapter.ts
176
+ function groundingFor(adapterType, create) {
177
+ return (adapter) => {
178
+ if (!(adapter instanceof adapterType)) {
179
+ throw new Error(
180
+ `A ${adapterType.name} grounding needs a ${adapterType.name} adapter, not ${adapter.constructor.name}.`
181
+ );
182
+ }
183
+ return create(adapter);
184
+ };
185
+ }
186
+ var Adapter = class {
187
+ #policyAnalyzer;
156
188
  /**
157
- * Get the table from context by name.
189
+ * Injects the strategy that guards validate() and execute().
158
190
  */
159
- getTable(ctx, name) {
160
- return ctx.tables.find((t) => t.name === name);
191
+ constructor(policyAnalyzer) {
192
+ this.#policyAnalyzer = policyAnalyzer;
161
193
  }
162
194
  /**
163
- * Execute the grounding process.
164
- * Annotates columns in ctx.tables and ctx.views with values.
195
+ * Introspect the database schema and return context fragments.
196
+ *
197
+ * Executes all configured groundings to populate the context, then
198
+ * generates fragments from the complete context data.
199
+ *
200
+ * @param ctx - Optional grounding context for sharing state between groundings
201
+ * @returns Array of context fragments representing the database schema
165
202
  */
166
- async execute(ctx) {
167
- const allContainers = [...ctx.tables, ...ctx.views];
168
- const total = allContainers.length;
169
- for (let i = 0; i < allContainers.length; i++) {
170
- const container = allContainers[i];
171
- ctx.onProgress({
172
- type: "phase:progress",
173
- phase: "column_values",
174
- table: container.name,
175
- message: `Collecting column values for ${container.name}...`,
176
- current: i + 1,
177
- total
178
- });
179
- const table2 = this.getTable(ctx, container.name);
180
- for (const column2 of container.columns) {
181
- try {
182
- const result = await this.resolveColumnValues(
183
- container.name,
184
- column2,
185
- table2?.constraints
186
- );
187
- if (result) {
188
- column2.kind = result.kind;
189
- column2.values = result.values;
190
- }
191
- } catch (error) {
192
- console.warn(
193
- "Error collecting column values for",
194
- container.name,
195
- column2.name,
196
- error
197
- );
203
+ async introspect(ctx = createGroundingContext()) {
204
+ for (const fn of this.grounding) {
205
+ const grounding = fn(this);
206
+ if (grounding.phase) {
207
+ ctx.onProgress({
208
+ type: "phase:start",
209
+ phase: grounding.phase,
210
+ message: `Starting ${grounding.name} introspection...`
211
+ });
212
+ }
213
+ try {
214
+ await grounding.execute(ctx);
215
+ } finally {
216
+ if (grounding.phase) {
217
+ ctx.onProgress({
218
+ type: "phase:end",
219
+ phase: grounding.phase,
220
+ message: `Finished ${grounding.name} introspection.`
221
+ });
198
222
  }
199
223
  }
200
224
  }
201
- }
202
- exceedsMaxValueLength(values) {
203
- return values.some((v) => v.length > this.maxValueLength);
225
+ return this.#toSchemaFragments(ctx);
204
226
  }
205
227
  /**
206
- * Resolve column values from all sources in priority order.
228
+ * Resolve the allowed entity names (tables + views) from grounding config.
229
+ * Asks each grounding to contribute its entities; annotators contribute
230
+ * nothing, so their metadata scans are skipped.
231
+ * Results are NOT cached — call once and store the result.
207
232
  */
208
- async resolveColumnValues(tableName, column2, constraints2) {
209
- const enumValues = await this.collectEnumValues(tableName, column2);
210
- if (enumValues?.length && !this.exceedsMaxValueLength(enumValues)) {
211
- return { kind: "Enum", values: enumValues };
212
- }
213
- if (constraints2) {
214
- for (const constraint2 of constraints2) {
215
- const checkValues = this.parseCheckConstraint(constraint2, column2.name);
216
- if (checkValues?.length && !this.exceedsMaxValueLength(checkValues)) {
217
- return { kind: "Enum", values: checkValues };
218
- }
219
- }
220
- }
221
- const lowCardValues = await this.collectLowCardinality(tableName, column2);
222
- if (lowCardValues?.length && !this.exceedsMaxValueLength(lowCardValues)) {
223
- return { kind: "LowCardinality", values: lowCardValues };
233
+ async resolveAllowedEntities() {
234
+ const ctx = createGroundingContext();
235
+ for (const fn of this.grounding) {
236
+ await fn(this).contributeEntities(ctx);
224
237
  }
225
- return void 0;
226
- }
227
- };
228
-
229
- // packages/text2sql/src/lib/adapters/groundings/constraint.grounding.ts
230
- var ConstraintGrounding = class extends AbstractGrounding {
231
- constructor(config = {}) {
232
- super("constraint", "constraints");
238
+ return [...ctx.tables.map((t) => t.name), ...ctx.views.map((v) => v.name)];
233
239
  }
234
240
  /**
235
- * Execute the grounding process.
236
- * Annotates tables in ctx.tables with their constraints.
241
+ * Convert complete grounding context to schema fragments.
242
+ * Called after all groundings have populated ctx with data.
237
243
  */
238
- async execute(ctx) {
239
- const total = ctx.tables.length;
240
- for (let i = 0; i < ctx.tables.length; i++) {
241
- const table2 = ctx.tables[i];
242
- ctx.onProgress({
243
- type: "phase:progress",
244
- phase: "constraints",
245
- table: table2.name,
246
- message: `Loading constraints for ${table2.name}...`,
247
- current: i + 1,
248
- total
249
- });
250
- try {
251
- table2.constraints = await this.getConstraints(table2.name);
252
- } catch (error) {
253
- console.warn("Error collecting constraints for", table2.name, error);
244
+ #toSchemaFragments(ctx) {
245
+ const fragments = [];
246
+ if (ctx.info) {
247
+ fragments.push(
248
+ dialectInfo({
249
+ dialect: ctx.info.dialect,
250
+ version: ctx.info.version,
251
+ database: ctx.info.database,
252
+ details: ctx.info.details
253
+ })
254
+ );
255
+ }
256
+ for (const t of ctx.tables) {
257
+ fragments.push(this.#tableToFragment(t));
258
+ }
259
+ for (const v of ctx.views) {
260
+ fragments.push(this.#viewToFragment(v));
261
+ }
262
+ const tableMap = new Map(ctx.tables.map((t) => [t.name, t]));
263
+ const tableColumnSets = new Map(
264
+ ctx.tables.map((t) => [t.name, new Set(t.columns.map((c) => c.name))])
265
+ );
266
+ for (const rel of ctx.relationships) {
267
+ const sourceColumns = tableColumnSets.get(rel.table);
268
+ const targetColumns = tableColumnSets.get(rel.referenced_table);
269
+ if (sourceColumns && rel.from.some((column2) => !sourceColumns.has(column2))) {
270
+ continue;
271
+ }
272
+ if (targetColumns && rel.to.some((column2) => !targetColumns.has(column2))) {
273
+ continue;
254
274
  }
275
+ const sourceTable = tableMap.get(rel.table);
276
+ const targetTable = tableMap.get(rel.referenced_table);
277
+ fragments.push(
278
+ this.#relationshipToFragment(rel, sourceTable, targetTable)
279
+ );
255
280
  }
256
- }
257
- };
258
-
259
- // packages/text2sql/src/lib/adapters/groundings/indexes.grounding.ts
260
- var IndexesGrounding = class extends AbstractGrounding {
261
- constructor(config = {}) {
262
- super("index", "indexes");
281
+ fragments.push(...ctx.fragments ?? []);
282
+ return fragments;
263
283
  }
264
284
  /**
265
- * Execute the grounding process.
266
- * Annotates tables in ctx.tables with their indexes and marks indexed columns.
285
+ * Convert a Table to a table fragment with nested column, index, and constraint fragments.
267
286
  */
268
- async execute(ctx) {
269
- const total = ctx.tables.length;
270
- for (let i = 0; i < ctx.tables.length; i++) {
271
- const table2 = ctx.tables[i];
272
- ctx.onProgress({
273
- type: "phase:progress",
274
- phase: "indexes",
275
- table: table2.name,
276
- message: `Loading indexes for ${table2.name}...`,
277
- current: i + 1,
278
- total
279
- });
280
- table2.indexes = await this.getIndexes(table2.name);
281
- for (const index2 of table2.indexes ?? []) {
282
- for (const colName of index2.columns) {
283
- const column2 = table2.columns.find((c) => c.name === colName);
284
- if (column2) {
285
- column2.isIndexed = true;
286
- }
287
+ #tableToFragment(t) {
288
+ const pkConstraint = t.constraints?.find((c) => c.type === "PRIMARY_KEY");
289
+ const pkColumns = new Set(pkConstraint?.columns ?? []);
290
+ const notNullColumns = new Set(
291
+ t.constraints?.filter((c) => c.type === "NOT_NULL").flatMap((c) => c.columns ?? []) ?? []
292
+ );
293
+ const defaultByColumn = /* @__PURE__ */ new Map();
294
+ for (const c of t.constraints?.filter((c2) => c2.type === "DEFAULT") ?? []) {
295
+ for (const col of c.columns ?? []) {
296
+ if (c.defaultValue != null) {
297
+ defaultByColumn.set(col, c.defaultValue);
287
298
  }
288
299
  }
289
300
  }
290
- }
291
- };
292
-
293
- // packages/text2sql/src/lib/adapters/groundings/info.grounding.ts
294
- var InfoGrounding = class extends AbstractGrounding {
295
- constructor(config = {}) {
296
- super("dialectInfo", "info");
301
+ const uniqueColumns = new Set(
302
+ t.constraints?.filter((c) => c.type === "UNIQUE" && c.columns?.length === 1).flatMap((c) => c.columns ?? []) ?? []
303
+ );
304
+ const fkByColumn = /* @__PURE__ */ new Map();
305
+ for (const c of t.constraints?.filter((c2) => c2.type === "FOREIGN_KEY") ?? []) {
306
+ const cols = c.columns ?? [];
307
+ const refCols = c.referencedColumns ?? [];
308
+ for (let i = 0; i < cols.length; i++) {
309
+ const refCol = refCols[i] ?? refCols[0] ?? cols[i];
310
+ fkByColumn.set(cols[i], `${c.referencedTable}.${refCol}`);
311
+ }
312
+ }
313
+ const columnFragments = t.columns.map(
314
+ (col) => column({
315
+ name: col.name,
316
+ type: col.type,
317
+ pk: pkColumns.has(col.name) || void 0,
318
+ fk: fkByColumn.get(col.name),
319
+ unique: uniqueColumns.has(col.name) || void 0,
320
+ notNull: notNullColumns.has(col.name) || void 0,
321
+ default: defaultByColumn.get(col.name),
322
+ indexed: col.isIndexed || void 0,
323
+ values: col.values,
324
+ stats: col.stats
325
+ })
326
+ );
327
+ const presentColumns = new Set(t.columns.map((c) => c.name));
328
+ const indexFragments = (t.indexes ?? []).filter(
329
+ (idx) => idx.columns.every((column2) => presentColumns.has(column2))
330
+ ).map(
331
+ (idx) => index({
332
+ name: idx.name,
333
+ columns: idx.columns,
334
+ unique: idx.unique,
335
+ type: idx.type
336
+ })
337
+ );
338
+ const constraintFragments = (t.constraints ?? []).filter(
339
+ (c) => c.type === "CHECK" || c.type === "UNIQUE" && (c.columns?.length ?? 0) > 1
340
+ ).filter(
341
+ (c) => !c.columns?.length || c.columns.every((column2) => presentColumns.has(column2))
342
+ ).map(
343
+ (c) => constraint({
344
+ name: c.name,
345
+ type: c.type,
346
+ columns: c.columns,
347
+ definition: c.definition
348
+ })
349
+ );
350
+ return table({
351
+ name: t.name,
352
+ schema: t.schema,
353
+ rowCount: t.rowCount,
354
+ sizeHint: t.sizeHint,
355
+ columns: columnFragments,
356
+ indexes: indexFragments.length > 0 ? indexFragments : void 0,
357
+ constraints: constraintFragments.length > 0 ? constraintFragments : void 0
358
+ });
297
359
  }
298
360
  /**
299
- * Execute the grounding process.
300
- * Writes database info to ctx.info.
361
+ * Convert a View to a view fragment with nested column fragments.
301
362
  */
302
- async execute(ctx) {
303
- ctx.info = await this.collectInfo();
304
- }
305
- };
306
-
307
- // packages/text2sql/src/lib/adapters/groundings/row-count.grounding.ts
308
- var RowCountGrounding = class extends AbstractGrounding {
309
- constructor(config = {}) {
310
- super("rowCount", "row_counts");
363
+ #viewToFragment(v) {
364
+ const columnFragments = v.columns.map(
365
+ (col) => column({
366
+ name: col.name,
367
+ type: col.type,
368
+ values: col.values,
369
+ stats: col.stats
370
+ })
371
+ );
372
+ return view({
373
+ name: v.name,
374
+ schema: v.schema,
375
+ columns: columnFragments,
376
+ definition: v.definition
377
+ });
311
378
  }
312
379
  /**
313
- * Execute the grounding process.
314
- * Annotates tables in ctx.tables with row counts and size hints.
380
+ * Convert a Relationship to a relationship fragment.
381
+ * Infers cardinality from row counts if available.
315
382
  */
316
- async execute(ctx) {
317
- const total = ctx.tables.length;
318
- for (let i = 0; i < ctx.tables.length; i++) {
319
- const table2 = ctx.tables[i];
320
- ctx.onProgress({
321
- type: "phase:progress",
322
- phase: "row_counts",
323
- table: table2.name,
324
- message: `Counting rows in ${table2.name}...`,
325
- current: i + 1,
326
- total
327
- });
328
- const count = await this.getRowCount(table2.name);
329
- if (count != null) {
330
- table2.rowCount = count;
331
- table2.sizeHint = this.#classifyRowCount(count);
383
+ #relationshipToFragment(rel, sourceTable, targetTable) {
384
+ const sourceCount = sourceTable?.rowCount;
385
+ const targetCount = targetTable?.rowCount;
386
+ let cardinality;
387
+ if (sourceCount != null && targetCount != null && targetCount > 0) {
388
+ const ratio = sourceCount / targetCount;
389
+ if (ratio > 5) {
390
+ cardinality = "many-to-one";
391
+ } else if (ratio < 1.2 && ratio > 0.8) {
392
+ cardinality = "one-to-one";
393
+ } else if (ratio < 0.2) {
394
+ cardinality = "one-to-many";
332
395
  }
333
396
  }
397
+ return relationship({
398
+ from: { table: rel.table, columns: rel.from },
399
+ to: { table: rel.referenced_table, columns: rel.to },
400
+ cardinality
401
+ });
334
402
  }
335
- /**
336
- * Classify row count into a size hint category.
337
- */
338
- #classifyRowCount(count) {
339
- if (count < 100) return "tiny";
340
- if (count < 1e3) return "small";
341
- if (count < 1e4) return "medium";
342
- if (count < 1e5) return "large";
343
- return "huge";
403
+ format(sql) {
404
+ const decoded = this.#decodeShellEscapes(sql);
405
+ try {
406
+ return formatSql(decoded, { language: this.formatterLanguage });
407
+ } catch {
408
+ return decoded;
409
+ }
344
410
  }
345
- };
346
-
347
- // packages/text2sql/src/lib/adapters/groundings/table.grounding.ts
348
- var TableGrounding = class extends AbstractGrounding {
349
- #filter;
350
- #columns;
351
- #forward;
352
- #backward;
353
- constructor(config = {}) {
354
- super("table", "tables");
355
- this.#filter = config.filter;
356
- this.#columns = config.columns;
357
- this.#forward = config.forward;
358
- this.#backward = config.backward;
411
+ #decodeShellEscapes(sql) {
412
+ return sql.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\([().*$])/g, "$1").replace(/\\(?=$)/gm, "");
359
413
  }
360
- async contributeEntities(ctx) {
361
- await this.execute(ctx);
414
+ #cachedAllowedEntities = null;
415
+ async #resolveScope() {
416
+ if (this.#cachedAllowedEntities) return this.#cachedAllowedEntities;
417
+ this.#cachedAllowedEntities = await this.resolveAllowedEntities();
418
+ return this.#cachedAllowedEntities;
419
+ }
420
+ async #analyzePolicy(sql) {
421
+ return this.#policyAnalyzer.analyze(sql, {
422
+ resolveAllowedEntities: () => this.#resolveScope()
423
+ });
424
+ }
425
+ async validate(sql) {
426
+ const decoded = this.#decodeShellEscapes(sql);
427
+ const violation = await this.#analyzePolicy(decoded);
428
+ if (violation?.kind === "read-only") return violation.message;
429
+ if (violation?.kind === "scope") return JSON.stringify(violation.payload);
430
+ return this.validateImpl(decoded);
431
+ }
432
+ async execute(sql) {
433
+ return this.executeImpl(await this.enforceExecutionPolicy(sql));
434
+ }
435
+ async enforceExecutionPolicy(sql) {
436
+ const decoded = this.#decodeShellEscapes(sql);
437
+ const violation = await this.#analyzePolicy(decoded);
438
+ if (violation?.kind === "read-only") {
439
+ throw new SQLReadOnlyError(violation.message);
440
+ }
441
+ if (violation?.kind === "scope") {
442
+ throw new SQLScopeError(violation.payload);
443
+ }
444
+ return decoded;
362
445
  }
363
446
  /**
364
- * Execute the grounding process.
365
- * Writes discovered tables and relationships to the context.
447
+ * Run an introspection query and check every row against `row`, the shape
448
+ * the driver returns for that query. A row of any other shape throws.
366
449
  */
367
- async execute(ctx) {
368
- const seedTables = await this.applyFilter();
369
- const forward = this.#forward;
370
- const backward = this.#backward;
371
- if (!forward && !backward) {
372
- const tables3 = await Promise.all(
373
- seedTables.map(async (name, index2) => {
374
- ctx.onProgress({
375
- type: "phase:progress",
376
- phase: "tables",
377
- table: name,
378
- message: `Loading table ${name}...`,
379
- current: index2 + 1,
380
- total: seedTables.length
381
- });
382
- return applyColumnFilter(await this.getTable(name), this.#columns);
383
- })
450
+ async runQuery(sql, row) {
451
+ const rows = z.array(row).safeParse(await this.queryRows(sql));
452
+ if (!rows.success) {
453
+ throw new Error(
454
+ `${this.constructor.name} introspection query returned rows of an unexpected shape:
455
+ ${z.prettifyError(rows.error)}
456
+ SQL: ${sql.trim()}`,
457
+ { cause: rows.error }
384
458
  );
385
- ctx.tables.push(...tables3);
386
- return;
387
- }
388
- const tables2 = {};
389
- const allRelationships = [];
390
- const seenRelationships = /* @__PURE__ */ new Set();
391
- const forwardQueue = [];
392
- const backwardQueue = [];
393
- const forwardVisited = /* @__PURE__ */ new Set();
394
- const backwardVisited = /* @__PURE__ */ new Set();
395
- let loadedCount = 0;
396
- for (const name of seedTables) {
397
- if (forward) forwardQueue.push({ name, depth: 0 });
398
- if (backward) backwardQueue.push({ name, depth: 0 });
399
459
  }
400
- const forwardLimit = forward === true ? Infinity : forward || 0;
401
- while (forwardQueue.length > 0) {
402
- const item = forwardQueue.shift();
403
- if (!item) break;
404
- const { name, depth } = item;
405
- if (forwardVisited.has(name)) continue;
406
- forwardVisited.add(name);
407
- if (!tables2[name]) {
408
- loadedCount++;
409
- ctx.onProgress({
410
- type: "phase:progress",
411
- phase: "tables",
412
- table: name,
413
- message: `Loading table ${name}...`,
414
- current: loadedCount
415
- });
416
- tables2[name] = applyColumnFilter(
417
- await this.getTable(name),
418
- this.#columns
419
- );
420
- }
421
- if (depth < forwardLimit) {
422
- const rels = await this.findOutgoingRelations(name);
423
- for (const rel of rels) {
424
- if (!this.isRelationshipVisible(rel)) continue;
425
- this.addRelationship(rel, allRelationships, seenRelationships);
426
- if (!forwardVisited.has(rel.referenced_table)) {
427
- forwardQueue.push({ name: rel.referenced_table, depth: depth + 1 });
428
- }
429
- }
430
- }
460
+ return rows.data;
461
+ }
462
+ /**
463
+ * The rows in a driver's execute() result: the result itself when it is an
464
+ * array, otherwise the first array found at one of `paths` (dot-separated,
465
+ * so `recordsets.0` reads `result.recordsets[0]`).
466
+ */
467
+ rowsFrom(result, paths) {
468
+ if (Array.isArray(result)) return result;
469
+ for (const path of paths) {
470
+ const rows = path.split(".").reduce(
471
+ (value, key) => typeof value === "object" && value !== null ? Reflect.get(value, key) : void 0,
472
+ result
473
+ );
474
+ if (Array.isArray(rows)) return rows;
431
475
  }
432
- const backwardLimit = backward === true ? Infinity : backward || 0;
433
- while (backwardQueue.length > 0) {
434
- const item = backwardQueue.shift();
435
- if (!item) break;
436
- const { name, depth } = item;
437
- if (backwardVisited.has(name)) continue;
438
- backwardVisited.add(name);
439
- if (!tables2[name]) {
440
- loadedCount++;
441
- ctx.onProgress({
442
- type: "phase:progress",
443
- phase: "tables",
444
- table: name,
445
- message: `Loading table ${name}...`,
446
- current: loadedCount
447
- });
448
- tables2[name] = applyColumnFilter(
449
- await this.getTable(name),
450
- this.#columns
451
- );
452
- }
453
- if (depth < backwardLimit) {
454
- const rels = await this.findIncomingRelations(name);
455
- for (const rel of rels) {
456
- if (!this.isRelationshipVisible(rel)) continue;
457
- this.addRelationship(rel, allRelationships, seenRelationships);
458
- if (!backwardVisited.has(rel.table)) {
459
- backwardQueue.push({ name: rel.table, depth: depth + 1 });
460
- }
461
- }
462
- }
463
- }
464
- const tablesList = Object.values(tables2);
465
- ctx.tables.push(...tablesList);
466
- ctx.relationships.push(...allRelationships);
476
+ throw new Error(
477
+ `${this.constructor.name} execute() must return an array of rows or an object holding one at ${paths.map((path) => `result.${path}`).join(", ")}.`
478
+ );
467
479
  }
468
480
  /**
469
- * Apply the filter to get seed table names.
470
- * If filter is an explicit array, skip querying all table names.
481
+ * Convert unknown database value to number.
482
+ * Handles number, bigint, and string types.
471
483
  */
472
- async applyFilter() {
473
- const filter = this.#filter;
474
- if (Array.isArray(filter)) {
475
- return filter;
484
+ toNumber(value) {
485
+ if (typeof value === "number" && Number.isFinite(value)) {
486
+ return value;
476
487
  }
477
- const names = await this.getAllTableNames();
478
- if (!filter) {
479
- return names;
488
+ if (typeof value === "bigint") {
489
+ return Number(value);
480
490
  }
481
- if (filter instanceof RegExp) {
482
- return names.filter((name) => filter.test(name));
491
+ if (typeof value === "string" && value.trim() !== "") {
492
+ const parsed = Number(value);
493
+ return Number.isFinite(parsed) ? parsed : void 0;
483
494
  }
484
- return names.filter(filter);
495
+ return void 0;
485
496
  }
486
497
  /**
487
- * Add a relationship to the collection, deduplicating by key.
498
+ * Parse a potentially qualified table name into schema and table parts.
488
499
  */
489
- addRelationship(rel, all, seen) {
490
- const key = `${rel.table}:${rel.from.join(",")}:${rel.referenced_table}:${rel.to.join(",")}`;
491
- if (!seen.has(key)) {
492
- seen.add(key);
493
- all.push(rel);
500
+ parseTableName(name) {
501
+ if (name.includes(".")) {
502
+ const [schema, ...rest] = name.split(".");
503
+ return { schema, table: rest.join(".") };
494
504
  }
505
+ return { schema: this.defaultSchema ?? "", table: name };
495
506
  }
496
- isRelationshipVisible(rel) {
497
- return this.areColumnsVisible(rel.from, this.#columns?.[rel.table]) && this.areColumnsVisible(rel.to, this.#columns?.[rel.referenced_table]);
507
+ /**
508
+ * Escape a string value for use in SQL string literals (single quotes).
509
+ * Used in WHERE clauses like: WHERE name = '${escapeString(value)}'
510
+ */
511
+ escapeString(value) {
512
+ return value.replace(/'/g, "''");
498
513
  }
499
- areColumnsVisible(names, filter) {
500
- if (!filter) return true;
501
- return filterColumns(
502
- names.map((name) => ({ name })),
503
- filter
504
- ).length === names.length;
514
+ /**
515
+ * Build a SQL filter clause to include/exclude schemas.
516
+ * @param columnName - The schema column name (e.g., 'TABLE_SCHEMA')
517
+ * @param allowedSchemas - If provided, filter to these schemas only
518
+ */
519
+ buildSchemaFilter(columnName, allowedSchemas) {
520
+ if (allowedSchemas && allowedSchemas.length > 0) {
521
+ const values = allowedSchemas.map((s) => `'${this.escapeString(s)}'`).join(", ");
522
+ return `AND ${columnName} IN (${values})`;
523
+ }
524
+ if (this.systemSchemas.length > 0) {
525
+ const values = this.systemSchemas.map((s) => `'${this.escapeString(s)}'`).join(", ");
526
+ return `AND ${columnName} NOT IN (${values})`;
527
+ }
528
+ return "";
505
529
  }
506
530
  };
507
531
 
508
- // packages/text2sql/src/lib/adapters/groundings/view.grounding.ts
509
- var ViewGrounding = class extends AbstractGrounding {
510
- #filter;
511
- #columns;
512
- includeDefinition;
513
- constructor(config = {}) {
514
- super("view", "views");
515
- this.#filter = config.filter;
516
- this.#columns = config.columns;
517
- this.includeDefinition = config.includeDefinition ?? true;
532
+ // packages/text2sql/src/lib/adapters/duckdb/duckdb.groundings.ts
533
+ import { z as z3 } from "zod";
534
+
535
+ // packages/text2sql/src/lib/adapters/groundings/abstract.grounding.ts
536
+ function filterColumns(columns, filter) {
537
+ if (Array.isArray(filter)) {
538
+ return columns.filter((col) => filter.includes(col.name));
518
539
  }
519
- async contributeEntities(ctx) {
520
- await this.execute(ctx);
540
+ if (filter instanceof RegExp) {
541
+ return columns.filter((col) => filter.test(col.name));
521
542
  }
543
+ return columns.filter((col) => filter(col.name));
544
+ }
545
+ function applyColumnFilter(entity, columnsConfig) {
546
+ if (!columnsConfig) return entity;
547
+ const filter = columnsConfig[entity.name];
548
+ if (!filter) return entity;
549
+ return { ...entity, columns: filterColumns(entity.columns, filter) };
550
+ }
551
+ var AbstractGrounding = class {
522
552
  /**
523
- * Execute the grounding process.
524
- * Writes discovered views to the context.
553
+ * Grounding identifier for debugging/logging.
525
554
  */
526
- async execute(ctx) {
527
- const viewNames = await this.applyFilter();
528
- const views2 = await Promise.all(
529
- viewNames.map(async (name, index2) => {
530
- ctx.onProgress({
531
- type: "phase:progress",
532
- phase: "views",
533
- table: name,
534
- message: `Loading view ${name}...`,
535
- current: index2 + 1,
536
- total: viewNames.length
537
- });
538
- return applyColumnFilter(await this.getView(name), this.#columns);
539
- })
540
- );
541
- ctx.views.push(...views2);
555
+ name;
556
+ phase;
557
+ constructor(name, phase) {
558
+ this.name = name;
559
+ this.phase = phase;
542
560
  }
543
561
  /**
544
- * Apply the filter to get view names.
545
- * If filter is an explicit array, skip querying all view names.
562
+ * Populate ctx with the tables/views this grounding makes available for scope
563
+ * resolution. Annotators decorate entities other groundings produced and add
564
+ * no membership, so the default is a no-op; entity-producing groundings
565
+ * override it. This lets scope resolution skip the annotators' metadata scans.
566
+ *
567
+ * @param _ctx - Shared context for accumulating entity membership
546
568
  */
547
- async applyFilter() {
548
- const filter = this.#filter;
549
- if (Array.isArray(filter)) {
550
- return filter;
551
- }
552
- const names = await this.getAllViewNames();
553
- if (!filter) {
554
- return names;
555
- }
556
- if (filter instanceof RegExp) {
557
- return names.filter((name) => filter.test(name));
558
- }
559
- return names.filter(filter);
569
+ async contributeEntities(_ctx) {
560
570
  }
561
571
  };
562
572
 
563
- // packages/text2sql/src/lib/adapters/duckdb/duckdb-identifiers.ts
564
- import { quotedIdentifier } from "@duckdb/node-api";
565
- function formatDuckDBIdentifierPath(parts) {
566
- return parts.map(quotedIdentifier).join(".");
567
- }
568
- function parseDuckDBRelationName(name) {
569
- const parts = splitIdentifierPath(name);
570
- if (parts.length === 1) return { table: parts[0] };
571
- if (parts.length === 2) {
572
- return { schema: parts[0], table: parts[1] };
573
- }
574
- if (parts.length === 3) {
575
- return { catalog: parts[0], schema: parts[1], table: parts[2] };
573
+ // packages/text2sql/src/lib/adapters/groundings/column-stats.grounding.ts
574
+ var ColumnStatsGrounding = class extends AbstractGrounding {
575
+ constructor(config = {}) {
576
+ super("columnStats", "column_stats");
576
577
  }
577
- throw new Error(`DuckDB relation must have at most three parts: ${name}`);
578
- }
579
- function splitIdentifierPath(input) {
580
- const parts = [];
581
- let part = "";
582
- let quoted = false;
583
- for (let index2 = 0; index2 < input.length; index2++) {
584
- const character = input[index2];
585
- if (character === '"') {
586
- if (quoted && input[index2 + 1] === '"') {
587
- part += '"';
588
- index2++;
589
- } else {
590
- quoted = !quoted;
578
+ /**
579
+ * Execute the grounding process.
580
+ * Annotates columns in ctx.tables and ctx.views with statistics.
581
+ */
582
+ async execute(ctx) {
583
+ const allContainers = [...ctx.tables, ...ctx.views];
584
+ const total = allContainers.length;
585
+ for (let i = 0; i < allContainers.length; i++) {
586
+ const container = allContainers[i];
587
+ ctx.onProgress({
588
+ type: "phase:progress",
589
+ phase: "column_stats",
590
+ table: container.name,
591
+ message: `Collecting stats for ${container.name}...`,
592
+ current: i + 1,
593
+ total
594
+ });
595
+ for (const column2 of container.columns) {
596
+ try {
597
+ const stats = await this.collectStats(container.name, column2);
598
+ if (stats) {
599
+ column2.stats = stats;
600
+ }
601
+ } catch (error) {
602
+ console.warn(
603
+ "Error collecting stats for",
604
+ container.name,
605
+ column2.name,
606
+ error
607
+ );
608
+ }
591
609
  }
592
- } else if (character === "." && !quoted) {
593
- if (!part) throw new Error(`Invalid DuckDB relation: ${input}`);
594
- parts.push(part);
595
- part = "";
596
- } else {
597
- part += character;
598
610
  }
599
611
  }
600
- if (quoted || !part) throw new Error(`Invalid DuckDB relation: ${input}`);
601
- parts.push(part);
602
- return parts;
603
- }
612
+ };
604
613
 
605
- // packages/text2sql/src/lib/adapters/duckdb/duckdb.groundings.ts
606
- var CONSTRAINT_TYPES = /* @__PURE__ */ new Map([
607
- ["CHECK", "CHECK"],
608
- ["UNIQUE", "UNIQUE"],
609
- ["NOT NULL", "NOT_NULL"],
610
- ["PRIMARY KEY", "PRIMARY_KEY"],
611
- ["FOREIGN KEY", "FOREIGN_KEY"]
612
- ]);
613
- var DuckDBTableGrounding = class extends TableGrounding {
614
- #adapter;
615
- constructor(adapter, config = {}) {
616
- super(config);
617
- this.#adapter = adapter;
614
+ // packages/text2sql/src/lib/adapters/groundings/column-values.grounding.ts
615
+ var ColumnValuesGrounding = class extends AbstractGrounding {
616
+ lowCardinalityLimit;
617
+ maxValueLength;
618
+ constructor(config = {}) {
619
+ super("columnValues", "column_values");
620
+ this.lowCardinalityLimit = config.lowCardinalityLimit ?? 20;
621
+ this.maxValueLength = config.maxValueLength ?? 100;
618
622
  }
619
- async applyFilter() {
620
- const names = await super.applyFilter();
621
- const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
622
- const result = [];
623
- for (const name of names) {
624
- const relation = await this.#adapter.resolveRelationName(name);
625
- if (catalogs.includes(relation.catalog) && schemas.includes(relation.schema)) {
626
- result.push(
627
- this.#adapter.formatRelationName(
628
- relation.catalog,
629
- relation.schema,
630
- relation.table
631
- )
632
- );
633
- }
623
+ /**
624
+ * Get values for native ENUM type columns.
625
+ * Return undefined if column is not an ENUM type.
626
+ * Default implementation returns undefined (no native ENUM support).
627
+ */
628
+ async collectEnumValues(_tableName, _column) {
629
+ return void 0;
630
+ }
631
+ /**
632
+ * Parse CHECK constraint for enum-like IN clause.
633
+ * Extracts values from patterns like:
634
+ * - CHECK (status IN ('active', 'inactive'))
635
+ * - CHECK ((status)::text = ANY (ARRAY['a'::text, 'b'::text]))
636
+ * - CHECK (status = 'active' OR status = 'inactive')
637
+ */
638
+ parseCheckConstraint(constraint2, columnName) {
639
+ if (constraint2.type !== "CHECK" || !constraint2.definition) {
640
+ return void 0;
634
641
  }
635
- return result;
642
+ if (constraint2.columns && !constraint2.columns.includes(columnName)) {
643
+ return void 0;
644
+ }
645
+ const def = constraint2.definition;
646
+ const escapedCol = this.escapeRegex(columnName);
647
+ const colPattern = `(?:\\(?\\(?${escapedCol}\\)?(?:::(?:text|varchar|character varying))?\\)?)`;
648
+ const inMatch = def.match(
649
+ new RegExp(`${colPattern}\\s+IN\\s*\\(([^)]+)\\)`, "i")
650
+ );
651
+ if (inMatch) {
652
+ return this.extractStringValues(inMatch[1]);
653
+ }
654
+ const anyMatch = def.match(
655
+ new RegExp(
656
+ `${colPattern}\\s*=\\s*ANY\\s*\\(\\s*(?:ARRAY)?\\s*\\[([^\\]]+)\\]`,
657
+ "i"
658
+ )
659
+ );
660
+ if (anyMatch) {
661
+ return this.extractStringValues(anyMatch[1]);
662
+ }
663
+ const orPattern = new RegExp(
664
+ `\\b${this.escapeRegex(columnName)}\\b\\s*=\\s*'([^']*)'`,
665
+ "gi"
666
+ );
667
+ const orMatches = [...def.matchAll(orPattern)];
668
+ if (orMatches.length >= 2) {
669
+ return orMatches.map((m) => m[1]);
670
+ }
671
+ return void 0;
636
672
  }
637
- async getAllTableNames() {
638
- const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
639
- const rows = await this.#adapter.runQuery(`
640
- SELECT database_name, schema_name, table_name
641
- FROM duckdb_tables()
642
- WHERE NOT internal
643
- AND NOT temporary
644
- AND database_name IN (${sqlLiterals(this.#adapter, catalogs)})
645
- AND schema_name IN (${sqlLiterals(this.#adapter, schemas)})
646
- ORDER BY database_name, schema_name, table_name
647
- `);
648
- return rows.map((row) => {
649
- if (typeof row.database_name !== "string" || typeof row.schema_name !== "string" || typeof row.table_name !== "string") {
650
- throw new Error("DuckDB table catalog returned an unknown row shape.");
651
- }
652
- return this.#adapter.formatRelationName(
653
- row.database_name,
654
- row.schema_name,
655
- row.table_name
656
- );
657
- });
673
+ /**
674
+ * Extract string values from a comma-separated list.
675
+ */
676
+ extractStringValues(input) {
677
+ const values = [];
678
+ const matches = input.matchAll(/'([^']*)'/g);
679
+ for (const match of matches) {
680
+ values.push(match[1]);
681
+ }
682
+ return values.length > 0 ? values : void 0;
658
683
  }
659
- async getTable(tableName) {
660
- const { catalog, schema, table: table2 } = await this.#adapter.resolveRelationName(tableName);
661
- const rows = await this.#adapter.runQuery(`
662
- SELECT column_name, data_type
663
- FROM duckdb_columns()
664
- WHERE database_name = '${this.#adapter.escapeString(catalog)}'
665
- AND schema_name = '${this.#adapter.escapeString(schema)}'
666
- AND table_name = '${this.#adapter.escapeString(table2)}'
667
- ORDER BY column_index
668
- `);
669
- return {
670
- name: this.#adapter.formatRelationName(catalog, schema, table2),
671
- schema: formatDuckDBIdentifierPath([catalog, schema]),
672
- rawName: table2,
673
- columns: rows.map((row) => {
674
- if (typeof row.column_name !== "string" || typeof row.data_type !== "string") {
675
- throw new Error(
676
- "DuckDB column catalog returned an unknown row shape."
684
+ /**
685
+ * Escape special regex characters in a string.
686
+ */
687
+ escapeRegex(str) {
688
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
689
+ }
690
+ /**
691
+ * Get the table from context by name.
692
+ */
693
+ getTable(ctx, name) {
694
+ return ctx.tables.find((t) => t.name === name);
695
+ }
696
+ /**
697
+ * Execute the grounding process.
698
+ * Annotates columns in ctx.tables and ctx.views with values.
699
+ */
700
+ async execute(ctx) {
701
+ const allContainers = [...ctx.tables, ...ctx.views];
702
+ const total = allContainers.length;
703
+ for (let i = 0; i < allContainers.length; i++) {
704
+ const container = allContainers[i];
705
+ ctx.onProgress({
706
+ type: "phase:progress",
707
+ phase: "column_values",
708
+ table: container.name,
709
+ message: `Collecting column values for ${container.name}...`,
710
+ current: i + 1,
711
+ total
712
+ });
713
+ const table2 = this.getTable(ctx, container.name);
714
+ for (const column2 of container.columns) {
715
+ try {
716
+ const result = await this.resolveColumnValues(
717
+ container.name,
718
+ column2,
719
+ table2?.constraints
720
+ );
721
+ if (result) {
722
+ column2.kind = result.kind;
723
+ column2.values = result.values;
724
+ }
725
+ } catch (error) {
726
+ console.warn(
727
+ "Error collecting column values for",
728
+ container.name,
729
+ column2.name,
730
+ error
677
731
  );
678
732
  }
679
- return { name: row.column_name, type: row.data_type };
680
- })
681
- };
733
+ }
734
+ }
682
735
  }
683
- async findOutgoingRelations(tableName) {
684
- const relation = await this.#adapter.resolveRelationName(tableName);
685
- const rows = await foreignKeys(this.#adapter, relation, "outgoing");
686
- return rows.map((row) => relationshipFromRow(this.#adapter, relation, row));
736
+ exceedsMaxValueLength(values) {
737
+ return values.some((v) => v.length > this.maxValueLength);
687
738
  }
688
- async findIncomingRelations(tableName) {
689
- const relation = await this.#adapter.resolveRelationName(tableName);
690
- const rows = await foreignKeys(this.#adapter, relation, "incoming");
691
- return rows.map((row) => {
692
- const source = {
693
- catalog: relation.catalog,
694
- schema: relation.schema,
695
- table: requiredString(row.table_name, "table_name")
696
- };
697
- return relationshipFromRow(this.#adapter, source, row);
698
- });
739
+ /**
740
+ * Resolve column values from all sources in priority order.
741
+ */
742
+ async resolveColumnValues(tableName, column2, constraints2) {
743
+ const enumValues = await this.collectEnumValues(tableName, column2);
744
+ if (enumValues?.length && !this.exceedsMaxValueLength(enumValues)) {
745
+ return { kind: "Enum", values: enumValues };
746
+ }
747
+ if (constraints2) {
748
+ for (const constraint2 of constraints2) {
749
+ const checkValues = this.parseCheckConstraint(constraint2, column2.name);
750
+ if (checkValues?.length && !this.exceedsMaxValueLength(checkValues)) {
751
+ return { kind: "Enum", values: checkValues };
752
+ }
753
+ }
754
+ }
755
+ const lowCardValues = await this.collectLowCardinality(tableName, column2);
756
+ if (lowCardValues?.length && !this.exceedsMaxValueLength(lowCardValues)) {
757
+ return { kind: "LowCardinality", values: lowCardValues };
758
+ }
759
+ return void 0;
699
760
  }
700
761
  };
701
- var DuckDBInfoGrounding = class extends InfoGrounding {
702
- #adapter;
703
- constructor(adapter, config = {}) {
704
- super(config);
705
- this.#adapter = adapter;
762
+
763
+ // packages/text2sql/src/lib/adapters/groundings/constraint.grounding.ts
764
+ var ConstraintGrounding = class extends AbstractGrounding {
765
+ constructor(config = {}) {
766
+ super("constraint", "constraints");
706
767
  }
707
- async collectInfo() {
708
- const rows = await this.#adapter.runQuery(`
709
- SELECT
710
- version() AS version,
711
- current_database() AS catalog,
712
- current_schema() AS schema
713
- `);
714
- const row = rows[0];
715
- return {
716
- dialect: "duckdb",
717
- version: typeof row?.version === "string" ? row.version : void 0,
718
- database: typeof row?.catalog === "string" ? row.catalog : void 0,
719
- details: {
720
- currentSchema: typeof row?.schema === "string" ? row.schema : "unknown",
721
- identifierQualification: "catalog.schema.table",
722
- parameterPlaceholders: ["$1", "$name"]
768
+ /**
769
+ * Execute the grounding process.
770
+ * Annotates tables in ctx.tables with their constraints.
771
+ */
772
+ async execute(ctx) {
773
+ const total = ctx.tables.length;
774
+ for (let i = 0; i < ctx.tables.length; i++) {
775
+ const table2 = ctx.tables[i];
776
+ ctx.onProgress({
777
+ type: "phase:progress",
778
+ phase: "constraints",
779
+ table: table2.name,
780
+ message: `Loading constraints for ${table2.name}...`,
781
+ current: i + 1,
782
+ total
783
+ });
784
+ try {
785
+ table2.constraints = await this.getConstraints(table2.name);
786
+ } catch (error) {
787
+ console.warn("Error collecting constraints for", table2.name, error);
723
788
  }
724
- };
789
+ }
725
790
  }
726
791
  };
727
- var DuckDBViewGrounding = class extends ViewGrounding {
728
- #adapter;
729
- constructor(adapter, config = {}) {
730
- super(config);
731
- this.#adapter = adapter;
732
- }
733
- async applyFilter() {
734
- const names = await super.applyFilter();
735
- const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
736
- const result = [];
737
- for (const name of names) {
738
- const relation = await this.#adapter.resolveRelationName(name);
739
- if (catalogs.includes(relation.catalog) && schemas.includes(relation.schema)) {
740
- result.push(
741
- this.#adapter.formatRelationName(
742
- relation.catalog,
743
- relation.schema,
744
- relation.table
745
- )
746
- );
792
+
793
+ // packages/text2sql/src/lib/adapters/groundings/indexes.grounding.ts
794
+ var IndexesGrounding = class extends AbstractGrounding {
795
+ constructor(config = {}) {
796
+ super("index", "indexes");
797
+ }
798
+ /**
799
+ * Execute the grounding process.
800
+ * Annotates tables in ctx.tables with their indexes and marks indexed columns.
801
+ */
802
+ async execute(ctx) {
803
+ const total = ctx.tables.length;
804
+ for (let i = 0; i < ctx.tables.length; i++) {
805
+ const table2 = ctx.tables[i];
806
+ ctx.onProgress({
807
+ type: "phase:progress",
808
+ phase: "indexes",
809
+ table: table2.name,
810
+ message: `Loading indexes for ${table2.name}...`,
811
+ current: i + 1,
812
+ total
813
+ });
814
+ table2.indexes = await this.getIndexes(table2.name);
815
+ for (const index2 of table2.indexes ?? []) {
816
+ for (const colName of index2.columns) {
817
+ const column2 = table2.columns.find((c) => c.name === colName);
818
+ if (column2) {
819
+ column2.isIndexed = true;
820
+ }
821
+ }
747
822
  }
748
823
  }
749
- return result;
750
- }
751
- async getAllViewNames() {
752
- const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
753
- const rows = await this.#adapter.runQuery(`
754
- SELECT database_name, schema_name, view_name
755
- FROM duckdb_views()
756
- WHERE NOT internal
757
- AND NOT temporary
758
- AND database_name IN (${sqlLiterals(this.#adapter, catalogs)})
759
- AND schema_name IN (${sqlLiterals(this.#adapter, schemas)})
760
- ORDER BY database_name, schema_name, view_name
761
- `);
762
- return rows.map(
763
- (row) => this.#adapter.formatRelationName(
764
- requiredString(row.database_name, "database_name"),
765
- requiredString(row.schema_name, "schema_name"),
766
- requiredString(row.view_name, "view_name")
767
- )
768
- );
769
- }
770
- async getView(viewName) {
771
- const relation = await this.#adapter.resolveRelationName(viewName);
772
- const where = relationPredicate(this.#adapter, relation);
773
- const [columns, definitions] = await Promise.all([
774
- this.#adapter.runQuery(`
775
- SELECT column_name, data_type
776
- FROM duckdb_columns()
777
- WHERE ${where}
778
- ORDER BY column_index
779
- `),
780
- this.includeDefinition ? this.#adapter.runQuery(`
781
- SELECT sql
782
- FROM duckdb_views()
783
- WHERE database_name = '${this.#adapter.escapeString(relation.catalog)}'
784
- AND schema_name = '${this.#adapter.escapeString(relation.schema)}'
785
- AND view_name = '${this.#adapter.escapeString(relation.table)}'
786
- `) : Promise.resolve([])
787
- ]);
788
- return {
789
- name: this.#adapter.formatRelationName(
790
- relation.catalog,
791
- relation.schema,
792
- relation.table
793
- ),
794
- schema: formatDuckDBIdentifierPath([relation.catalog, relation.schema]),
795
- rawName: relation.table,
796
- definition: typeof definitions[0]?.sql === "string" ? definitions[0].sql : void 0,
797
- columns: columns.map(columnFromRow)
798
- };
799
824
  }
800
825
  };
801
- var DuckDBConstraintGrounding = class extends ConstraintGrounding {
802
- #adapter;
803
- constructor(adapter, config = {}) {
804
- super(config);
805
- this.#adapter = adapter;
826
+
827
+ // packages/text2sql/src/lib/adapters/groundings/info.grounding.ts
828
+ var InfoGrounding = class extends AbstractGrounding {
829
+ constructor(config = {}) {
830
+ super("dialectInfo", "info");
806
831
  }
807
- async getConstraints(tableName) {
808
- const relation = await this.#adapter.resolveRelationName(tableName);
809
- const [rows, columns] = await Promise.all([
810
- this.#adapter.runQuery(`
811
- SELECT
812
- constraint_name,
813
- constraint_type,
814
- expression,
815
- constraint_column_names,
816
- referenced_table,
817
- referenced_column_names
818
- FROM duckdb_constraints()
819
- WHERE ${relationPredicate(this.#adapter, relation)}
820
- ORDER BY constraint_index
821
- `),
822
- this.#adapter.runQuery(`
823
- SELECT column_name, column_default
824
- FROM duckdb_columns()
825
- WHERE ${relationPredicate(this.#adapter, relation)}
826
- AND column_default IS NOT NULL
827
- ORDER BY column_index
828
- `)
829
- ]);
830
- const constraints2 = rows.map(
831
- (row) => constraintFromRow(this.#adapter, relation, row)
832
- );
833
- for (const column2 of columns) {
834
- if (typeof column2.column_name === "string" && typeof column2.column_default === "string") {
835
- constraints2.push({
836
- name: `${relation.table}_${column2.column_name}_default`,
837
- type: "DEFAULT",
838
- columns: [column2.column_name],
839
- defaultValue: column2.column_default
840
- });
841
- }
842
- }
843
- return constraints2;
832
+ /**
833
+ * Execute the grounding process.
834
+ * Writes database info to ctx.info.
835
+ */
836
+ async execute(ctx) {
837
+ ctx.info = await this.collectInfo();
844
838
  }
845
839
  };
846
- var DuckDBIndexesGrounding = class extends IndexesGrounding {
847
- #adapter;
848
- constructor(adapter, config = {}) {
849
- super(config);
850
- this.#adapter = adapter;
840
+
841
+ // packages/text2sql/src/lib/adapters/groundings/row-count.grounding.ts
842
+ var RowCountGrounding = class extends AbstractGrounding {
843
+ constructor(config = {}) {
844
+ super("rowCount", "row_counts");
851
845
  }
852
- async getIndexes(tableName) {
853
- const relation = await this.#adapter.resolveRelationName(tableName);
854
- const [rows, columnRows] = await Promise.all([
855
- this.#adapter.runQuery(`
856
- SELECT
857
- index_name,
858
- TRY_CAST(expressions AS VARCHAR[]) AS expressions,
859
- is_unique,
860
- is_primary
861
- FROM duckdb_indexes()
862
- WHERE ${relationPredicate(this.#adapter, relation)}
863
- ORDER BY index_name
864
- `),
865
- this.#adapter.runQuery(`
866
- SELECT column_name
867
- FROM duckdb_columns()
868
- WHERE ${relationPredicate(this.#adapter, relation)}
869
- ORDER BY column_index
870
- `)
871
- ]);
872
- const tableColumns = columnRows.map(
873
- (row) => requiredString(row.column_name, "column_name")
874
- );
875
- return rows.flatMap((row) => {
876
- if (typeof row.index_name !== "string") {
877
- throw new Error("DuckDB index catalog returned an unknown row shape.");
878
- }
879
- if (!Array.isArray(row.expressions) || row.expressions.some((expression) => typeof expression !== "string")) {
880
- return [];
846
+ /**
847
+ * Execute the grounding process.
848
+ * Annotates tables in ctx.tables with row counts and size hints.
849
+ */
850
+ async execute(ctx) {
851
+ const total = ctx.tables.length;
852
+ for (let i = 0; i < ctx.tables.length; i++) {
853
+ const table2 = ctx.tables[i];
854
+ ctx.onProgress({
855
+ type: "phase:progress",
856
+ phase: "row_counts",
857
+ table: table2.name,
858
+ message: `Counting rows in ${table2.name}...`,
859
+ current: i + 1,
860
+ total
861
+ });
862
+ const count = await this.getRowCount(table2.name);
863
+ if (count != null) {
864
+ table2.rowCount = count;
865
+ table2.sizeHint = this.#classifyRowCount(count);
881
866
  }
882
- const columns = row.expressions.map(
883
- (expression) => tableColumns.find(
884
- (column2) => expression === column2 || expression === this.#adapter.quoteIdentifier(column2)
885
- )
886
- );
887
- return columns.length > 0 && columns.every((column2) => column2 !== void 0) ? [
888
- {
889
- name: row.index_name,
890
- columns,
891
- unique: row.is_unique === true,
892
- type: row.is_primary === true ? "PRIMARY" : void 0
893
- }
894
- ] : [];
895
- });
896
- }
897
- };
898
- var DuckDBRowCountGrounding = class extends RowCountGrounding {
899
- #adapter;
900
- constructor(adapter, config = {}) {
901
- super(config);
902
- this.#adapter = adapter;
867
+ }
903
868
  }
904
- async getRowCount(tableName) {
905
- const relation = await this.#adapter.resolveRelationName(tableName);
906
- const rows = await this.#adapter.runQuery(`
907
- SELECT estimated_size
908
- FROM duckdb_tables()
909
- WHERE ${relationPredicate(this.#adapter, relation)}
910
- `);
911
- return this.#adapter.toNumber(rows[0]?.estimated_size);
869
+ /**
870
+ * Classify row count into a size hint category.
871
+ */
872
+ #classifyRowCount(count) {
873
+ if (count < 100) return "tiny";
874
+ if (count < 1e3) return "small";
875
+ if (count < 1e4) return "medium";
876
+ if (count < 1e5) return "large";
877
+ return "huge";
912
878
  }
913
879
  };
914
- var DuckDBColumnStatsGrounding = class extends ColumnStatsGrounding {
915
- #adapter;
916
- constructor(adapter, config = {}) {
917
- super(config);
918
- this.#adapter = adapter;
880
+
881
+ // packages/text2sql/src/lib/adapters/groundings/rows.ts
882
+ import { z as z2 } from "zod";
883
+ var nameRow = z2.object({ name: z2.string() });
884
+ var versionRow = z2.object({ version: z2.string() });
885
+ var databaseRow = z2.object({ db: z2.string() });
886
+ var viewDefinitionRow = z2.object({
887
+ definition: z2.string().nullable()
888
+ });
889
+ var distinctValueRow = z2.object({ value: z2.unknown() });
890
+ var textValueRow = z2.object({ value: z2.string() });
891
+ var numericValue = z2.union([z2.number(), z2.bigint(), z2.string()]);
892
+
893
+ // packages/text2sql/src/lib/adapters/groundings/table.grounding.ts
894
+ var TableGrounding = class extends AbstractGrounding {
895
+ #filter;
896
+ #columns;
897
+ #forward;
898
+ #backward;
899
+ constructor(config = {}) {
900
+ super("table", "tables");
901
+ this.#filter = config.filter;
902
+ this.#columns = config.columns;
903
+ this.#forward = config.forward;
904
+ this.#backward = config.backward;
919
905
  }
920
- async collectStats(tableName, column2) {
921
- if (!/int|real|numeric|double|float|decimal|date|time|bool/i.test(column2.type)) {
922
- return void 0;
923
- }
924
- const relation = this.#adapter.quoteRelationName(tableName);
925
- const identifier = this.#adapter.quoteIdentifier(column2.name);
926
- const rows = await this.#adapter.runQuery(`
927
- SELECT
928
- CAST(MIN(${identifier}) AS VARCHAR) AS min_value,
929
- CAST(MAX(${identifier}) AS VARCHAR) AS max_value,
930
- CASE
931
- WHEN COUNT(*) = 0 THEN NULL
932
- ELSE COUNT(*) FILTER (WHERE ${identifier} IS NULL)::DOUBLE / COUNT(*)
933
- END AS null_fraction,
934
- COUNT(DISTINCT ${identifier}) AS n_distinct
935
- FROM ${relation}
936
- `);
937
- const row = rows[0];
938
- if (!row) return void 0;
939
- return {
940
- min: typeof row.min_value === "string" ? row.min_value : void 0,
941
- max: typeof row.max_value === "string" ? row.max_value : void 0,
942
- nullFraction: this.#adapter.toNumber(row.null_fraction),
943
- nDistinct: this.#adapter.toNumber(row.n_distinct)
944
- };
906
+ async contributeEntities(ctx) {
907
+ await this.execute(ctx);
945
908
  }
946
- };
947
- var DuckDBColumnValuesGrounding = class extends ColumnValuesGrounding {
948
- #adapter;
949
- constructor(adapter, config = {}) {
950
- super(config);
951
- this.#adapter = adapter;
952
- }
953
- async collectEnumValues(_tableName, column2) {
954
- return parseEnumLabels(column2.type);
955
- }
956
- async collectLowCardinality(tableName, column2) {
957
- if (/\b(?:BLOB|STRUCT|MAP|LIST|UNION|ARRAY)\b/i.test(column2.type)) {
958
- return void 0;
959
- }
960
- const relation = this.#adapter.quoteRelationName(tableName);
961
- const identifier = this.#adapter.quoteIdentifier(column2.name);
962
- const rows = await this.#adapter.runQuery(`
963
- SELECT DISTINCT CAST(${identifier} AS VARCHAR) AS value
964
- FROM ${relation}
965
- WHERE ${identifier} IS NOT NULL
966
- ORDER BY value
967
- LIMIT ${this.lowCardinalityLimit + 1}
968
- `);
969
- if (rows.length === 0 || rows.length > this.lowCardinalityLimit) {
970
- return void 0;
971
- }
972
- if (rows.some((row) => typeof row.value !== "string")) return void 0;
973
- return rows.map((row) => row.value);
974
- }
975
- };
976
- function sqlLiterals(adapter, values) {
977
- return values.map((value) => `'${adapter.escapeString(value)}'`).join(", ");
978
- }
979
- function relationPredicate(adapter, relation) {
980
- return `database_name = '${adapter.escapeString(relation.catalog)}'
981
- AND schema_name = '${adapter.escapeString(relation.schema)}'
982
- AND table_name = '${adapter.escapeString(relation.table)}'`;
983
- }
984
- function columnFromRow(row) {
985
- return {
986
- name: requiredString(row.column_name, "column_name"),
987
- type: requiredString(row.data_type, "data_type")
988
- };
989
- }
990
- async function foreignKeys(adapter, relation, direction) {
991
- const predicate = direction === "outgoing" ? `table_name = '${adapter.escapeString(relation.table)}'` : `referenced_table = '${adapter.escapeString(relation.table)}'`;
992
- return adapter.runQuery(`
993
- SELECT
994
- table_name,
995
- constraint_name,
996
- constraint_type,
997
- expression,
998
- constraint_column_names,
999
- referenced_table,
1000
- referenced_column_names
1001
- FROM duckdb_constraints()
1002
- WHERE database_name = '${adapter.escapeString(relation.catalog)}'
1003
- AND schema_name = '${adapter.escapeString(relation.schema)}'
1004
- AND constraint_type = 'FOREIGN KEY'
1005
- AND ${predicate}
1006
- ORDER BY table_name, constraint_index
1007
- `);
1008
- }
1009
- function relationshipFromRow(adapter, source, row) {
1010
- return {
1011
- table: adapter.formatRelationName(
1012
- source.catalog,
1013
- source.schema,
1014
- source.table
1015
- ),
1016
- from: stringArray(row.constraint_column_names, "constraint_column_names"),
1017
- referenced_table: adapter.formatRelationName(
1018
- source.catalog,
1019
- source.schema,
1020
- requiredString(row.referenced_table, "referenced_table")
1021
- ),
1022
- to: stringArray(row.referenced_column_names, "referenced_column_names")
1023
- };
1024
- }
1025
- function constraintFromRow(adapter, relation, row) {
1026
- const type = requiredString(row.constraint_type, "constraint_type");
1027
- const mapped = CONSTRAINT_TYPES.get(type);
1028
- if (!mapped) throw new Error(`Unknown DuckDB constraint type: ${type}`);
1029
- const referenced = mapped === "FOREIGN_KEY" ? adapter.formatRelationName(
1030
- relation.catalog,
1031
- relation.schema,
1032
- requiredString(row.referenced_table, "referenced_table")
1033
- ) : void 0;
1034
- return {
1035
- name: requiredString(row.constraint_name, "constraint_name"),
1036
- type: mapped,
1037
- columns: stringArray(
1038
- row.constraint_column_names,
1039
- "constraint_column_names"
1040
- ),
1041
- definition: mapped === "CHECK" && typeof row.expression === "string" ? row.expression : void 0,
1042
- referencedTable: referenced,
1043
- referencedColumns: mapped === "FOREIGN_KEY" ? stringArray(row.referenced_column_names, "referenced_column_names") : void 0
1044
- };
1045
- }
1046
- function parseEnumLabels(type) {
1047
- const body = /^ENUM\((.*)\)$/i.exec(type)?.[1];
1048
- if (body === void 0) return void 0;
1049
- const labels = [...body.matchAll(/'((?:''|[^'])*)'/g)].map(
1050
- (match) => match[1].replaceAll("''", "'")
1051
- );
1052
- return labels.length > 0 ? labels : void 0;
1053
- }
1054
- function requiredString(value, field) {
1055
- if (typeof value !== "string") {
1056
- throw new Error(`DuckDB catalog field ${field} must be a string.`);
1057
- }
1058
- return value;
1059
- }
1060
- function stringArray(value, field) {
1061
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1062
- throw new Error(`DuckDB catalog field ${field} must be a string array.`);
1063
- }
1064
- return value;
1065
- }
1066
-
1067
- // packages/text2sql/src/lib/adapters/adapter.ts
1068
- import { format as formatSql } from "sql-formatter";
1069
-
1070
- // packages/text2sql/src/lib/agents/exceptions.ts
1071
- var sqlValidationMarker = /* @__PURE__ */ Symbol("SQLValidationError");
1072
- var unanswerableSqlMarker = /* @__PURE__ */ Symbol("UnanswerableSQLError");
1073
- var sqlScopeMarker = /* @__PURE__ */ Symbol("SQLScopeError");
1074
- var sqlReadOnlyMarker = /* @__PURE__ */ Symbol("SQLReadOnlyError");
1075
- var SQLValidationError = class _SQLValidationError extends Error {
1076
- [sqlValidationMarker];
1077
- constructor(message) {
1078
- super(message);
1079
- this.name = "SQLValidationError";
1080
- this[sqlValidationMarker] = true;
1081
- }
1082
- static isInstance(error) {
1083
- return error instanceof _SQLValidationError && error[sqlValidationMarker] === true;
1084
- }
1085
- };
1086
- var UnanswerableSQLError = class _UnanswerableSQLError extends Error {
1087
- [unanswerableSqlMarker];
1088
- constructor(message) {
1089
- super(message);
1090
- this.name = "UnanswerableSQLError";
1091
- this[unanswerableSqlMarker] = true;
1092
- }
1093
- static isInstance(error) {
1094
- return error instanceof _UnanswerableSQLError && error[unanswerableSqlMarker] === true;
1095
- }
1096
- };
1097
- var SQLScopeError = class _SQLScopeError extends Error {
1098
- [sqlScopeMarker];
1099
- payload;
1100
- errorType;
1101
- constructor(payload) {
1102
- super(JSON.stringify(payload));
1103
- this.name = "SQLScopeError";
1104
- this.payload = payload;
1105
- this.errorType = payload.error_type;
1106
- this[sqlScopeMarker] = true;
1107
- }
1108
- static isInstance(error) {
1109
- return error instanceof _SQLScopeError && error[sqlScopeMarker] === true;
1110
- }
1111
- };
1112
- var SQLReadOnlyError = class _SQLReadOnlyError extends Error {
1113
- [sqlReadOnlyMarker];
1114
- constructor(message) {
1115
- super(message);
1116
- this.name = "SQLReadOnlyError";
1117
- this[sqlReadOnlyMarker] = true;
1118
- }
1119
- static isInstance(error) {
1120
- return error instanceof _SQLReadOnlyError && error[sqlReadOnlyMarker] === true;
1121
- }
1122
- };
1123
-
1124
- // packages/text2sql/src/lib/fragments/schema.ts
1125
- function dialectInfo(input) {
1126
- return {
1127
- name: "dialectInfo",
1128
- data: {
1129
- dialect: input.dialect,
1130
- ...input.version && { version: input.version },
1131
- ...input.database && { database: input.database },
1132
- ...input.details && { details: input.details }
1133
- }
1134
- };
1135
- }
1136
- function table(input) {
1137
- return {
1138
- name: "table",
1139
- data: {
1140
- name: input.name,
1141
- ...input.schema && { schema: input.schema },
1142
- ...input.rowCount != null && { rowCount: input.rowCount },
1143
- ...input.sizeHint && { sizeHint: input.sizeHint },
1144
- columns: input.columns,
1145
- ...input.indexes?.length && { indexes: input.indexes },
1146
- ...input.constraints?.length && { constraints: input.constraints }
1147
- }
1148
- };
1149
- }
1150
- function column(input) {
1151
- return {
1152
- name: "column",
1153
- data: {
1154
- name: input.name,
1155
- type: input.type,
1156
- ...input.pk && { pk: true },
1157
- ...input.fk && { fk: input.fk },
1158
- ...input.unique && { unique: true },
1159
- ...input.notNull && { notNull: true },
1160
- ...input.default && { default: input.default },
1161
- ...input.indexed && { indexed: true },
1162
- ...input.values?.length && { values: input.values },
1163
- ...input.stats && { stats: input.stats }
909
+ /**
910
+ * Execute the grounding process.
911
+ * Writes discovered tables and relationships to the context.
912
+ */
913
+ async execute(ctx) {
914
+ const seedTables = await this.applyFilter();
915
+ const forward = this.#forward;
916
+ const backward = this.#backward;
917
+ if (!forward && !backward) {
918
+ const tables3 = await Promise.all(
919
+ seedTables.map(async (name, index2) => {
920
+ ctx.onProgress({
921
+ type: "phase:progress",
922
+ phase: "tables",
923
+ table: name,
924
+ message: `Loading table ${name}...`,
925
+ current: index2 + 1,
926
+ total: seedTables.length
927
+ });
928
+ return applyColumnFilter(await this.getTable(name), this.#columns);
929
+ })
930
+ );
931
+ ctx.tables.push(...tables3);
932
+ return;
1164
933
  }
1165
- };
1166
- }
1167
- function index(input) {
1168
- return {
1169
- name: "index",
1170
- data: {
1171
- name: input.name,
1172
- columns: input.columns,
1173
- ...input.unique && { unique: true },
1174
- ...input.type && { type: input.type }
934
+ const tables2 = {};
935
+ const allRelationships = [];
936
+ const seenRelationships = /* @__PURE__ */ new Set();
937
+ const forwardQueue = [];
938
+ const backwardQueue = [];
939
+ const forwardVisited = /* @__PURE__ */ new Set();
940
+ const backwardVisited = /* @__PURE__ */ new Set();
941
+ let loadedCount = 0;
942
+ for (const name of seedTables) {
943
+ if (forward) forwardQueue.push({ name, depth: 0 });
944
+ if (backward) backwardQueue.push({ name, depth: 0 });
1175
945
  }
1176
- };
1177
- }
1178
- function constraint(input) {
1179
- return {
1180
- name: "constraint",
1181
- data: {
1182
- name: input.name,
1183
- type: input.type,
1184
- ...input.columns?.length && { columns: input.columns },
1185
- ...input.definition && { definition: input.definition },
1186
- ...input.defaultValue && { defaultValue: input.defaultValue },
1187
- ...input.referencedTable && { referencedTable: input.referencedTable },
1188
- ...input.referencedColumns?.length && {
1189
- referencedColumns: input.referencedColumns
946
+ const forwardLimit = forward === true ? Infinity : forward || 0;
947
+ while (forwardQueue.length > 0) {
948
+ const item = forwardQueue.shift();
949
+ if (!item) break;
950
+ const { name, depth } = item;
951
+ if (forwardVisited.has(name)) continue;
952
+ forwardVisited.add(name);
953
+ if (!tables2[name]) {
954
+ loadedCount++;
955
+ ctx.onProgress({
956
+ type: "phase:progress",
957
+ phase: "tables",
958
+ table: name,
959
+ message: `Loading table ${name}...`,
960
+ current: loadedCount
961
+ });
962
+ tables2[name] = applyColumnFilter(
963
+ await this.getTable(name),
964
+ this.#columns
965
+ );
1190
966
  }
1191
- }
1192
- };
1193
- }
1194
- function view(input) {
1195
- return {
1196
- name: "view",
1197
- data: {
1198
- name: input.name,
1199
- ...input.schema && { schema: input.schema },
1200
- columns: input.columns,
1201
- ...input.definition && { definition: input.definition }
1202
- }
1203
- };
1204
- }
1205
- function relationship(input) {
1206
- return {
1207
- name: "relationship",
1208
- data: {
1209
- from: input.from,
1210
- to: input.to,
1211
- ...input.cardinality && { cardinality: input.cardinality }
1212
- }
1213
- };
1214
- }
1215
-
1216
- // packages/text2sql/src/lib/adapters/groundings/context.ts
1217
- var noopProgress = () => {
1218
- };
1219
- function timestampProgress(onProgress) {
1220
- if (!onProgress) return noopProgress;
1221
- return (progress) => {
1222
- onProgress({
1223
- ...progress,
1224
- timestampMs: progress.timestampMs ?? Date.now()
1225
- });
1226
- };
1227
- }
1228
- function createGroundingContext(options = {}) {
1229
- return {
1230
- tables: [],
1231
- views: [],
1232
- relationships: [],
1233
- info: void 0,
1234
- fragments: [],
1235
- cache: /* @__PURE__ */ new Map(),
1236
- onProgress: timestampProgress(options.onProgress)
1237
- };
1238
- }
1239
-
1240
- // packages/text2sql/src/lib/adapters/adapter.ts
1241
- var Adapter = class {
1242
- #policyAnalyzer;
1243
- /**
1244
- * Injects the strategy that guards validate() and execute().
1245
- */
1246
- constructor(policyAnalyzer) {
1247
- this.#policyAnalyzer = policyAnalyzer;
1248
- }
1249
- /**
1250
- * Introspect the database schema and return context fragments.
1251
- *
1252
- * Executes all configured groundings to populate the context, then
1253
- * generates fragments from the complete context data.
1254
- *
1255
- * @param ctx - Optional grounding context for sharing state between groundings
1256
- * @returns Array of context fragments representing the database schema
1257
- */
1258
- async introspect(ctx = createGroundingContext()) {
1259
- for (const fn of this.grounding) {
1260
- const grounding = fn(this);
1261
- if (grounding.phase) {
967
+ if (depth < forwardLimit) {
968
+ const rels = await this.findOutgoingRelations(name);
969
+ for (const rel of rels) {
970
+ if (!this.isRelationshipVisible(rel)) continue;
971
+ this.addRelationship(rel, allRelationships, seenRelationships);
972
+ if (!forwardVisited.has(rel.referenced_table)) {
973
+ forwardQueue.push({ name: rel.referenced_table, depth: depth + 1 });
974
+ }
975
+ }
976
+ }
977
+ }
978
+ const backwardLimit = backward === true ? Infinity : backward || 0;
979
+ while (backwardQueue.length > 0) {
980
+ const item = backwardQueue.shift();
981
+ if (!item) break;
982
+ const { name, depth } = item;
983
+ if (backwardVisited.has(name)) continue;
984
+ backwardVisited.add(name);
985
+ if (!tables2[name]) {
986
+ loadedCount++;
1262
987
  ctx.onProgress({
1263
- type: "phase:start",
1264
- phase: grounding.phase,
1265
- message: `Starting ${grounding.name} introspection...`
988
+ type: "phase:progress",
989
+ phase: "tables",
990
+ table: name,
991
+ message: `Loading table ${name}...`,
992
+ current: loadedCount
1266
993
  });
994
+ tables2[name] = applyColumnFilter(
995
+ await this.getTable(name),
996
+ this.#columns
997
+ );
1267
998
  }
1268
- try {
1269
- await grounding.execute(ctx);
1270
- } finally {
1271
- if (grounding.phase) {
1272
- ctx.onProgress({
1273
- type: "phase:end",
1274
- phase: grounding.phase,
1275
- message: `Finished ${grounding.name} introspection.`
1276
- });
999
+ if (depth < backwardLimit) {
1000
+ const rels = await this.findIncomingRelations(name);
1001
+ for (const rel of rels) {
1002
+ if (!this.isRelationshipVisible(rel)) continue;
1003
+ this.addRelationship(rel, allRelationships, seenRelationships);
1004
+ if (!backwardVisited.has(rel.table)) {
1005
+ backwardQueue.push({ name: rel.table, depth: depth + 1 });
1006
+ }
1277
1007
  }
1278
1008
  }
1279
1009
  }
1280
- return this.#toSchemaFragments(ctx);
1010
+ const tablesList = Object.values(tables2);
1011
+ ctx.tables.push(...tablesList);
1012
+ ctx.relationships.push(...allRelationships);
1281
1013
  }
1282
1014
  /**
1283
- * Resolve the allowed entity names (tables + views) from grounding config.
1284
- * Asks each grounding to contribute its entities; annotators contribute
1285
- * nothing, so their metadata scans are skipped.
1286
- * Results are NOT cached — call once and store the result.
1015
+ * Apply the filter to get seed table names.
1016
+ * If filter is an explicit array, skip querying all table names.
1287
1017
  */
1288
- async resolveAllowedEntities() {
1289
- const ctx = createGroundingContext();
1290
- for (const fn of this.grounding) {
1291
- await fn(this).contributeEntities(ctx);
1018
+ async applyFilter() {
1019
+ const filter = this.#filter;
1020
+ if (Array.isArray(filter)) {
1021
+ return filter;
1292
1022
  }
1293
- return [...ctx.tables.map((t) => t.name), ...ctx.views.map((v) => v.name)];
1023
+ const names = await this.getAllTableNames();
1024
+ if (!filter) {
1025
+ return names;
1026
+ }
1027
+ if (filter instanceof RegExp) {
1028
+ return names.filter((name) => filter.test(name));
1029
+ }
1030
+ return names.filter(filter);
1294
1031
  }
1295
1032
  /**
1296
- * Convert complete grounding context to schema fragments.
1297
- * Called after all groundings have populated ctx with data.
1033
+ * Add a relationship to the collection, deduplicating by key.
1298
1034
  */
1299
- #toSchemaFragments(ctx) {
1300
- const fragments = [];
1301
- if (ctx.info) {
1302
- fragments.push(
1303
- dialectInfo({
1304
- dialect: ctx.info.dialect,
1305
- version: ctx.info.version,
1306
- database: ctx.info.database,
1307
- details: ctx.info.details
1308
- })
1309
- );
1310
- }
1311
- for (const t of ctx.tables) {
1312
- fragments.push(this.#tableToFragment(t));
1313
- }
1314
- for (const v of ctx.views) {
1315
- fragments.push(this.#viewToFragment(v));
1316
- }
1317
- const tableMap = new Map(ctx.tables.map((t) => [t.name, t]));
1318
- const tableColumnSets = new Map(
1319
- ctx.tables.map((t) => [t.name, new Set(t.columns.map((c) => c.name))])
1320
- );
1321
- for (const rel of ctx.relationships) {
1322
- const sourceColumns = tableColumnSets.get(rel.table);
1323
- const targetColumns = tableColumnSets.get(rel.referenced_table);
1324
- if (sourceColumns && rel.from.some((column2) => !sourceColumns.has(column2))) {
1325
- continue;
1326
- }
1327
- if (targetColumns && rel.to.some((column2) => !targetColumns.has(column2))) {
1328
- continue;
1329
- }
1330
- const sourceTable = tableMap.get(rel.table);
1331
- const targetTable = tableMap.get(rel.referenced_table);
1332
- fragments.push(
1333
- this.#relationshipToFragment(rel, sourceTable, targetTable)
1334
- );
1035
+ addRelationship(rel, all, seen) {
1036
+ const key = `${rel.table}:${rel.from.join(",")}:${rel.referenced_table}:${rel.to.join(",")}`;
1037
+ if (!seen.has(key)) {
1038
+ seen.add(key);
1039
+ all.push(rel);
1335
1040
  }
1336
- fragments.push(...ctx.fragments ?? []);
1337
- return fragments;
1041
+ }
1042
+ isRelationshipVisible(rel) {
1043
+ return this.areColumnsVisible(rel.from, this.#columns?.[rel.table]) && this.areColumnsVisible(rel.to, this.#columns?.[rel.referenced_table]);
1044
+ }
1045
+ areColumnsVisible(names, filter) {
1046
+ if (!filter) return true;
1047
+ return filterColumns(
1048
+ names.map((name) => ({ name })),
1049
+ filter
1050
+ ).length === names.length;
1051
+ }
1052
+ };
1053
+
1054
+ // packages/text2sql/src/lib/adapters/groundings/view.grounding.ts
1055
+ var ViewGrounding = class extends AbstractGrounding {
1056
+ #filter;
1057
+ #columns;
1058
+ includeDefinition;
1059
+ constructor(config = {}) {
1060
+ super("view", "views");
1061
+ this.#filter = config.filter;
1062
+ this.#columns = config.columns;
1063
+ this.includeDefinition = config.includeDefinition ?? true;
1064
+ }
1065
+ async contributeEntities(ctx) {
1066
+ await this.execute(ctx);
1338
1067
  }
1339
1068
  /**
1340
- * Convert a Table to a table fragment with nested column, index, and constraint fragments.
1069
+ * Execute the grounding process.
1070
+ * Writes discovered views to the context.
1341
1071
  */
1342
- #tableToFragment(t) {
1343
- const pkConstraint = t.constraints?.find((c) => c.type === "PRIMARY_KEY");
1344
- const pkColumns = new Set(pkConstraint?.columns ?? []);
1345
- const notNullColumns = new Set(
1346
- t.constraints?.filter((c) => c.type === "NOT_NULL").flatMap((c) => c.columns ?? []) ?? []
1072
+ async execute(ctx) {
1073
+ const viewNames = await this.applyFilter();
1074
+ const views2 = await Promise.all(
1075
+ viewNames.map(async (name, index2) => {
1076
+ ctx.onProgress({
1077
+ type: "phase:progress",
1078
+ phase: "views",
1079
+ table: name,
1080
+ message: `Loading view ${name}...`,
1081
+ current: index2 + 1,
1082
+ total: viewNames.length
1083
+ });
1084
+ return applyColumnFilter(await this.getView(name), this.#columns);
1085
+ })
1347
1086
  );
1348
- const defaultByColumn = /* @__PURE__ */ new Map();
1349
- for (const c of t.constraints?.filter((c2) => c2.type === "DEFAULT") ?? []) {
1350
- for (const col of c.columns ?? []) {
1351
- if (c.defaultValue != null) {
1352
- defaultByColumn.set(col, c.defaultValue);
1353
- }
1087
+ ctx.views.push(...views2);
1088
+ }
1089
+ /**
1090
+ * Apply the filter to get view names.
1091
+ * If filter is an explicit array, skip querying all view names.
1092
+ */
1093
+ async applyFilter() {
1094
+ const filter = this.#filter;
1095
+ if (Array.isArray(filter)) {
1096
+ return filter;
1097
+ }
1098
+ const names = await this.getAllViewNames();
1099
+ if (!filter) {
1100
+ return names;
1101
+ }
1102
+ if (filter instanceof RegExp) {
1103
+ return names.filter((name) => filter.test(name));
1104
+ }
1105
+ return names.filter(filter);
1106
+ }
1107
+ };
1108
+
1109
+ // packages/text2sql/src/lib/adapters/duckdb/duckdb-identifiers.ts
1110
+ import { quotedIdentifier } from "@duckdb/node-api";
1111
+ function formatDuckDBIdentifierPath(parts) {
1112
+ return parts.map(quotedIdentifier).join(".");
1113
+ }
1114
+ function parseDuckDBRelationName(name) {
1115
+ const parts = splitIdentifierPath(name);
1116
+ if (parts.length === 1) return { table: parts[0] };
1117
+ if (parts.length === 2) {
1118
+ return { schema: parts[0], table: parts[1] };
1119
+ }
1120
+ if (parts.length === 3) {
1121
+ return { catalog: parts[0], schema: parts[1], table: parts[2] };
1122
+ }
1123
+ throw new Error(`DuckDB relation must have at most three parts: ${name}`);
1124
+ }
1125
+ function splitIdentifierPath(input) {
1126
+ const parts = [];
1127
+ let part = "";
1128
+ let quoted = false;
1129
+ for (let index2 = 0; index2 < input.length; index2++) {
1130
+ const character = input[index2];
1131
+ if (character === '"') {
1132
+ if (quoted && input[index2 + 1] === '"') {
1133
+ part += '"';
1134
+ index2++;
1135
+ } else {
1136
+ quoted = !quoted;
1354
1137
  }
1138
+ } else if (character === "." && !quoted) {
1139
+ if (!part) throw new Error(`Invalid DuckDB relation: ${input}`);
1140
+ parts.push(part);
1141
+ part = "";
1142
+ } else {
1143
+ part += character;
1355
1144
  }
1356
- const uniqueColumns = new Set(
1357
- t.constraints?.filter((c) => c.type === "UNIQUE" && c.columns?.length === 1).flatMap((c) => c.columns ?? []) ?? []
1358
- );
1359
- const fkByColumn = /* @__PURE__ */ new Map();
1360
- for (const c of t.constraints?.filter((c2) => c2.type === "FOREIGN_KEY") ?? []) {
1361
- const cols = c.columns ?? [];
1362
- const refCols = c.referencedColumns ?? [];
1363
- for (let i = 0; i < cols.length; i++) {
1364
- const refCol = refCols[i] ?? refCols[0] ?? cols[i];
1365
- fkByColumn.set(cols[i], `${c.referencedTable}.${refCol}`);
1145
+ }
1146
+ if (quoted || !part) throw new Error(`Invalid DuckDB relation: ${input}`);
1147
+ parts.push(part);
1148
+ return parts;
1149
+ }
1150
+
1151
+ // packages/text2sql/src/lib/adapters/duckdb/duckdb.groundings.ts
1152
+ var tableNameRow = z3.object({
1153
+ database_name: z3.string(),
1154
+ schema_name: z3.string(),
1155
+ table_name: z3.string()
1156
+ });
1157
+ var viewNameRow = z3.object({
1158
+ database_name: z3.string(),
1159
+ schema_name: z3.string(),
1160
+ view_name: z3.string()
1161
+ });
1162
+ var columnRow = z3.object({ column_name: z3.string(), data_type: z3.string() });
1163
+ var columnNameRow = z3.object({ column_name: z3.string() });
1164
+ var columnDefaultRow = z3.object({
1165
+ column_name: z3.string(),
1166
+ column_default: z3.string()
1167
+ });
1168
+ var infoRow = z3.object({
1169
+ version: z3.string(),
1170
+ catalog: z3.string(),
1171
+ schema: z3.string()
1172
+ });
1173
+ var viewSqlRow = z3.object({ sql: z3.string().nullable() });
1174
+ var constraintRow = z3.object({
1175
+ constraint_name: z3.string(),
1176
+ constraint_type: z3.string(),
1177
+ expression: z3.string().nullable(),
1178
+ constraint_column_names: z3.array(z3.string()),
1179
+ referenced_table: z3.string().nullable(),
1180
+ referenced_column_names: z3.array(z3.string())
1181
+ });
1182
+ var foreignKeyRow = constraintRow.extend({ table_name: z3.string() });
1183
+ var indexRow = z3.object({
1184
+ index_name: z3.string(),
1185
+ expressions: z3.array(z3.string()).nullable(),
1186
+ is_unique: z3.boolean(),
1187
+ is_primary: z3.boolean()
1188
+ });
1189
+ var estimatedSizeRow = z3.object({ estimated_size: numericValue.nullable() });
1190
+ var columnStatsRow = z3.object({
1191
+ min_value: z3.string().nullable(),
1192
+ max_value: z3.string().nullable(),
1193
+ null_fraction: z3.number().nullable(),
1194
+ n_distinct: numericValue
1195
+ });
1196
+ var CONSTRAINT_TYPES = /* @__PURE__ */ new Map([
1197
+ ["CHECK", "CHECK"],
1198
+ ["UNIQUE", "UNIQUE"],
1199
+ ["NOT NULL", "NOT_NULL"],
1200
+ ["PRIMARY KEY", "PRIMARY_KEY"],
1201
+ ["FOREIGN KEY", "FOREIGN_KEY"]
1202
+ ]);
1203
+ var DuckDBTableGrounding = class extends TableGrounding {
1204
+ #adapter;
1205
+ constructor(adapter, config = {}) {
1206
+ super(config);
1207
+ this.#adapter = adapter;
1208
+ }
1209
+ async applyFilter() {
1210
+ const names = await super.applyFilter();
1211
+ const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
1212
+ const result = [];
1213
+ for (const name of names) {
1214
+ const relation = await this.#adapter.resolveRelationName(name);
1215
+ if (catalogs.includes(relation.catalog) && schemas.includes(relation.schema)) {
1216
+ result.push(
1217
+ this.#adapter.formatRelationName(
1218
+ relation.catalog,
1219
+ relation.schema,
1220
+ relation.table
1221
+ )
1222
+ );
1366
1223
  }
1367
1224
  }
1368
- const columnFragments = t.columns.map(
1369
- (col) => column({
1370
- name: col.name,
1371
- type: col.type,
1372
- pk: pkColumns.has(col.name) || void 0,
1373
- fk: fkByColumn.get(col.name),
1374
- unique: uniqueColumns.has(col.name) || void 0,
1375
- notNull: notNullColumns.has(col.name) || void 0,
1376
- default: defaultByColumn.get(col.name),
1377
- indexed: col.isIndexed || void 0,
1378
- values: col.values,
1379
- stats: col.stats
1380
- })
1225
+ return result;
1226
+ }
1227
+ async getAllTableNames() {
1228
+ const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
1229
+ const rows = await this.#adapter.runQuery(
1230
+ `
1231
+ SELECT database_name, schema_name, table_name
1232
+ FROM duckdb_tables()
1233
+ WHERE NOT internal
1234
+ AND NOT temporary
1235
+ AND database_name IN (${sqlLiterals(this.#adapter, catalogs)})
1236
+ AND schema_name IN (${sqlLiterals(this.#adapter, schemas)})
1237
+ ORDER BY database_name, schema_name, table_name
1238
+ `,
1239
+ tableNameRow
1381
1240
  );
1382
- const presentColumns = new Set(t.columns.map((c) => c.name));
1383
- const indexFragments = (t.indexes ?? []).filter(
1384
- (idx) => idx.columns.every((column2) => presentColumns.has(column2))
1385
- ).map(
1386
- (idx) => index({
1387
- name: idx.name,
1388
- columns: idx.columns,
1389
- unique: idx.unique,
1390
- type: idx.type
1391
- })
1241
+ return rows.map(
1242
+ (row) => this.#adapter.formatRelationName(
1243
+ row.database_name,
1244
+ row.schema_name,
1245
+ row.table_name
1246
+ )
1392
1247
  );
1393
- const constraintFragments = (t.constraints ?? []).filter(
1394
- (c) => c.type === "CHECK" || c.type === "UNIQUE" && (c.columns?.length ?? 0) > 1
1395
- ).filter(
1396
- (c) => !c.columns?.length || c.columns.every((column2) => presentColumns.has(column2))
1397
- ).map(
1398
- (c) => constraint({
1399
- name: c.name,
1400
- type: c.type,
1401
- columns: c.columns,
1402
- definition: c.definition
1403
- })
1248
+ }
1249
+ async getTable(tableName) {
1250
+ const { catalog, schema, table: table2 } = await this.#adapter.resolveRelationName(tableName);
1251
+ const rows = await this.#adapter.runQuery(
1252
+ `
1253
+ SELECT column_name, data_type
1254
+ FROM duckdb_columns()
1255
+ WHERE database_name = '${this.#adapter.escapeString(catalog)}'
1256
+ AND schema_name = '${this.#adapter.escapeString(schema)}'
1257
+ AND table_name = '${this.#adapter.escapeString(table2)}'
1258
+ ORDER BY column_index
1259
+ `,
1260
+ columnRow
1404
1261
  );
1405
- return table({
1406
- name: t.name,
1407
- schema: t.schema,
1408
- rowCount: t.rowCount,
1409
- sizeHint: t.sizeHint,
1410
- columns: columnFragments,
1411
- indexes: indexFragments.length > 0 ? indexFragments : void 0,
1412
- constraints: constraintFragments.length > 0 ? constraintFragments : void 0
1262
+ return {
1263
+ name: this.#adapter.formatRelationName(catalog, schema, table2),
1264
+ schema: formatDuckDBIdentifierPath([catalog, schema]),
1265
+ rawName: table2,
1266
+ columns: rows.map(columnFromRow)
1267
+ };
1268
+ }
1269
+ async findOutgoingRelations(tableName) {
1270
+ const relation = await this.#adapter.resolveRelationName(tableName);
1271
+ const rows = await foreignKeys(this.#adapter, relation, "outgoing");
1272
+ return rows.map((row) => relationshipFromRow(this.#adapter, relation, row));
1273
+ }
1274
+ async findIncomingRelations(tableName) {
1275
+ const relation = await this.#adapter.resolveRelationName(tableName);
1276
+ const rows = await foreignKeys(this.#adapter, relation, "incoming");
1277
+ return rows.map((row) => {
1278
+ const source = {
1279
+ catalog: relation.catalog,
1280
+ schema: relation.schema,
1281
+ table: row.table_name
1282
+ };
1283
+ return relationshipFromRow(this.#adapter, source, row);
1413
1284
  });
1414
1285
  }
1415
- /**
1416
- * Convert a View to a view fragment with nested column fragments.
1417
- */
1418
- #viewToFragment(v) {
1419
- const columnFragments = v.columns.map(
1420
- (col) => column({
1421
- name: col.name,
1422
- type: col.type,
1423
- values: col.values,
1424
- stats: col.stats
1425
- })
1286
+ };
1287
+ var DuckDBInfoGrounding = class extends InfoGrounding {
1288
+ #adapter;
1289
+ constructor(adapter, config = {}) {
1290
+ super(config);
1291
+ this.#adapter = adapter;
1292
+ }
1293
+ async collectInfo() {
1294
+ const rows = await this.#adapter.runQuery(
1295
+ `
1296
+ SELECT
1297
+ version() AS version,
1298
+ current_database() AS catalog,
1299
+ current_schema() AS schema
1300
+ `,
1301
+ infoRow
1426
1302
  );
1427
- return view({
1428
- name: v.name,
1429
- schema: v.schema,
1430
- columns: columnFragments,
1431
- definition: v.definition
1432
- });
1303
+ const row = rows.at(0);
1304
+ return {
1305
+ dialect: "duckdb",
1306
+ version: row?.version,
1307
+ database: row?.catalog,
1308
+ details: {
1309
+ currentSchema: row?.schema ?? "unknown",
1310
+ identifierQualification: "catalog.schema.table",
1311
+ parameterPlaceholders: ["$1", "$name"]
1312
+ }
1313
+ };
1433
1314
  }
1434
- /**
1435
- * Convert a Relationship to a relationship fragment.
1436
- * Infers cardinality from row counts if available.
1437
- */
1438
- #relationshipToFragment(rel, sourceTable, targetTable) {
1439
- const sourceCount = sourceTable?.rowCount;
1440
- const targetCount = targetTable?.rowCount;
1441
- let cardinality;
1442
- if (sourceCount != null && targetCount != null && targetCount > 0) {
1443
- const ratio = sourceCount / targetCount;
1444
- if (ratio > 5) {
1445
- cardinality = "many-to-one";
1446
- } else if (ratio < 1.2 && ratio > 0.8) {
1447
- cardinality = "one-to-one";
1448
- } else if (ratio < 0.2) {
1449
- cardinality = "one-to-many";
1315
+ };
1316
+ var DuckDBViewGrounding = class extends ViewGrounding {
1317
+ #adapter;
1318
+ constructor(adapter, config = {}) {
1319
+ super(config);
1320
+ this.#adapter = adapter;
1321
+ }
1322
+ async applyFilter() {
1323
+ const names = await super.applyFilter();
1324
+ const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
1325
+ const result = [];
1326
+ for (const name of names) {
1327
+ const relation = await this.#adapter.resolveRelationName(name);
1328
+ if (catalogs.includes(relation.catalog) && schemas.includes(relation.schema)) {
1329
+ result.push(
1330
+ this.#adapter.formatRelationName(
1331
+ relation.catalog,
1332
+ relation.schema,
1333
+ relation.table
1334
+ )
1335
+ );
1450
1336
  }
1451
1337
  }
1452
- return relationship({
1453
- from: { table: rel.table, columns: rel.from },
1454
- to: { table: rel.referenced_table, columns: rel.to },
1455
- cardinality
1456
- });
1338
+ return result;
1457
1339
  }
1458
- format(sql) {
1459
- const decoded = this.#decodeShellEscapes(sql);
1460
- try {
1461
- return formatSql(decoded, { language: this.formatterLanguage });
1462
- } catch {
1463
- return decoded;
1340
+ async getAllViewNames() {
1341
+ const { catalogs, schemas } = await this.#adapter.scopedNamespaces();
1342
+ const rows = await this.#adapter.runQuery(
1343
+ `
1344
+ SELECT database_name, schema_name, view_name
1345
+ FROM duckdb_views()
1346
+ WHERE NOT internal
1347
+ AND NOT temporary
1348
+ AND database_name IN (${sqlLiterals(this.#adapter, catalogs)})
1349
+ AND schema_name IN (${sqlLiterals(this.#adapter, schemas)})
1350
+ ORDER BY database_name, schema_name, view_name
1351
+ `,
1352
+ viewNameRow
1353
+ );
1354
+ return rows.map(
1355
+ (row) => this.#adapter.formatRelationName(
1356
+ row.database_name,
1357
+ row.schema_name,
1358
+ row.view_name
1359
+ )
1360
+ );
1361
+ }
1362
+ async getView(viewName) {
1363
+ const relation = await this.#adapter.resolveRelationName(viewName);
1364
+ const where = relationPredicate(this.#adapter, relation);
1365
+ const [columns, definitions] = await Promise.all([
1366
+ this.#adapter.runQuery(
1367
+ `
1368
+ SELECT column_name, data_type
1369
+ FROM duckdb_columns()
1370
+ WHERE ${where}
1371
+ ORDER BY column_index
1372
+ `,
1373
+ columnRow
1374
+ ),
1375
+ this.includeDefinition ? this.#adapter.runQuery(
1376
+ `
1377
+ SELECT sql
1378
+ FROM duckdb_views()
1379
+ WHERE database_name = '${this.#adapter.escapeString(relation.catalog)}'
1380
+ AND schema_name = '${this.#adapter.escapeString(relation.schema)}'
1381
+ AND view_name = '${this.#adapter.escapeString(relation.table)}'
1382
+ `,
1383
+ viewSqlRow
1384
+ ) : Promise.resolve([])
1385
+ ]);
1386
+ return {
1387
+ name: this.#adapter.formatRelationName(
1388
+ relation.catalog,
1389
+ relation.schema,
1390
+ relation.table
1391
+ ),
1392
+ schema: formatDuckDBIdentifierPath([relation.catalog, relation.schema]),
1393
+ rawName: relation.table,
1394
+ definition: definitions.at(0)?.sql ?? void 0,
1395
+ columns: columns.map(columnFromRow)
1396
+ };
1397
+ }
1398
+ };
1399
+ var DuckDBConstraintGrounding = class extends ConstraintGrounding {
1400
+ #adapter;
1401
+ constructor(adapter, config = {}) {
1402
+ super(config);
1403
+ this.#adapter = adapter;
1404
+ }
1405
+ async getConstraints(tableName) {
1406
+ const relation = await this.#adapter.resolveRelationName(tableName);
1407
+ const [rows, columns] = await Promise.all([
1408
+ this.#adapter.runQuery(
1409
+ `
1410
+ SELECT
1411
+ constraint_name,
1412
+ constraint_type,
1413
+ expression,
1414
+ constraint_column_names,
1415
+ referenced_table,
1416
+ referenced_column_names
1417
+ FROM duckdb_constraints()
1418
+ WHERE ${relationPredicate(this.#adapter, relation)}
1419
+ ORDER BY constraint_index
1420
+ `,
1421
+ constraintRow
1422
+ ),
1423
+ this.#adapter.runQuery(
1424
+ `
1425
+ SELECT column_name, column_default
1426
+ FROM duckdb_columns()
1427
+ WHERE ${relationPredicate(this.#adapter, relation)}
1428
+ AND column_default IS NOT NULL
1429
+ ORDER BY column_index
1430
+ `,
1431
+ columnDefaultRow
1432
+ )
1433
+ ]);
1434
+ const constraints2 = rows.map(
1435
+ (row) => constraintFromRow(this.#adapter, relation, row)
1436
+ );
1437
+ for (const column2 of columns) {
1438
+ constraints2.push({
1439
+ name: `${relation.table}_${column2.column_name}_default`,
1440
+ type: "DEFAULT",
1441
+ columns: [column2.column_name],
1442
+ defaultValue: column2.column_default
1443
+ });
1464
1444
  }
1445
+ return constraints2;
1465
1446
  }
1466
- #decodeShellEscapes(sql) {
1467
- return sql.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\([().*$])/g, "$1").replace(/\\(?=$)/gm, "");
1468
- }
1469
- #cachedAllowedEntities = null;
1470
- async #resolveScope() {
1471
- if (this.#cachedAllowedEntities) return this.#cachedAllowedEntities;
1472
- this.#cachedAllowedEntities = await this.resolveAllowedEntities();
1473
- return this.#cachedAllowedEntities;
1447
+ };
1448
+ var DuckDBIndexesGrounding = class extends IndexesGrounding {
1449
+ #adapter;
1450
+ constructor(adapter, config = {}) {
1451
+ super(config);
1452
+ this.#adapter = adapter;
1474
1453
  }
1475
- async #analyzePolicy(sql) {
1476
- return this.#policyAnalyzer.analyze(sql, {
1477
- resolveAllowedEntities: () => this.#resolveScope()
1454
+ async getIndexes(tableName) {
1455
+ const relation = await this.#adapter.resolveRelationName(tableName);
1456
+ const [rows, columnRows] = await Promise.all([
1457
+ this.#adapter.runQuery(
1458
+ `
1459
+ SELECT
1460
+ index_name,
1461
+ TRY_CAST(expressions AS VARCHAR[]) AS expressions,
1462
+ is_unique,
1463
+ is_primary
1464
+ FROM duckdb_indexes()
1465
+ WHERE ${relationPredicate(this.#adapter, relation)}
1466
+ ORDER BY index_name
1467
+ `,
1468
+ indexRow
1469
+ ),
1470
+ this.#adapter.runQuery(
1471
+ `
1472
+ SELECT column_name
1473
+ FROM duckdb_columns()
1474
+ WHERE ${relationPredicate(this.#adapter, relation)}
1475
+ ORDER BY column_index
1476
+ `,
1477
+ columnNameRow
1478
+ )
1479
+ ]);
1480
+ const tableColumns = columnRows.map((row) => row.column_name);
1481
+ return rows.flatMap((row) => {
1482
+ if (row.expressions === null) {
1483
+ return [];
1484
+ }
1485
+ const columns = row.expressions.map(
1486
+ (expression) => tableColumns.find(
1487
+ (column2) => expression === column2 || expression === this.#adapter.quoteIdentifier(column2)
1488
+ )
1489
+ );
1490
+ return columns.length > 0 && columns.every((column2) => column2 !== void 0) ? [
1491
+ {
1492
+ name: row.index_name,
1493
+ columns,
1494
+ unique: row.is_unique,
1495
+ type: row.is_primary ? "PRIMARY" : void 0
1496
+ }
1497
+ ] : [];
1478
1498
  });
1479
1499
  }
1480
- async validate(sql) {
1481
- const decoded = this.#decodeShellEscapes(sql);
1482
- const violation = await this.#analyzePolicy(decoded);
1483
- if (violation?.kind === "read-only") return violation.message;
1484
- if (violation?.kind === "scope") return JSON.stringify(violation.payload);
1485
- return this.validateImpl(decoded);
1500
+ };
1501
+ var DuckDBRowCountGrounding = class extends RowCountGrounding {
1502
+ #adapter;
1503
+ constructor(adapter, config = {}) {
1504
+ super(config);
1505
+ this.#adapter = adapter;
1486
1506
  }
1487
- async execute(sql) {
1488
- return this.executeImpl(await this.enforceExecutionPolicy(sql));
1507
+ async getRowCount(tableName) {
1508
+ const relation = await this.#adapter.resolveRelationName(tableName);
1509
+ const rows = await this.#adapter.runQuery(
1510
+ `
1511
+ SELECT estimated_size
1512
+ FROM duckdb_tables()
1513
+ WHERE ${relationPredicate(this.#adapter, relation)}
1514
+ `,
1515
+ estimatedSizeRow
1516
+ );
1517
+ return this.#adapter.toNumber(rows.at(0)?.estimated_size);
1489
1518
  }
1490
- async enforceExecutionPolicy(sql) {
1491
- const decoded = this.#decodeShellEscapes(sql);
1492
- const violation = await this.#analyzePolicy(decoded);
1493
- if (violation?.kind === "read-only") {
1494
- throw new SQLReadOnlyError(violation.message);
1495
- }
1496
- if (violation?.kind === "scope") {
1497
- throw new SQLScopeError(violation.payload);
1498
- }
1499
- return decoded;
1519
+ };
1520
+ var DuckDBColumnStatsGrounding = class extends ColumnStatsGrounding {
1521
+ #adapter;
1522
+ constructor(adapter, config = {}) {
1523
+ super(config);
1524
+ this.#adapter = adapter;
1500
1525
  }
1501
- /**
1502
- * Convert unknown database value to number.
1503
- * Handles number, bigint, and string types.
1504
- */
1505
- toNumber(value) {
1506
- if (typeof value === "number" && Number.isFinite(value)) {
1507
- return value;
1508
- }
1509
- if (typeof value === "bigint") {
1510
- return Number(value);
1511
- }
1512
- if (typeof value === "string" && value.trim() !== "") {
1513
- const parsed = Number(value);
1514
- return Number.isFinite(parsed) ? parsed : void 0;
1526
+ async collectStats(tableName, column2) {
1527
+ if (!/int|real|numeric|double|float|decimal|date|time|bool/i.test(column2.type)) {
1528
+ return void 0;
1515
1529
  }
1516
- return void 0;
1530
+ const relation = this.#adapter.quoteRelationName(tableName);
1531
+ const identifier = this.#adapter.quoteIdentifier(column2.name);
1532
+ const rows = await this.#adapter.runQuery(
1533
+ `
1534
+ SELECT
1535
+ CAST(MIN(${identifier}) AS VARCHAR) AS min_value,
1536
+ CAST(MAX(${identifier}) AS VARCHAR) AS max_value,
1537
+ CASE
1538
+ WHEN COUNT(*) = 0 THEN NULL
1539
+ ELSE COUNT(*) FILTER (WHERE ${identifier} IS NULL)::DOUBLE / COUNT(*)
1540
+ END AS null_fraction,
1541
+ COUNT(DISTINCT ${identifier}) AS n_distinct
1542
+ FROM ${relation}
1543
+ `,
1544
+ columnStatsRow
1545
+ );
1546
+ const row = rows.at(0);
1547
+ if (!row) return void 0;
1548
+ return {
1549
+ min: row.min_value ?? void 0,
1550
+ max: row.max_value ?? void 0,
1551
+ nullFraction: this.#adapter.toNumber(row.null_fraction),
1552
+ nDistinct: this.#adapter.toNumber(row.n_distinct)
1553
+ };
1517
1554
  }
1518
- /**
1519
- * Parse a potentially qualified table name into schema and table parts.
1520
- */
1521
- parseTableName(name) {
1522
- if (name.includes(".")) {
1523
- const [schema, ...rest] = name.split(".");
1524
- return { schema, table: rest.join(".") };
1525
- }
1526
- return { schema: this.defaultSchema ?? "", table: name };
1555
+ };
1556
+ var DuckDBColumnValuesGrounding = class extends ColumnValuesGrounding {
1557
+ #adapter;
1558
+ constructor(adapter, config = {}) {
1559
+ super(config);
1560
+ this.#adapter = adapter;
1527
1561
  }
1528
- /**
1529
- * Escape a string value for use in SQL string literals (single quotes).
1530
- * Used in WHERE clauses like: WHERE name = '${escapeString(value)}'
1531
- */
1532
- escapeString(value) {
1533
- return value.replace(/'/g, "''");
1562
+ async collectEnumValues(_tableName, column2) {
1563
+ return parseEnumLabels(column2.type);
1534
1564
  }
1535
- /**
1536
- * Build a SQL filter clause to include/exclude schemas.
1537
- * @param columnName - The schema column name (e.g., 'TABLE_SCHEMA')
1538
- * @param allowedSchemas - If provided, filter to these schemas only
1539
- */
1540
- buildSchemaFilter(columnName, allowedSchemas) {
1541
- if (allowedSchemas && allowedSchemas.length > 0) {
1542
- const values = allowedSchemas.map((s) => `'${this.escapeString(s)}'`).join(", ");
1543
- return `AND ${columnName} IN (${values})`;
1565
+ async collectLowCardinality(tableName, column2) {
1566
+ if (/\b(?:BLOB|STRUCT|MAP|LIST|UNION|ARRAY)\b/i.test(column2.type)) {
1567
+ return void 0;
1544
1568
  }
1545
- if (this.systemSchemas.length > 0) {
1546
- const values = this.systemSchemas.map((s) => `'${this.escapeString(s)}'`).join(", ");
1547
- return `AND ${columnName} NOT IN (${values})`;
1569
+ const relation = this.#adapter.quoteRelationName(tableName);
1570
+ const identifier = this.#adapter.quoteIdentifier(column2.name);
1571
+ const rows = await this.#adapter.runQuery(
1572
+ `
1573
+ SELECT DISTINCT CAST(${identifier} AS VARCHAR) AS value
1574
+ FROM ${relation}
1575
+ WHERE ${identifier} IS NOT NULL
1576
+ ORDER BY value
1577
+ LIMIT ${this.lowCardinalityLimit + 1}
1578
+ `,
1579
+ textValueRow
1580
+ );
1581
+ if (rows.length === 0 || rows.length > this.lowCardinalityLimit) {
1582
+ return void 0;
1548
1583
  }
1549
- return "";
1584
+ return rows.map((row) => row.value);
1550
1585
  }
1551
1586
  };
1587
+ function sqlLiterals(adapter, values) {
1588
+ return values.map((value) => `'${adapter.escapeString(value)}'`).join(", ");
1589
+ }
1590
+ function relationPredicate(adapter, relation) {
1591
+ return `database_name = '${adapter.escapeString(relation.catalog)}'
1592
+ AND schema_name = '${adapter.escapeString(relation.schema)}'
1593
+ AND table_name = '${adapter.escapeString(relation.table)}'`;
1594
+ }
1595
+ function columnFromRow(row) {
1596
+ return { name: row.column_name, type: row.data_type };
1597
+ }
1598
+ async function foreignKeys(adapter, relation, direction) {
1599
+ const predicate = direction === "outgoing" ? `table_name = '${adapter.escapeString(relation.table)}'` : `referenced_table = '${adapter.escapeString(relation.table)}'`;
1600
+ return adapter.runQuery(
1601
+ `
1602
+ SELECT
1603
+ table_name,
1604
+ constraint_name,
1605
+ constraint_type,
1606
+ expression,
1607
+ constraint_column_names,
1608
+ referenced_table,
1609
+ referenced_column_names
1610
+ FROM duckdb_constraints()
1611
+ WHERE database_name = '${adapter.escapeString(relation.catalog)}'
1612
+ AND schema_name = '${adapter.escapeString(relation.schema)}'
1613
+ AND constraint_type = 'FOREIGN KEY'
1614
+ AND ${predicate}
1615
+ ORDER BY table_name, constraint_index
1616
+ `,
1617
+ foreignKeyRow
1618
+ );
1619
+ }
1620
+ function relationshipFromRow(adapter, source, row) {
1621
+ return {
1622
+ table: adapter.formatRelationName(
1623
+ source.catalog,
1624
+ source.schema,
1625
+ source.table
1626
+ ),
1627
+ from: row.constraint_column_names,
1628
+ referenced_table: adapter.formatRelationName(
1629
+ source.catalog,
1630
+ source.schema,
1631
+ requiredString(row.referenced_table, "referenced_table")
1632
+ ),
1633
+ to: row.referenced_column_names
1634
+ };
1635
+ }
1636
+ function constraintFromRow(adapter, relation, row) {
1637
+ const mapped = CONSTRAINT_TYPES.get(row.constraint_type);
1638
+ if (!mapped) {
1639
+ throw new Error(`Unknown DuckDB constraint type: ${row.constraint_type}`);
1640
+ }
1641
+ const referenced = mapped === "FOREIGN_KEY" ? adapter.formatRelationName(
1642
+ relation.catalog,
1643
+ relation.schema,
1644
+ requiredString(row.referenced_table, "referenced_table")
1645
+ ) : void 0;
1646
+ return {
1647
+ name: row.constraint_name,
1648
+ type: mapped,
1649
+ columns: row.constraint_column_names,
1650
+ definition: mapped === "CHECK" && row.expression !== null ? row.expression : void 0,
1651
+ referencedTable: referenced,
1652
+ referencedColumns: mapped === "FOREIGN_KEY" ? row.referenced_column_names : void 0
1653
+ };
1654
+ }
1655
+ function parseEnumLabels(type) {
1656
+ const body = /^ENUM\((.*)\)$/i.exec(type)?.[1];
1657
+ if (body === void 0) return void 0;
1658
+ const labels = [...body.matchAll(/'((?:''|[^'])*)'/g)].map(
1659
+ (match) => match[1].replaceAll("''", "'")
1660
+ );
1661
+ return labels.length > 0 ? labels : void 0;
1662
+ }
1663
+ function requiredString(value, field) {
1664
+ if (value === null) {
1665
+ throw new Error(`DuckDB catalog field ${field} must be a string.`);
1666
+ }
1667
+ return value;
1668
+ }
1669
+
1670
+ // packages/text2sql/src/lib/adapters/duckdb/duckdb.ts
1671
+ import { z as z4 } from "zod";
1552
1672
 
1553
1673
  // packages/text2sql/src/lib/sql-scope-error.ts
1554
1674
  function buildScopeParseErrorPayload(sql, parserDialect, error) {
@@ -1764,6 +1884,7 @@ function isRecord(value) {
1764
1884
  }
1765
1885
 
1766
1886
  // packages/text2sql/src/lib/adapters/duckdb/duckdb.ts
1887
+ var namespaceRow = z4.object({ catalog: z4.string(), schema: z4.string() });
1767
1888
  var DuckDB = class extends Adapter {
1768
1889
  #options;
1769
1890
  #namespace;
@@ -1788,7 +1909,7 @@ var DuckDB = class extends Adapter {
1788
1909
  return this.#options.schemas;
1789
1910
  }
1790
1911
  async executeImpl(sql) {
1791
- return rowsFromResult(await this.#options.execute(sql));
1912
+ return this.rowsFrom(await this.#options.execute(sql), ["data", "rows"]);
1792
1913
  }
1793
1914
  async validateImpl(sql) {
1794
1915
  try {
@@ -1804,8 +1925,8 @@ var DuckDB = class extends Adapter {
1804
1925
  });
1805
1926
  }
1806
1927
  }
1807
- async runQuery(sql) {
1808
- return rowsFromResult(await this.#options.execute(sql));
1928
+ async queryRows(sql) {
1929
+ return this.rowsFrom(await this.#options.execute(sql), ["data", "rows"]);
1809
1930
  }
1810
1931
  quoteIdentifier(name) {
1811
1932
  return formatDuckDBIdentifierPath([name]);
@@ -1852,11 +1973,14 @@ var DuckDB = class extends Adapter {
1852
1973
  };
1853
1974
  }
1854
1975
  async #loadNamespace() {
1855
- const rows = await this.runQuery(`
1976
+ const rows = await this.runQuery(
1977
+ `
1856
1978
  SELECT current_database() AS catalog, current_schema() AS schema
1857
- `);
1858
- const row = rows[0];
1859
- if (rows.length !== 1 || typeof row?.catalog !== "string" || typeof row.schema !== "string") {
1979
+ `,
1980
+ namespaceRow
1981
+ );
1982
+ const row = rows.at(0);
1983
+ if (rows.length !== 1 || !row) {
1860
1984
  throw new Error(
1861
1985
  "DuckDB current namespace returned an unknown row shape."
1862
1986
  );
@@ -1869,42 +1993,55 @@ function validateScopeOption(name, values) {
1869
1993
  throw new Error(`DuckDB ${name} must contain at least one non-empty name.`);
1870
1994
  }
1871
1995
  }
1872
- function rowsFromResult(result) {
1873
- if (Array.isArray(result)) return result;
1874
- if (result && typeof result === "object") {
1875
- const candidate = result;
1876
- if (Array.isArray(candidate.data)) return candidate.data;
1877
- if (Array.isArray(candidate.rows)) return candidate.rows;
1878
- }
1879
- throw new Error(
1880
- "DuckDB execute() must return an array of rows, { data: rows }, or { rows }."
1881
- );
1882
- }
1883
1996
 
1884
1997
  // packages/text2sql/src/lib/adapters/duckdb/index.ts
1885
1998
  function tables(config = {}) {
1886
- return (adapter) => new DuckDBTableGrounding(adapter, config);
1999
+ return groundingFor(
2000
+ DuckDB,
2001
+ (adapter) => new DuckDBTableGrounding(adapter, config)
2002
+ );
1887
2003
  }
1888
2004
  function info(config = {}) {
1889
- return (adapter) => new DuckDBInfoGrounding(adapter, config);
2005
+ return groundingFor(
2006
+ DuckDB,
2007
+ (adapter) => new DuckDBInfoGrounding(adapter, config)
2008
+ );
1890
2009
  }
1891
2010
  function views(config = {}) {
1892
- return (adapter) => new DuckDBViewGrounding(adapter, config);
2011
+ return groundingFor(
2012
+ DuckDB,
2013
+ (adapter) => new DuckDBViewGrounding(adapter, config)
2014
+ );
1893
2015
  }
1894
2016
  function constraints(config = {}) {
1895
- return (adapter) => new DuckDBConstraintGrounding(adapter, config);
2017
+ return groundingFor(
2018
+ DuckDB,
2019
+ (adapter) => new DuckDBConstraintGrounding(adapter, config)
2020
+ );
1896
2021
  }
1897
2022
  function indexes(config = {}) {
1898
- return (adapter) => new DuckDBIndexesGrounding(adapter, config);
2023
+ return groundingFor(
2024
+ DuckDB,
2025
+ (adapter) => new DuckDBIndexesGrounding(adapter, config)
2026
+ );
1899
2027
  }
1900
2028
  function rowCount(config = {}) {
1901
- return (adapter) => new DuckDBRowCountGrounding(adapter, config);
2029
+ return groundingFor(
2030
+ DuckDB,
2031
+ (adapter) => new DuckDBRowCountGrounding(adapter, config)
2032
+ );
1902
2033
  }
1903
2034
  function columnStats(config = {}) {
1904
- return (adapter) => new DuckDBColumnStatsGrounding(adapter, config);
2035
+ return groundingFor(
2036
+ DuckDB,
2037
+ (adapter) => new DuckDBColumnStatsGrounding(adapter, config)
2038
+ );
1905
2039
  }
1906
2040
  function columnValues(config = {}) {
1907
- return (adapter) => new DuckDBColumnValuesGrounding(adapter, config);
2041
+ return groundingFor(
2042
+ DuckDB,
2043
+ (adapter) => new DuckDBColumnValuesGrounding(adapter, config)
2044
+ );
1908
2045
  }
1909
2046
  var duckdb_default = {
1910
2047
  tables,