@google-cloud/kcmd 0.0.0-stage → 0.2.0

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