@google-cloud/kcmd 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +266 -2
  3. package/build/ts/kcmd/cli.js +19578 -0
  4. package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
  5. package/build/ts/kcmd/gcp/alloydb.js +70 -0
  6. package/build/ts/kcmd/gcp/api.d.ts +29 -0
  7. package/build/ts/kcmd/gcp/api.js +162 -0
  8. package/build/ts/kcmd/gcp/biglake.d.ts +16 -0
  9. package/build/ts/kcmd/gcp/biglake.js +50 -0
  10. package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
  11. package/build/ts/kcmd/gcp/bigquery.js +134 -0
  12. package/build/ts/kcmd/gcp/context.d.ts +10 -0
  13. package/build/ts/kcmd/gcp/context.js +74 -0
  14. package/build/ts/kcmd/gcp/crm.d.ts +13 -0
  15. package/build/ts/kcmd/gcp/crm.js +91 -0
  16. package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
  17. package/build/ts/kcmd/gcp/dataplex.js +406 -0
  18. package/build/ts/kcmd/gcp/index.d.ts +3 -0
  19. package/build/ts/kcmd/gcp/index.js +21 -0
  20. package/build/ts/kcmd/gcp/spanner.d.ts +63 -0
  21. package/build/ts/kcmd/gcp/spanner.js +123 -0
  22. package/build/ts/kcmd/index.d.ts +7 -0
  23. package/build/ts/kcmd/index.js +46 -0
  24. package/build/ts/kcmd/layout.d.ts +16 -0
  25. package/build/ts/kcmd/layout.js +31 -0
  26. package/build/ts/kcmd/layouts/documents.d.ts +24 -0
  27. package/build/ts/kcmd/layouts/documents.js +264 -0
  28. package/build/ts/kcmd/layouts/okf.d.ts +31 -0
  29. package/build/ts/kcmd/layouts/okf.js +344 -0
  30. package/build/ts/kcmd/layouts/semantic-model.d.ts +61 -0
  31. package/build/ts/kcmd/layouts/semantic-model.js +542 -0
  32. package/build/ts/kcmd/layouts/standard.d.ts +13 -0
  33. package/build/ts/kcmd/layouts/standard.js +108 -0
  34. package/build/ts/kcmd/manifest.d.ts +34 -0
  35. package/build/ts/kcmd/manifest.js +262 -0
  36. package/build/ts/kcmd/metadata.d.ts +25 -0
  37. package/build/ts/kcmd/metadata.js +8 -0
  38. package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
  39. package/build/ts/kcmd/okf/catalog_residue.js +124 -0
  40. package/build/ts/kcmd/okf/concept.d.ts +14 -0
  41. package/build/ts/kcmd/okf/concept.js +104 -0
  42. package/build/ts/kcmd/okf/concept_to_entry.d.ts +8 -0
  43. package/build/ts/kcmd/okf/concept_to_entry.js +254 -0
  44. package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
  45. package/build/ts/kcmd/okf/init_okf_workspace.js +115 -0
  46. package/build/ts/kcmd/okf/links.d.ts +23 -0
  47. package/build/ts/kcmd/okf/links.js +468 -0
  48. package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
  49. package/build/ts/kcmd/okf/okf_metadata.js +82 -0
  50. package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
  51. package/build/ts/kcmd/semantic/bigquery.js +525 -0
  52. package/build/ts/kcmd/semantic/binding.d.ts +3 -0
  53. package/build/ts/kcmd/semantic/binding.js +97 -0
  54. package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
  55. package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
  56. package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
  57. package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
  58. package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
  59. package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
  60. package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
  61. package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
  62. package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
  63. package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
  64. package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
  65. package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
  66. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +129 -0
  67. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1396 -0
  68. package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
  69. package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
  70. package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
  71. package/build/ts/kcmd/semantic/deployment_target.js +379 -0
  72. package/build/ts/kcmd/semantic/graph_ddl.d.ts +34 -0
  73. package/build/ts/kcmd/semantic/graph_ddl.js +563 -0
  74. package/build/ts/kcmd/semantic/import_model.d.ts +50 -0
  75. package/build/ts/kcmd/semantic/import_model.js +1275 -0
  76. package/build/ts/kcmd/semantic/ir.d.ts +589 -0
  77. package/build/ts/kcmd/semantic/ir.js +247 -0
  78. package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
  79. package/build/ts/kcmd/semantic/kc_actions.js +531 -0
  80. package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
  81. package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
  82. package/build/ts/kcmd/semantic/kc_converter.d.ts +31 -0
  83. package/build/ts/kcmd/semantic/kc_converter.js +824 -0
  84. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +70 -0
  85. package/build/ts/kcmd/semantic/kc_custom_types.js +828 -0
  86. package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
  87. package/build/ts/kcmd/semantic/kc_entries.js +284 -0
  88. package/build/ts/kcmd/semantic/kc_ids.d.ts +103 -0
  89. package/build/ts/kcmd/semantic/kc_ids.js +191 -0
  90. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +32 -0
  91. package/build/ts/kcmd/semantic/knowledge_catalog.js +1043 -0
  92. package/build/ts/kcmd/semantic/loader.d.ts +262 -0
  93. package/build/ts/kcmd/semantic/loader.js +3440 -0
  94. package/build/ts/kcmd/semantic/names.d.ts +10 -0
  95. package/build/ts/kcmd/semantic/names.js +85 -0
  96. package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
  97. package/build/ts/kcmd/semantic/osi_converter.js +911 -0
  98. package/build/ts/kcmd/semantic/parameters.d.ts +18 -0
  99. package/build/ts/kcmd/semantic/parameters.js +152 -0
  100. package/build/ts/kcmd/semantic/pull_kc.d.ts +14 -0
  101. package/build/ts/kcmd/semantic/pull_kc.js +346 -0
  102. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +54 -0
  103. package/build/ts/kcmd/semantic/resolve_inheritance.js +330 -0
  104. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +250 -0
  105. package/build/ts/kcmd/semantic/resolve_profiles.js +1822 -0
  106. package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
  107. package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
  108. package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
  109. package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
  110. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +94 -0
  111. package/build/ts/kcmd/semantic/runtime/runtime.js +318 -0
  112. package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
  113. package/build/ts/kcmd/semantic/runtime/store.js +344 -0
  114. package/build/ts/kcmd/semantic/skills.d.ts +110 -0
  115. package/build/ts/kcmd/semantic/skills.js +697 -0
  116. package/build/ts/kcmd/semantic/source_dialect.d.ts +10 -0
  117. package/build/ts/kcmd/semantic/source_dialect.js +261 -0
  118. package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
  119. package/build/ts/kcmd/semantic/source_entries.js +282 -0
  120. package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
  121. package/build/ts/kcmd/semantic/spanner.js +60 -0
  122. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +17 -0
  123. package/build/ts/kcmd/semantic/sql_expr_utils.js +221 -0
  124. package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
  125. package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
  126. package/build/ts/kcmd/semantic/sql_parser.d.ts +44 -0
  127. package/build/ts/kcmd/semantic/sql_parser.js +378 -0
  128. package/build/ts/kcmd/semantic/transpile.d.ts +74 -0
  129. package/build/ts/kcmd/semantic/transpile.js +563 -0
  130. package/build/ts/kcmd/semantic/validate.d.ts +35 -0
  131. package/build/ts/kcmd/semantic/validate.js +1899 -0
  132. package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
  133. package/build/ts/kcmd/semantic/yaml_options.js +58 -0
  134. package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
  135. package/build/ts/kcmd/semantic/yaml_update.js +782 -0
  136. package/build/ts/kcmd/snapshot.d.ts +33 -0
  137. package/build/ts/kcmd/snapshot.js +379 -0
  138. package/build/ts/kcmd/source.d.ts +31 -0
  139. package/build/ts/kcmd/source.js +119 -0
  140. package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
  141. package/build/ts/kcmd/sources/bq-dataset.js +131 -0
  142. package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
  143. package/build/ts/kcmd/sources/entrygroup.js +83 -0
  144. package/build/ts/kcmd/sources/kb.d.ts +21 -0
  145. package/build/ts/kcmd/sources/kb.js +82 -0
  146. package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
  147. package/build/ts/kcmd/sources/semantic-model.js +48 -0
  148. package/build/ts/kcmd/sync.d.ts +35 -0
  149. package/build/ts/kcmd/sync.js +697 -0
  150. package/package.json +62 -4
@@ -0,0 +1,378 @@
1
+ "use strict";
2
+ // The SQL engine kcmd reads and translates expressions with: the
3
+ // @polyglot-sql/sdk Rust/WASM engine, bundled into the tool.
4
+ //
5
+ // Loading the engine is asynchronous and happens once per process, through
6
+ // `loadSqlEngine`. Parsing is synchronous after that, so the validators that
7
+ // read expressions stay synchronous: a command that runs them loads the engine
8
+ // first, and so does a test.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.loadSqlEngine = loadSqlEngine;
11
+ exports.sqlColumns = sqlColumns;
12
+ exports.sqlExpressionTree = sqlExpressionTree;
13
+ exports.holdsNoSql = holdsNoSql;
14
+ exports.withoutComments = withoutComments;
15
+ const node_fs_1 = require("node:fs");
16
+ // The engine, initialized once and shared process-wide. Under Bun, the
17
+ // Rust/WASM blob is embedded into the standalone binary via `import(...,
18
+ // {with: {type: 'file'}})` (statically analyzable, so bundled); under Node.js,
19
+ // it is resolved from `node_modules` via `require.resolve(...)`. In both
20
+ // cases the bytes are handed to `init()` via the `/manual` entry so the same
21
+ // loader works inside a `bun --compile` binary and a standard npm install.
22
+ let enginePromise;
23
+ let loadedEngine;
24
+ function loadSqlEngine() {
25
+ if (!enginePromise) {
26
+ enginePromise = (async () => {
27
+ const wasmPath = 'Bun' in globalThis ?
28
+ (await import('@polyglot-sql/sdk/polyglot_sql.wasm', { with: { type: 'file' } }))
29
+ .default :
30
+ require.resolve('@polyglot-sql/sdk/polyglot_sql.wasm');
31
+ const engine = await import('@polyglot-sql/sdk/manual');
32
+ await engine.init({ wasmUrl: (0, node_fs_1.readFileSync)(wasmPath) });
33
+ loadedEngine = engine;
34
+ return engine;
35
+ })().catch(err => {
36
+ // Don't cache the failure: a transient init error (e.g. a failed read)
37
+ // must not permanently disable the engine for a long-lived process.
38
+ // Clear the memo so the next call retries, then propagate.
39
+ enginePromise = undefined;
40
+ throw err;
41
+ });
42
+ }
43
+ return enginePromise;
44
+ }
45
+ // The engine's dialect for each kcmd dialect. The engine has no Spanner
46
+ // dialect, so Spanner's GoogleSQL is read as BigQuery's.
47
+ const PARSER_DIALECTS = {
48
+ ANSI_SQL: 'generic',
49
+ BIGQUERY: 'bigquery',
50
+ SPANNER: 'bigquery',
51
+ POSTGRES: 'postgresql',
52
+ ALLOYDB: 'postgresql',
53
+ MYSQL: 'mysql',
54
+ SQLSERVER: 'tsql',
55
+ SNOWFLAKE: 'snowflake',
56
+ DATABRICKS: 'databricks',
57
+ };
58
+ // Every column `expression` reads, in the order written, or undefined when the
59
+ // engine cannot read it as one expression in `dialect`. An empty expression
60
+ // reads none. A struct path reads as
61
+ // its first two parts, so `orders.address.city` reads the column `address`
62
+ // qualified by `orders`. A function namespace such as `NET` in
63
+ // `NET.HOST(orders.url)` is not a column, and the expression a chained call
64
+ // applies to, such as `(orders.name)` in `(orders.name).UPPER()`, is read.
65
+ // Throws when the engine is not loaded.
66
+ function sqlColumns(expression, dialect) {
67
+ const tree = sqlExpressionTree(expression, dialect);
68
+ if (!expression.trim())
69
+ return [];
70
+ if (!tree)
71
+ return undefined;
72
+ const columns = [];
73
+ collectColumns(tree, columns, new Set(), PARSER_DIALECTS[dialect.toUpperCase()] ?? 'generic');
74
+ return columns;
75
+ }
76
+ // The engine's tree of `expression` read as one expression in `dialect`, or
77
+ // undefined when it cannot be read so or is empty. A text that starts with a
78
+ // keyword, such as `Order.amount`, is read again inside parentheses on lines
79
+ // of their own, so a trailing `--` comment stays inside.
80
+ function sqlExpressionTree(expression, dialect) {
81
+ if (!loadedEngine) {
82
+ throw new Error('the SQL parser is not loaded; call loadSqlEngine() first');
83
+ }
84
+ if (!expression.trim())
85
+ return undefined;
86
+ const engineDialect = PARSER_DIALECTS[dialect.toUpperCase()] ?? 'generic';
87
+ const ast = oneExpression(engineDialect, `SELECT ${expression}`) ??
88
+ oneExpression(engineDialect, `SELECT (\n${expression}\n)`, 'paren');
89
+ return ast?.select.expressions[0];
90
+ }
91
+ // The parsed statement when `sql` is a `SELECT` of exactly one expression and
92
+ // nothing else: no alias, no `FROM`, `WHERE`, `DISTINCT` or other clause. The
93
+ // expression is of node kind `kind` when given. Undefined otherwise.
94
+ function oneExpression(dialect, sql, kind) {
95
+ const res = loadedEngine.parse(sql, dialect);
96
+ if (!res.success || res.ast?.length !== 1)
97
+ return undefined;
98
+ const select = res.ast[0]?.select;
99
+ const items = select?.expressions;
100
+ if (!Array.isArray(items) || items.length !== 1)
101
+ return undefined;
102
+ const itemKind = Object.keys(items[0] ?? {})[0];
103
+ if (itemKind === 'alias' || (kind && itemKind !== kind))
104
+ return undefined;
105
+ // Inside the parentheses, `x AS y` reads as an alias too.
106
+ const inner = itemKind === 'paren' ? items[0].paren?.this : undefined;
107
+ if (inner && Object.keys(inner)[0] === 'alias')
108
+ return undefined;
109
+ const isEmpty = (v) => v === null || v === undefined ||
110
+ v === false || (Array.isArray(v) && v.length === 0) ||
111
+ (typeof v === 'object' && Object.keys(v).length === 0);
112
+ for (const [key, value] of Object.entries(select)) {
113
+ if (key !== 'expressions' && !isEmpty(value))
114
+ return undefined;
115
+ }
116
+ return res.ast[0];
117
+ }
118
+ // Functions whose first argument is a date-part unit (such as `DAY` or `MONTH`)
119
+ // that the parser represents as an unquoted bare `column` node in MySQL,
120
+ // Databricks, T-SQL or generic ANSI SQL.
121
+ const LEADING_DATE_PART_FUNCTIONS = new Set([
122
+ 'DATEDIFF',
123
+ 'DATEADD',
124
+ 'DATE_ADD',
125
+ 'DATE_BUCKET',
126
+ 'DATE_DIFF',
127
+ 'TIMESTAMPDIFF',
128
+ 'TIMESTAMPADD',
129
+ ]);
130
+ // In BigQuery (and Spanner), these take a column first, as in
131
+ // `DATE_DIFF(end_col, start_col, DAY)` or `DATE_BUCKET(d, INTERVAL 7 DAY)`, so
132
+ // their first argument is a column even when its name matches a short unit
133
+ // such as `d`.
134
+ const BIGQUERY_TRAILING_UNIT_FUNCTIONS = new Set(['DATE_ADD', 'DATE_BUCKET', 'DATE_DIFF']);
135
+ // BigQuery functions whose last argument is a date part, as in
136
+ // `DATE_TRUNC(d, MONTH)` or `DATE_DIFF(a, b, WEEK(MONDAY))`. Under the `SAFE.`
137
+ // prefix the engine reads that date part as a column.
138
+ const BIGQUERY_DATE_PART_FUNCTIONS = new Set([
139
+ 'DATE_DIFF', 'DATETIME_DIFF', 'TIMESTAMP_DIFF', 'TIME_DIFF', 'DATE_TRUNC',
140
+ 'DATETIME_TRUNC', 'TIMESTAMP_TRUNC', 'TIME_TRUNC', 'LAST_DAY',
141
+ ]);
142
+ const DATE_PART_KEYWORDS = new Set([
143
+ 'YEAR', 'YEARS', 'YYYY', 'YY', 'QUARTER',
144
+ 'QUARTERS', 'QQ', 'Q', 'MONTH', 'MONTHS',
145
+ 'MM', 'M', 'WEEK', 'WEEKS', 'WK',
146
+ 'WW', 'DAYOFYEAR', 'DY', 'DOY', 'DAY',
147
+ 'DAYS', 'DD', 'D', 'DAYOFWEEK', 'DW',
148
+ 'WEEKDAY', 'HOUR', 'HOURS', 'HH', 'MINUTE',
149
+ 'MINUTES', 'MI', 'N', 'SECOND', 'SECONDS',
150
+ 'SS', 'S', 'MILLISECOND', 'MILLISECONDS', 'MS',
151
+ 'MICROSECOND', 'MICROSECONDS', 'US', 'NANOSECOND', 'NANOSECONDS',
152
+ 'NS', 'ISOWEEK', 'ISOYEAR',
153
+ ]);
154
+ function collectColumns(node, out, bound, dialect) {
155
+ const bigQuery = dialect === 'bigquery';
156
+ if (Array.isArray(node)) {
157
+ for (const item of node) {
158
+ collectColumns(item, out, bound, dialect);
159
+ }
160
+ return;
161
+ }
162
+ if (!node || typeof node !== 'object')
163
+ return;
164
+ const obj = node;
165
+ const keys = Object.keys(obj);
166
+ if (keys.length === 1 && keys[0] === 'column' &&
167
+ typeof obj.column?.name?.name === 'string') {
168
+ const qualifier = obj.column.table?.name;
169
+ const name = obj.column.name.name;
170
+ if (typeof qualifier === 'string' ? bound.has(qualifier) : bound.has(name)) {
171
+ return;
172
+ }
173
+ out.push({
174
+ ...(typeof qualifier === 'string' ? { qualifier } : {}),
175
+ name,
176
+ });
177
+ return;
178
+ }
179
+ if (keys.length === 1 && keys[0] === 'dot') {
180
+ // A struct path reads as its first two parts. The engine reads `Time.hour`
181
+ // as a field of a column named `Time`, because `Time` is also a type name,
182
+ // so the first part is still the qualifier.
183
+ let inner = obj.dot;
184
+ while (inner?.this && Object.keys(inner.this).length === 1 &&
185
+ inner.this.dot) {
186
+ inner = inner.this.dot;
187
+ }
188
+ const column = inner?.this?.column;
189
+ if (typeof column?.name?.name === 'string') {
190
+ const qualifier = column.table?.name;
191
+ if (typeof qualifier === 'string') {
192
+ if (!bound.has(qualifier)) {
193
+ out.push({ qualifier, name: column.name.name });
194
+ }
195
+ }
196
+ else if (typeof inner.field?.name === 'string') {
197
+ if (!bound.has(column.name.name)) {
198
+ out.push({ qualifier: column.name.name, name: inner.field.name });
199
+ }
200
+ }
201
+ return;
202
+ }
203
+ collectColumns(obj.dot?.this, out, bound, dialect);
204
+ return;
205
+ }
206
+ if (keys.length === 1 && keys[0] === 'method_call') {
207
+ // In `NET.HOST(x)` and `my_project.my_dataset.my_udf(x)` the part before
208
+ // the dot is a bare name path that names a function namespace, so it is not
209
+ // read. In T-SQL, `orders.geo.STLength()` calls a method of the column
210
+ // `orders.geo`, so a receiver written as a qualified column is read. Any
211
+ // other receiver is the expression a chained call applies to, as in
212
+ // `(orders.name).UPPER()` or `LOWER(orders.name).TRIM()`, and it is.
213
+ const receiver = obj.method_call?.this;
214
+ const columnMethod = dialect === 'tsql' &&
215
+ typeof receiver?.column?.table?.name === 'string';
216
+ if (columnMethod || !isNamePath(receiver)) {
217
+ collectColumns(receiver, out, bound, dialect);
218
+ }
219
+ collectColumns(obj.method_call?.args, out, bound, dialect);
220
+ return;
221
+ }
222
+ if (keys.length === 1 && keys[0] === 'function') {
223
+ const fnName = String(obj.function?.name ?? '').toUpperCase();
224
+ const hasLeadingUnit = LEADING_DATE_PART_FUNCTIONS.has(fnName) &&
225
+ !(bigQuery && BIGQUERY_TRAILING_UNIT_FUNCTIONS.has(fnName));
226
+ const args = Array.isArray(obj.function?.args) ? obj.function.args : [];
227
+ const col0 = args[0]?.column;
228
+ if (hasLeadingUnit && args.length >= 3 && col0 &&
229
+ col0.table == null && col0.name?.quoted === false &&
230
+ typeof col0.name?.name === 'string' &&
231
+ DATE_PART_KEYWORDS.has(col0.name.name.toUpperCase())) {
232
+ for (const arg of args.slice(1)) {
233
+ collectColumns(arg, out, bound, dialect);
234
+ }
235
+ return;
236
+ }
237
+ if (bigQuery && fnName.startsWith('SAFE.') &&
238
+ BIGQUERY_DATE_PART_FUNCTIONS.has(fnName.slice('SAFE.'.length)) &&
239
+ args.length >= 2 && isDatePart(args[args.length - 1])) {
240
+ collectColumns(args.slice(0, -1), out, bound, dialect);
241
+ return;
242
+ }
243
+ }
244
+ if (keys.length === 1 && keys[0] === 'lambda') {
245
+ const next = new Set(bound);
246
+ for (const p of obj.lambda?.parameters ?? []) {
247
+ if (typeof p?.name === 'string')
248
+ next.add(p.name);
249
+ }
250
+ collectColumns(obj.lambda?.body, out, next, dialect);
251
+ return;
252
+ }
253
+ if (keys.length === 1 && keys[0] === 'select') {
254
+ const select = obj.select;
255
+ const next = new Set(bound);
256
+ for (const cte of select?.with?.ctes ?? []) {
257
+ if (typeof cte?.alias?.name === 'string')
258
+ next.add(cte.alias.name);
259
+ collectColumns(cte?.this, out, next, dialect);
260
+ }
261
+ for (const expr of select?.from?.expressions ?? []) {
262
+ collectColumns(expr, out, next, dialect);
263
+ addSourceAliases(expr, next);
264
+ }
265
+ // Inside a subquery with its own `FROM` clause, a bare identifier in the
266
+ // remaining clauses may come from that `FROM` source (e.g. a derived
267
+ // table's column or an unaliased `UNNEST` field), so only qualified
268
+ // references outside `next` are kept.
269
+ const target = select?.from ? [] : out;
270
+ for (const join of select?.joins ?? []) {
271
+ addSourceAliases(join?.this, next);
272
+ collectColumns(join, target, next, dialect);
273
+ }
274
+ for (const item of select?.expressions ?? []) {
275
+ if (typeof item?.alias?.alias?.name === 'string') {
276
+ next.add(item.alias.alias.name);
277
+ }
278
+ }
279
+ for (const [k, v] of Object.entries(select ?? {})) {
280
+ if (k !== 'with' && k !== 'from' && k !== 'joins') {
281
+ collectColumns(v, target, next, dialect);
282
+ }
283
+ }
284
+ if (select?.from) {
285
+ out.push(...target.filter(c => c.qualifier !== undefined));
286
+ }
287
+ return;
288
+ }
289
+ for (const value of Object.values(obj)) {
290
+ collectColumns(value, out, bound, dialect);
291
+ }
292
+ }
293
+ // Whether `node` is a date part written as a bare name, such as `DAY`, or as
294
+ // `WEEK(MONDAY)`.
295
+ function isDatePart(node) {
296
+ const col = node?.column;
297
+ if (col) {
298
+ return col.table == null && col.name?.quoted === false &&
299
+ typeof col.name?.name === 'string' &&
300
+ DATE_PART_KEYWORDS.has(col.name.name.toUpperCase());
301
+ }
302
+ return String(node?.function?.name ?? '').toUpperCase() === 'WEEK';
303
+ }
304
+ function addSourceAliases(source, bound) {
305
+ if (typeof source?.alias?.alias?.name === 'string') {
306
+ bound.add(source.alias.alias.name);
307
+ }
308
+ const inner = source?.alias?.this ?? source;
309
+ for (const kind of ['unnest', 'table', 'subquery']) {
310
+ const node = inner?.[kind];
311
+ if (typeof node?.alias?.name === 'string')
312
+ bound.add(node.alias.name);
313
+ if (typeof node?.offset_alias?.name === 'string') {
314
+ bound.add(node.offset_alias.name);
315
+ }
316
+ }
317
+ }
318
+ // Whether `node` is a name or a dotted run of names, such as `NET` or
319
+ // `my_project.my_dataset`, with no other expression inside it.
320
+ function isNamePath(node) {
321
+ if (!node || typeof node !== 'object')
322
+ return false;
323
+ const obj = node;
324
+ const keys = Object.keys(obj);
325
+ if (keys.length !== 1)
326
+ return false;
327
+ if (keys[0] === 'column')
328
+ return true;
329
+ return keys[0] === 'dot' && isNamePath(obj.dot?.this);
330
+ }
331
+ // The tokens of `expression` in `dialect`, or undefined when the engine cannot
332
+ // tokenize it. Throws when the engine is not loaded.
333
+ function sqlTokens(expression, dialect) {
334
+ if (!loadedEngine) {
335
+ throw new Error('the SQL parser is not loaded; call loadSqlEngine() first');
336
+ }
337
+ const engineDialect = PARSER_DIALECTS[dialect.toUpperCase()] ?? 'generic';
338
+ const res = loadedEngine.tokenize(expression, engineDialect);
339
+ return res.success ? res.tokens ?? [] : undefined;
340
+ }
341
+ // Whether `expression` holds no SQL in `dialect`: it is empty, only whitespace
342
+ // or only comments. False when the engine cannot read it. Throws when the
343
+ // engine is not loaded.
344
+ function holdsNoSql(expression, dialect) {
345
+ return sqlTokens(expression, dialect)?.length === 0;
346
+ }
347
+ // `expression` with its comments removed, as the engine reads them in
348
+ // `dialect`. Comments between two tokens become one space, or nothing when
349
+ // either token is a `.`, so `orders/* c */.amount` reads `orders.amount`.
350
+ // Comments before the first token or after the last are dropped. The rest of
351
+ // the text stays as written. Returns `expression` itself when it has no
352
+ // comment, holds nothing but comments, or the engine cannot read it. Throws
353
+ // when the engine is not loaded.
354
+ function withoutComments(expression, dialect) {
355
+ const tokens = sqlTokens(expression, dialect);
356
+ if (!tokens?.some(t => t.comments.length || t.trailing_comments.length)) {
357
+ return expression;
358
+ }
359
+ const chars = Array.from(expression);
360
+ let out = '';
361
+ let end = 0;
362
+ let previous = '';
363
+ for (const t of tokens) {
364
+ // The text between two tokens is whitespace and comments.
365
+ const between = chars.slice(end, t.span.start).join('');
366
+ const text = chars.slice(t.span.start, t.span.end).join('');
367
+ if (out) {
368
+ if (between.trim() === '')
369
+ out += between;
370
+ else if (previous !== '.' && text !== '.')
371
+ out += ' ';
372
+ }
373
+ out += text;
374
+ previous = text;
375
+ end = t.span.end;
376
+ }
377
+ return out;
378
+ }
@@ -0,0 +1,74 @@
1
+ import { SemanticModel } from './ir';
2
+ export interface TranspileRequest {
3
+ id: string;
4
+ dialect: string;
5
+ expression: string;
6
+ }
7
+ export interface TranspileResponse {
8
+ id: string;
9
+ sql?: string;
10
+ error?: string;
11
+ }
12
+ export type SqlTranspiler = (requests: TranspileRequest[], target: string) => Promise<TranspileResponse[]>;
13
+ export interface TranspileOptions {
14
+ target?: string;
15
+ transpiler?: SqlTranspiler;
16
+ }
17
+ export interface TranspileResult {
18
+ model: SemanticModel;
19
+ warnings: string[];
20
+ }
21
+ /**
22
+ * Returns true if `expression`, read as generic SQL (`ANSI_SQL`), quotes a
23
+ * column, table qualifier, or struct field with double quotes (`"..."`).
24
+ *
25
+ * The `ANSI_SQL` parser marks both `"..."` and `` `...` `` column identifiers
26
+ * with `quoted: true`, whereas `BIGQUERY` marks only `` `...` `` as `quoted:
27
+ * true` and treats `"..."` as a string literal (or fails to parse `."..."`).
28
+ * Comparing how many `quotedNames` the two ASTs hold yields three cases:
29
+ * - `ANSI_SQL > BIGQUERY`: at least one column identifier is double-quoted
30
+ * (e.g., `"orders"."amount"` or `"amount"`).
31
+ * - `ANSI_SQL < BIGQUERY`: `expression` uses BigQuery-only syntax (such as a
32
+ * `# "comment"` line comment in `` `orders`.`col` # "comment" ``) that
33
+ * `BIGQUERY` parses while `ANSI_SQL` fails to parse (`count = 0`).
34
+ * - `ANSI_SQL === BIGQUERY`: any `"` is inside a string literal or comment
35
+ * (e.g., `` `orders`.`note` = 'a "quoted" ` string' ``), and any quoted
36
+ * column identifiers use backticks in both dialects.
37
+ */
38
+ export declare function hasDoubleQuotedColumn(expression: string): boolean;
39
+ /**
40
+ * Returns the names that `expression` double-quotes, each once, for a
41
+ * message: those the `ANSI_SQL` parser marks quoted that `expression` writes
42
+ * in double quotes, else the first name it marks quoted.
43
+ */
44
+ export declare function doubleQuotedNames(expression: string): string[];
45
+ /**
46
+ * Adds a translated `opts.target` dialect entry to every field or metric in
47
+ * `model` whose only texts are in vendor dialects or whose `ANSI_SQL` text
48
+ * double-quotes a column name, returning a cloned model plus diagnostics.
49
+ *
50
+ * The input model is never mutated. When no node needs transpilation, the clone
51
+ * is returned unchanged and the transpiler is not invoked. The
52
+ * qualifier-preservation guard parses the transpiled output with
53
+ * `columnReferences`, which requires the SQL parser to be loaded even when a
54
+ * custom `opts.transpiler` is injected (`loadSqlEngine()` is awaited before
55
+ * running the guard).
56
+ */
57
+ export declare function transpileModel(model: SemanticModel, opts?: TranspileOptions): Promise<TranspileResult>;
58
+ /**
59
+ * Returns true if the default translator reads SQL written in `dialect`
60
+ * (treating an empty string as `ANSI_SQL`).
61
+ *
62
+ * `transpileModel` leaves a text in any other dialect as written, with a
63
+ * warning.
64
+ */
65
+ export declare function transpilesFrom(dialect: string): boolean;
66
+ /**
67
+ * The default {@link SqlTranspiler}: transpiles via the embedded
68
+ * @polyglot-sql/sdk Rust/WASM engine. There is no external runtime dependency
69
+ * -- the WASM blob is bundled into the tool. It never throws: an engine that
70
+ * fails to initialize yields an `error` response for every request, and a
71
+ * per-request parse failure or unsupported dialect yields an `error` for just
72
+ * that request, so the caller degrades to the imported form + warning.
73
+ */
74
+ export declare const polyglotTranspiler: SqlTranspiler;