@google-cloud/kcmd 0.1.0 → 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 (60) hide show
  1. package/README.md +18 -20
  2. package/build/ts/kcmd/cli.js +5842 -5168
  3. package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
  4. package/build/ts/kcmd/gcp/biglake.js +13 -4
  5. package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
  6. package/build/ts/kcmd/gcp/spanner.js +6 -3
  7. package/build/ts/kcmd/layouts/okf.js +49 -7
  8. package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
  9. package/build/ts/kcmd/layouts/semantic-model.js +129 -50
  10. package/build/ts/kcmd/okf/concept.d.ts +1 -1
  11. package/build/ts/kcmd/okf/concept.js +6 -0
  12. package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
  13. package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
  14. package/build/ts/kcmd/okf/init_okf_workspace.js +8 -10
  15. package/build/ts/kcmd/okf/okf_metadata.js +17 -0
  16. package/build/ts/kcmd/semantic/bigquery.js +139 -195
  17. package/build/ts/kcmd/semantic/binding.js +14 -19
  18. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
  19. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +92 -121
  20. package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
  21. package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
  22. package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
  23. package/build/ts/kcmd/semantic/import_model.js +526 -103
  24. package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
  25. package/build/ts/kcmd/semantic/kc_converter.js +20 -5
  26. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
  27. package/build/ts/kcmd/semantic/kc_custom_types.js +16 -93
  28. package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
  29. package/build/ts/kcmd/semantic/kc_ids.js +4 -7
  30. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
  31. package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
  32. package/build/ts/kcmd/semantic/loader.d.ts +7 -11
  33. package/build/ts/kcmd/semantic/loader.js +284 -148
  34. package/build/ts/kcmd/semantic/osi_converter.js +1 -2
  35. package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
  36. package/build/ts/kcmd/semantic/parameters.js +22 -3
  37. package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
  38. package/build/ts/kcmd/semantic/pull_kc.js +38 -112
  39. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
  40. package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
  41. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
  42. package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
  43. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +0 -6
  44. package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
  45. package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
  46. package/build/ts/kcmd/semantic/source_dialect.js +83 -33
  47. package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
  48. package/build/ts/kcmd/semantic/source_entries.js +95 -36
  49. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
  50. package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
  51. package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
  52. package/build/ts/kcmd/semantic/sql_parser.js +22 -0
  53. package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
  54. package/build/ts/kcmd/semantic/transpile.js +79 -61
  55. package/build/ts/kcmd/semantic/validate.d.ts +37 -2
  56. package/build/ts/kcmd/semantic/validate.js +465 -98
  57. package/build/ts/kcmd/snapshot.d.ts +4 -0
  58. package/build/ts/kcmd/snapshot.js +7 -0
  59. package/build/ts/kcmd/sync.js +44 -7
  60. package/package.json +1 -1
@@ -2,87 +2,209 @@
2
2
  // Helpers for reading and rewriting entity-qualified SQL expressions across the
3
3
  // semantic-model layer.
4
4
  //
5
- // Expressions reference columns as `<Entity>.<column>`. The SQL parser
5
+ // An expression names a field of its entity as `<Entity>.<field>`, and a
6
+ // column of its table without a qualifier. The SQL parser
6
7
  // (`columnReferences`, `entitiesReached`, `fieldsReadOn`) reads column and
7
8
  // entity references across dialects for the loader, validator, profile checks,
8
- // pull reader, and transpiler. The graph generators still use the literal-
9
- // aware regex helpers (`referencedEntityNames`, `stripQualifier`).
9
+ // pull reader, and transpiler. The graph generators read them with the parser
10
+ // too: `measureEntities` places a measure, `singleAggregate` finds its
11
+ // aggregate, `withoutQualifier` makes an expression table-local,
12
+ // `qualifiedColumns` finds the sibling fields it names and
13
+ // `withSiblingsSpliced` puts their SQL in their place, and `requalified` moves
14
+ // an inherited expression onto the entity inheriting it.
10
15
  //
11
16
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.SUPPORTED_AGGREGATES = exports.STRING_LITERAL = void 0;
13
- exports.escapeRegExp = escapeRegExp;
14
- exports.blankStringLiterals = blankStringLiterals;
15
- exports.mapOutsideStringLiterals = mapOutsideStringLiterals;
16
- exports.referencedEntityNames = referencedEntityNames;
17
- exports.stripQualifier = stripQualifier;
17
+ exports.SUPPORTED_AGGREGATES = void 0;
18
+ exports.unusedName = unusedName;
19
+ exports.withoutQualifier = withoutQualifier;
20
+ exports.withSiblingsSpliced = withSiblingsSpliced;
21
+ exports.qualifiedColumns = qualifiedColumns;
22
+ exports.selfReferenceMessage = selfReferenceMessage;
23
+ exports.requalified = requalified;
24
+ exports.measureEntities = measureEntities;
18
25
  exports.sqlWithoutComments = sqlWithoutComments;
19
26
  exports.fieldBindingWithoutComments = fieldBindingWithoutComments;
20
27
  exports.columnReferences = columnReferences;
21
28
  exports.columnsForSource = columnsForSource;
22
29
  exports.entitiesReached = entitiesReached;
23
30
  exports.fieldsReadOn = fieldsReadOn;
31
+ exports.singleAggregate = singleAggregate;
24
32
  exports.isSingleAggregate = isSingleAggregate;
25
33
  exports.isColumnName = isColumnName;
26
34
  const ir_1 = require("./ir");
35
+ const sql_identifiers_1 = require("./sql_identifiers");
27
36
  const sql_parser_1 = require("./sql_parser");
28
- // Matches a single- or double-quoted SQL string literal, honoring backslash
29
- // escapes. (Triple-quoted / raw literals are uncommon in these expressions and
30
- // are treated as ordinary text.)
31
- exports.STRING_LITERAL = /'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/g;
32
- // Escapes a string so it can be embedded literally in a RegExp.
33
- function escapeRegExp(s) {
34
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
35
- }
36
- // Replaces string-literal contents with blanks of equal length, so scanning sees
37
- // literal-free text without shifting any offsets.
38
- function blankStringLiterals(expression) {
39
- return expression.replace(exports.STRING_LITERAL, m => ' '.repeat(m.length));
40
- }
41
- // Applies `fn` only to the parts of `expression` that lie outside string
42
- // literals, leaving each literal verbatim.
43
- function mapOutsideStringLiterals(expression, fn) {
37
+ // The dialect a graph generator reads `dialect` in: an ANSI_SQL text, or a
38
+ // text with no dialect, is read as BigQuery.
39
+ function graphReading(dialect) {
40
+ return !dialect || dialect === 'ANSI_SQL' ? 'BIGQUERY' : dialect;
41
+ }
42
+ // Each `<name>.` qualifier that `expression`, read in `dialect`, writes before
43
+ // a column, for a name in `names`, as indexes into the text's tokens, in the
44
+ // order written. A name matches case-sensitively, bare or quoted as the
45
+ // dialect quotes an identifier, and not after a dot, so the second `orders` in
46
+ // the struct path `orders.orders.id` is not one. The SQL parser decides which
47
+ // candidates are qualifiers, so a string literal, a comment or a function
48
+ // namespace such as `NET.` in `NET.HOST(orders.url)` holds none. The parser
49
+ // reads each candidate as a placeholder name, so one it reserves as a keyword,
50
+ // such as `Date` in `Date.days`, still reads as a qualifier. When the parser
51
+ // cannot read the text even so, every candidate is taken as a qualifier.
52
+ // Undefined when the engine cannot tokenize the text.
53
+ function qualifiersIn(expression, names, dialect) {
54
+ const tokens = (0, sql_parser_1.sqlTokens)(expression, dialect);
55
+ if (!tokens)
56
+ return undefined;
57
+ const chars = Array.from(expression);
58
+ const asWritten = (t) => chars.slice(t.span.start, t.span.end).join('');
59
+ const candidates = [];
60
+ tokens.forEach((t, i) => {
61
+ if (names.has(t.text) && tokens[i + 1]?.token_type === 'DOT' &&
62
+ tokens[i - 1]?.token_type !== 'DOT' &&
63
+ (t.token_type === 'QUOTED_IDENTIFIER' || asWritten(t) === t.text)) {
64
+ candidates.push(i);
65
+ }
66
+ });
67
+ if (!candidates.length)
68
+ return { tokens, at: [] };
69
+ // A placeholder stem the text does not hold.
70
+ const stem = unusedName(expression, 'kcmd_qualifier_');
71
+ let text = '';
72
+ let end = 0;
73
+ candidates.forEach((i, k) => {
74
+ text += chars.slice(end, tokens[i].span.start).join('') + stem + k;
75
+ end = tokens[i].span.end;
76
+ });
77
+ text += chars.slice(end).join('');
78
+ const columns = columnReferences(text, dialect);
79
+ const at = !columns ? candidates : candidates.filter((_, k) => columns.some(c => c.qualifier?.toLowerCase() === stem + k));
80
+ return { tokens, at };
81
+ }
82
+ // `base`, a lowercase name, with underscores added until `text` does not hold
83
+ // it in any case.
84
+ function unusedName(text, base) {
85
+ let name = base;
86
+ while (text.toLowerCase().includes(name))
87
+ name += '_';
88
+ return name;
89
+ }
90
+ // `expression` without each `<qualifier>.` it writes before a column (see
91
+ // qualifiersIn), so it reads the columns of the qualifier's own table. The
92
+ // rest of the text stays as written. The text is read in `dialect`; an
93
+ // ANSI_SQL text, or one with no dialect, is read as BigQuery, as the graph
94
+ // generators read it. Returns `expression` itself when it writes no such
95
+ // qualifier or the engine cannot tokenize it.
96
+ function withoutQualifier(expression, qualifier, dialect) {
97
+ return spliced(expression, qualifier, graphReading(dialect), new Map());
98
+ }
99
+ // `expression`, BigQuery SQL on `qualifier`'s table, as withoutQualifier gives
100
+ // it, except that each `<qualifier>.<field>` whose field `siblings` holds is
101
+ // replaced, whole, by the SQL it maps to, as given. The graph generators put
102
+ // the SQL of each sibling field an expression names in its place so (see
103
+ // graph_ddl.fieldSql).
104
+ function withSiblingsSpliced(expression, qualifier, siblings) {
105
+ return spliced(expression, qualifier, 'BIGQUERY', siblings);
106
+ }
107
+ // `expression`, read in `dialect`, without each `<qualifier>.` it writes
108
+ // before a column (see qualifiersIn), and with each `<qualifier>.<column>`
109
+ // whose column `replacements` holds replaced, whole, by the SQL it maps to.
110
+ function spliced(expression, qualifier, dialect, replacements) {
111
+ const read = qualifiersIn(expression, new Set([qualifier]), dialect);
112
+ if (!read?.at.length)
113
+ return expression;
114
+ const { tokens, at } = read;
115
+ const chars = Array.from(expression);
44
116
  let out = '';
45
- let last = 0;
46
- for (const m of expression.matchAll(exports.STRING_LITERAL)) {
47
- out += fn(expression.slice(last, m.index));
48
- out += m[0];
49
- last = m.index + m[0].length;
117
+ let end = 0;
118
+ for (const i of at) {
119
+ out += chars.slice(end, tokens[i].span.start).join('');
120
+ const column = tokens[i + 2];
121
+ const replacement = column && replacements.get(nameOf(column, chars));
122
+ if (replacement !== undefined) {
123
+ out += replacement;
124
+ end = column.span.end;
125
+ }
126
+ else {
127
+ // The qualifier, its dot, and what lies between the dot and the column.
128
+ end = column?.span.start ?? tokens[i + 1].span.end;
129
+ }
50
130
  }
51
- out += fn(expression.slice(last));
52
- return out;
53
- }
54
- // Builds a regex matching an `<entity>.` qualifier, including the BigQuery
55
- // backtick-quoted form (`` `entity`. ``). A negative lookbehind keeps the name
56
- // from matching inside a larger identifier (e.g. `customer_orders.`) or after a
57
- // dot in a struct path (e.g. `orders.customer.id`), and the optional backticks
58
- // let it match whether or not the identifier is quoted.
59
- function entityQualifier(name, flags = '') {
60
- return new RegExp(`(?<![\\w\`.])\`?${escapeRegExp(name)}\`?\\.`, flags);
61
- }
62
- // Returns the entity names whose `<name>.` qualifier appears in an expression,
63
- // in the order they first appear, ignoring text inside string literals.
64
- // Matching is case-sensitive (the emitter's behavior).
65
- function referencedEntityNames(expression, entityNames) {
66
- const scannable = blankStringLiterals(expression);
67
- const hits = [];
68
- for (const name of entityNames) {
69
- const m = entityQualifier(name).exec(scannable);
70
- if (m)
71
- hits.push({ name, at: m.index });
131
+ return out + chars.slice(end).join('');
132
+ }
133
+ // The column written after each `<qualifier>.` that `expression` writes before
134
+ // a column (see qualifiersIn), without its quotes, in the order written, so
135
+ // ``orders.a + `orders`.`b`.c`` gives `a` and `b`. The text is read in
136
+ // `dialect`, as withoutQualifier reads it, so a qualifier the parser reserves,
137
+ // such as `Date` in `Date.days`, gives its column too. Undefined when the
138
+ // engine cannot tokenize the text.
139
+ function qualifiedColumns(expression, qualifier, dialect) {
140
+ const read = qualifiersIn(expression, new Set([qualifier]), graphReading(dialect));
141
+ if (!read)
142
+ return undefined;
143
+ const chars = Array.from(expression);
144
+ return read.at.filter(i => read.tokens[i + 2])
145
+ .map(i => nameOf(read.tokens[i + 2], chars));
146
+ }
147
+ // The name token `t` of the text whose characters are `chars` gives, as
148
+ // written, without its quotes.
149
+ function nameOf(t, chars) {
150
+ return t.token_type === 'QUOTED_IDENTIFIER' ?
151
+ t.text :
152
+ chars.slice(t.span.start, t.span.end).join('');
153
+ }
154
+ // The error for a field of `owner` that names itself. `cycle` starts at that
155
+ // field and runs through the fields it names back to it, and `where` follows
156
+ // the field's name, as ` in model 'm' (doc)` does. The graph renders a field
157
+ // that names a sibling field from that sibling's own SQL (see
158
+ // graph_ddl.fieldSql), so a field that reaches itself has none.
159
+ function selfReferenceMessage(owner, cycle, where = '') {
160
+ const [field, ...through] = cycle;
161
+ return `field '${owner}.${field}'${where} names itself` +
162
+ (through.length ?
163
+ ` through ${through.map(n => `'${owner}.${n}'`).join(', ')}` :
164
+ '') +
165
+ `; a field expression may name other fields of its entity, but not ` +
166
+ `itself, directly or through other fields` +
167
+ (through.length ? '' :
168
+ `; to read the column of that name, write it ` +
169
+ `without the qualifier: '${(0, sql_identifiers_1.quoteIfReserved)(field)}'`);
170
+ }
171
+ // `expression` with each `<from>.` qualifier it writes before a column (see
172
+ // qualifiersIn) renamed `<to>.`, quoted as it was written. The rest of the
173
+ // text stays as written. The text is read in `dialect`, as withoutQualifier
174
+ // reads it. Returns `expression` itself when it writes no such qualifier or
175
+ // the engine cannot tokenize it.
176
+ function requalified(expression, from, to, dialect) {
177
+ const read = qualifiersIn(expression, new Set([from]), graphReading(dialect));
178
+ if (!read?.at.length)
179
+ return expression;
180
+ const { tokens, at } = read;
181
+ const chars = Array.from(expression);
182
+ let out = '';
183
+ let end = 0;
184
+ for (const i of at) {
185
+ const { start, end: after } = tokens[i].span;
186
+ out += chars.slice(end, start).join('') +
187
+ chars.slice(start, after).join('').replace(from, () => to);
188
+ end = after;
72
189
  }
73
- return hits.sort((a, b) => a.at - b.at).map(h => h.name);
190
+ return out + chars.slice(end).join('');
74
191
  }
75
- // Removes the `<entity>.` qualifier (bare or backtick-quoted) so an expression
76
- // references table-local columns, without touching text inside string literals.
77
- function stripQualifier(expression, entity) {
78
- const re = entityQualifier(entity, 'g');
79
- return mapOutsideStringLiterals(expression, seg => seg.replace(re, ''));
192
+ // The entities from `entityNames` whose qualifier a metric's BigQuery SQL
193
+ // `expression` writes before a column (see qualifiersIn), deduplicated in the
194
+ // order written, or else `entity`, the metric's anchor, when given. The
195
+ // BigQuery generator places the metric's measure on the entity this gives
196
+ // when it gives one, and a BigQuery push rejects a metric it gives several or
197
+ // none for (see validate.ts).
198
+ function measureEntities(expression, entityNames, entity) {
199
+ const read = qualifiersIn(expression, new Set(entityNames), 'BIGQUERY');
200
+ const reached = [...new Set(read?.at.map(i => read.tokens[i].text) ?? [])];
201
+ return reached.length || !entity ? reached : [entity];
80
202
  }
81
203
  // `text`, written in `dialect`, with its comments removed. An ANSI_SQL text, or
82
204
  // a text with no dialect, is read as BigQuery, as the loader and the graph
83
205
  // generators read it.
84
206
  function sqlWithoutComments(text, dialect) {
85
- return (0, sql_parser_1.withoutComments)(text, !dialect || dialect === 'ANSI_SQL' ? 'BIGQUERY' : dialect);
207
+ return (0, sql_parser_1.withoutComments)(text, graphReading(dialect));
86
208
  }
87
209
  // `field`'s binding, as fieldBinding gives it, with its comments removed.
88
210
  // Undefined when the field is unbound. The target text is read in
@@ -124,8 +246,11 @@ function columnReferences(expression, dialect) {
124
246
  // an `ANSI_SQL` entry as its own SQL. When the sources do not imply a single
125
247
  // dialect, the generic parser reads the text, and any bare identifier that
126
248
  // BigQuery's parser does not also read as a bare column (such as a `MONTH`
127
- // date part or a double-quoted string literal) is filtered out.
249
+ // date part or a double-quoted string literal) is filtered out. When their
250
+ // dialect is null, which no lookup read, an `ANSI_SQL` text is not read.
128
251
  function columnsForSource(text, dialect, impliedDialect) {
252
+ if (dialect === 'ANSI_SQL' && impliedDialect === null)
253
+ return undefined;
129
254
  if (dialect === 'ANSI_SQL' && impliedDialect) {
130
255
  return columnReferences(text, impliedDialect);
131
256
  }
@@ -165,15 +290,18 @@ function fieldsReadOn(columns, entityName) {
165
290
  // The aggregates BigQuery accepts in a MEASURE, its complete list. BigQuery
166
291
  // rejects any other there, such as COUNTIF or APPROX_COUNT_DISTINCT.
167
292
  exports.SUPPORTED_AGGREGATES = ['SUM', 'AVG', 'COUNT', 'MIN', 'MAX'];
168
- // Whether `expression`, read in `dialect`, is one of SUPPORTED_AGGREGATES over
169
- // one operand, or COUNT(*), with at most DISTINCT or ALL. The operand holds no
170
- // other aggregate, window or subquery.
171
- function isSingleAggregate(expression, dialect) {
293
+ // When `expression`, read in `dialect`, is one of SUPPORTED_AGGREGATES over
294
+ // one operand, or COUNT(*), with at most DISTINCT or ALL, and the operand holds
295
+ // no other aggregate, window or subquery: the function's name as written, the
296
+ // operand as written without the parentheses around it (so
297
+ // `COUNT(DISTINCT(x))` gives `x`), and whether the call is DISTINCT. COUNT(*)
298
+ // gives the operand `*`. Undefined otherwise.
299
+ function singleAggregate(expression, dialect) {
172
300
  const node = unwrapped((0, sql_parser_1.sqlExpressionTree)(expression, dialect));
173
301
  const kind = node && Object.keys(node).length === 1 ? Object.keys(node)[0] :
174
302
  undefined;
175
303
  if (!kind || !exports.SUPPORTED_AGGREGATES.includes(kind.toUpperCase())) {
176
- return false;
304
+ return undefined;
177
305
  }
178
306
  const call = node[kind];
179
307
  const star = kind === 'count' && call.star === true && !call.this;
@@ -181,12 +309,57 @@ function isSingleAggregate(expression, dialect) {
181
309
  if (!star &&
182
310
  (!operand || operand.star || operand.tuple ||
183
311
  nestsAggregateOrSubquery(operand))) {
184
- return false;
312
+ return undefined;
185
313
  }
186
314
  // The parser keeps the call's name in `name` or `original_name`.
187
315
  const known = new Set(['this', 'star', 'distinct', 'name', 'original_name']);
188
- return Object.entries(call).every(([key, v]) => known.has(key) || v === null || v === undefined ||
189
- v === false || (Array.isArray(v) && v.length === 0));
316
+ if (!Object.entries(call).every(([key, v]) => known.has(key) || v === null || v === undefined ||
317
+ v === false || (Array.isArray(v) && v.length === 0))) {
318
+ return undefined;
319
+ }
320
+ // The call as written: its name, `(`, DISTINCT or ALL if written, the
321
+ // operand and `)`, inside any parentheses around the whole call.
322
+ const tokens = unparenthesized((0, sql_parser_1.sqlTokens)(expression, dialect) ?? []);
323
+ if (tokens.length < 4 || tokens[1].token_type !== 'L_PAREN' ||
324
+ tokens[tokens.length - 1].token_type !== 'R_PAREN') {
325
+ return undefined;
326
+ }
327
+ let inner = tokens.slice(2, -1);
328
+ if (inner[0]?.token_type === 'DISTINCT' || inner[0]?.token_type === 'ALL') {
329
+ inner = inner.slice(1);
330
+ }
331
+ inner = unparenthesized(inner);
332
+ if (!inner.length)
333
+ return undefined;
334
+ const chars = Array.from(expression);
335
+ return {
336
+ fn: tokens[0].text,
337
+ operand: chars.slice(inner[0].span.start, inner[inner.length - 1].span.end)
338
+ .join(''),
339
+ distinct: call.distinct === true,
340
+ };
341
+ }
342
+ // Whether `expression`, read in `dialect`, is a single aggregate (see
343
+ // singleAggregate), the form a BigQuery measure takes.
344
+ function isSingleAggregate(expression, dialect) {
345
+ return singleAggregate(expression, dialect) !== undefined;
346
+ }
347
+ // `tokens` without the parentheses around all of them.
348
+ function unparenthesized(tokens) {
349
+ while (tokens.length >= 2 && tokens[0].token_type === 'L_PAREN') {
350
+ let depth = 0;
351
+ let closesAt = -1;
352
+ for (let i = 0; i < tokens.length && closesAt < 0; i++) {
353
+ if (tokens[i].token_type === 'L_PAREN')
354
+ depth++;
355
+ if (tokens[i].token_type === 'R_PAREN' && --depth === 0)
356
+ closesAt = i;
357
+ }
358
+ if (closesAt !== tokens.length - 1)
359
+ break;
360
+ tokens = tokens.slice(1, -1);
361
+ }
362
+ return tokens;
190
363
  }
191
364
  // The parser's tree `node` without the parentheses and comments around it.
192
365
  function unwrapped(node) {
@@ -23,7 +23,8 @@ export interface PolyglotEngine {
23
23
  error?: string;
24
24
  };
25
25
  }
26
- interface SqlToken {
26
+ export interface SqlToken {
27
+ token_type: string;
27
28
  text: string;
28
29
  span: {
29
30
  start: number;
@@ -33,12 +34,14 @@ interface SqlToken {
33
34
  trailing_comments: string[];
34
35
  }
35
36
  export declare function loadSqlEngine(): Promise<PolyglotEngine>;
37
+ export declare function loadedSqlEngine(): PolyglotEngine | undefined;
36
38
  export interface SqlColumn {
37
39
  qualifier?: string;
38
40
  name: string;
39
41
  }
40
42
  export declare function sqlColumns(expression: string, dialect: string): SqlColumn[] | undefined;
43
+ export declare function soleColumn(expression: string, dialect: string): SqlColumn | undefined;
41
44
  export declare function sqlExpressionTree(expression: string, dialect: string): any;
45
+ export declare function sqlTokens(expression: string, dialect: string): SqlToken[] | undefined;
42
46
  export declare function holdsNoSql(expression: string, dialect: string): boolean;
43
47
  export declare function withoutComments(expression: string, dialect: string): string;
44
- export {};
@@ -8,8 +8,11 @@
8
8
  // first, and so does a test.
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.loadSqlEngine = loadSqlEngine;
11
+ exports.loadedSqlEngine = loadedSqlEngine;
11
12
  exports.sqlColumns = sqlColumns;
13
+ exports.soleColumn = soleColumn;
12
14
  exports.sqlExpressionTree = sqlExpressionTree;
15
+ exports.sqlTokens = sqlTokens;
13
16
  exports.holdsNoSql = holdsNoSql;
14
17
  exports.withoutComments = withoutComments;
15
18
  const node_fs_1 = require("node:fs");
@@ -42,6 +45,10 @@ function loadSqlEngine() {
42
45
  }
43
46
  return enginePromise;
44
47
  }
48
+ // The engine loadSqlEngine has loaded, or undefined before it has.
49
+ function loadedSqlEngine() {
50
+ return loadedEngine;
51
+ }
45
52
  // The engine's dialect for each kcmd dialect. The engine has no Spanner
46
53
  // dialect, so Spanner's GoogleSQL is read as BigQuery's.
47
54
  const PARSER_DIALECTS = {
@@ -73,6 +80,21 @@ function sqlColumns(expression, dialect) {
73
80
  collectColumns(tree, columns, new Set(), PARSER_DIALECTS[dialect.toUpperCase()] ?? 'generic');
74
81
  return columns;
75
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
+ }
76
98
  // The engine's tree of `expression` read as one expression in `dialect`, or
77
99
  // undefined when it cannot be read so or is empty. A text that starts with a
78
100
  // keyword, such as `Order.amount`, is read again inside parentheses on lines
@@ -1,4 +1,4 @@
1
- import { SemanticModel } from './ir';
1
+ import { Field, ProfileSpec, SemanticModel } from './ir';
2
2
  export interface TranspileRequest {
3
3
  id: string;
4
4
  dialect: string;
@@ -13,11 +13,13 @@ export type SqlTranspiler = (requests: TranspileRequest[], target: string) => Pr
13
13
  export interface TranspileOptions {
14
14
  target?: string;
15
15
  transpiler?: SqlTranspiler;
16
+ translatesProfile?: (profile: ProfileSpec, model: SemanticModel) => boolean;
16
17
  }
17
18
  export interface TranspileResult {
18
19
  model: SemanticModel;
19
20
  warnings: string[];
20
21
  }
22
+ type TranspileNode = Pick<Field, 'expression' | 'dialects' | 'stringForm' | 'importedExpression' | 'importedDialect'>;
21
23
  /**
22
24
  * Returns true if `expression`, read as generic SQL (`ANSI_SQL`), quotes a
23
25
  * column, table qualifier, or struct field with double quotes (`"..."`).
@@ -43,9 +45,10 @@ export declare function hasDoubleQuotedColumn(expression: string): boolean;
43
45
  */
44
46
  export declare function doubleQuotedNames(expression: string): string[];
45
47
  /**
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.
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.
49
52
  *
50
53
  * The input model is never mutated. When no node needs transpilation, the clone
51
54
  * is returned unchanged and the transpiler is not invoked. The
@@ -56,13 +59,11 @@ export declare function doubleQuotedNames(expression: string): string[];
56
59
  */
57
60
  export declare function transpileModel(model: SemanticModel, opts?: TranspileOptions): Promise<TranspileResult>;
58
61
  /**
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.
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.
64
65
  */
65
- export declare function transpilesFrom(dialect: string): boolean;
66
+ export declare function translatesToBigQuery(node: TranspileNode, entityNames: string[]): boolean;
66
67
  /**
67
68
  * The default {@link SqlTranspiler}: transpiles via the embedded
68
69
  * @polyglot-sql/sdk Rust/WASM engine. There is no external runtime dependency
@@ -72,3 +73,4 @@ export declare function transpilesFrom(dialect: string): boolean;
72
73
  * that request, so the caller degrades to the imported form + warning.
73
74
  */
74
75
  export declare const polyglotTranspiler: SqlTranspiler;
76
+ export {};