@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.
- package/README.md +18 -20
- package/build/ts/kcmd/cli.js +5842 -5168
- package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
- package/build/ts/kcmd/gcp/biglake.js +13 -4
- package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
- package/build/ts/kcmd/gcp/spanner.js +6 -3
- package/build/ts/kcmd/layouts/okf.js +49 -7
- package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
- package/build/ts/kcmd/layouts/semantic-model.js +129 -50
- package/build/ts/kcmd/okf/concept.d.ts +1 -1
- package/build/ts/kcmd/okf/concept.js +6 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
- package/build/ts/kcmd/okf/init_okf_workspace.js +8 -10
- package/build/ts/kcmd/okf/okf_metadata.js +17 -0
- package/build/ts/kcmd/semantic/bigquery.js +139 -195
- package/build/ts/kcmd/semantic/binding.js +14 -19
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +92 -121
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
- package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
- package/build/ts/kcmd/semantic/import_model.js +526 -103
- package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
- package/build/ts/kcmd/semantic/kc_converter.js +20 -5
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
- package/build/ts/kcmd/semantic/kc_custom_types.js +16 -93
- package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
- package/build/ts/kcmd/semantic/kc_ids.js +4 -7
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
- package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
- package/build/ts/kcmd/semantic/loader.d.ts +7 -11
- package/build/ts/kcmd/semantic/loader.js +284 -148
- package/build/ts/kcmd/semantic/osi_converter.js +1 -2
- package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
- package/build/ts/kcmd/semantic/parameters.js +22 -3
- package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
- package/build/ts/kcmd/semantic/pull_kc.js +38 -112
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
- package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
- package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +0 -6
- package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
- package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
- package/build/ts/kcmd/semantic/source_dialect.js +83 -33
- package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
- package/build/ts/kcmd/semantic/source_entries.js +95 -36
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
- package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
- package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
- package/build/ts/kcmd/semantic/sql_parser.js +22 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
- package/build/ts/kcmd/semantic/transpile.js +79 -61
- package/build/ts/kcmd/semantic/validate.d.ts +37 -2
- package/build/ts/kcmd/semantic/validate.js +465 -98
- package/build/ts/kcmd/snapshot.d.ts +4 -0
- package/build/ts/kcmd/snapshot.js +7 -0
- package/build/ts/kcmd/sync.js +44 -7
- 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
|
-
//
|
|
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
|
|
9
|
-
//
|
|
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 =
|
|
13
|
-
exports.
|
|
14
|
-
exports.
|
|
15
|
-
exports.
|
|
16
|
-
exports.
|
|
17
|
-
exports.
|
|
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
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
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
|
|
46
|
-
for (const
|
|
47
|
-
out +=
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
190
|
+
return out + chars.slice(end).join('');
|
|
74
191
|
}
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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,
|
|
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
|
-
//
|
|
169
|
-
// one operand, or COUNT(*), with at most DISTINCT or ALL
|
|
170
|
-
// other aggregate, window or subquery
|
|
171
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
47
|
-
* `model` whose only texts are in vendor dialects or
|
|
48
|
-
* double-quotes a column name, returning a cloned model
|
|
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
|
-
*
|
|
60
|
-
*
|
|
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
|
|
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 {};
|