@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.
- package/LICENSE +203 -0
- package/README.md +264 -2
- package/build/ts/kcmd/cli.js +20252 -0
- package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
- package/build/ts/kcmd/gcp/alloydb.js +70 -0
- package/build/ts/kcmd/gcp/api.d.ts +29 -0
- package/build/ts/kcmd/gcp/api.js +162 -0
- package/build/ts/kcmd/gcp/biglake.d.ts +18 -0
- package/build/ts/kcmd/gcp/biglake.js +59 -0
- package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
- package/build/ts/kcmd/gcp/bigquery.js +134 -0
- package/build/ts/kcmd/gcp/context.d.ts +10 -0
- package/build/ts/kcmd/gcp/context.js +74 -0
- package/build/ts/kcmd/gcp/crm.d.ts +13 -0
- package/build/ts/kcmd/gcp/crm.js +91 -0
- package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
- package/build/ts/kcmd/gcp/dataplex.js +406 -0
- package/build/ts/kcmd/gcp/index.d.ts +3 -0
- package/build/ts/kcmd/gcp/index.js +21 -0
- package/build/ts/kcmd/gcp/spanner.d.ts +73 -0
- package/build/ts/kcmd/gcp/spanner.js +126 -0
- package/build/ts/kcmd/index.d.ts +7 -0
- package/build/ts/kcmd/index.js +46 -0
- package/build/ts/kcmd/layout.d.ts +16 -0
- package/build/ts/kcmd/layout.js +31 -0
- package/build/ts/kcmd/layouts/documents.d.ts +24 -0
- package/build/ts/kcmd/layouts/documents.js +264 -0
- package/build/ts/kcmd/layouts/okf.d.ts +31 -0
- package/build/ts/kcmd/layouts/okf.js +386 -0
- package/build/ts/kcmd/layouts/semantic-model.d.ts +74 -0
- package/build/ts/kcmd/layouts/semantic-model.js +621 -0
- package/build/ts/kcmd/layouts/standard.d.ts +13 -0
- package/build/ts/kcmd/layouts/standard.js +108 -0
- package/build/ts/kcmd/manifest.d.ts +34 -0
- package/build/ts/kcmd/manifest.js +262 -0
- package/build/ts/kcmd/metadata.d.ts +25 -0
- package/build/ts/kcmd/metadata.js +8 -0
- package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
- package/build/ts/kcmd/okf/catalog_residue.js +124 -0
- package/build/ts/kcmd/okf/concept.d.ts +14 -0
- package/build/ts/kcmd/okf/concept.js +110 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +10 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +281 -0
- package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
- package/build/ts/kcmd/okf/init_okf_workspace.js +113 -0
- package/build/ts/kcmd/okf/links.d.ts +23 -0
- package/build/ts/kcmd/okf/links.js +468 -0
- package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
- package/build/ts/kcmd/okf/okf_metadata.js +99 -0
- package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
- package/build/ts/kcmd/semantic/bigquery.js +469 -0
- package/build/ts/kcmd/semantic/binding.d.ts +3 -0
- package/build/ts/kcmd/semantic/binding.js +92 -0
- package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
- package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
- package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
- package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +104 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1367 -0
- package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
- package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
- package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
- package/build/ts/kcmd/semantic/deployment_target.js +379 -0
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +44 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +656 -0
- package/build/ts/kcmd/semantic/import_model.d.ts +72 -0
- package/build/ts/kcmd/semantic/import_model.js +1698 -0
- package/build/ts/kcmd/semantic/ir.d.ts +589 -0
- package/build/ts/kcmd/semantic/ir.js +247 -0
- package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
- package/build/ts/kcmd/semantic/kc_actions.js +531 -0
- package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
- package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
- package/build/ts/kcmd/semantic/kc_converter.d.ts +37 -0
- package/build/ts/kcmd/semantic/kc_converter.js +839 -0
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +63 -0
- package/build/ts/kcmd/semantic/kc_custom_types.js +751 -0
- package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
- package/build/ts/kcmd/semantic/kc_entries.js +284 -0
- package/build/ts/kcmd/semantic/kc_ids.d.ts +102 -0
- package/build/ts/kcmd/semantic/kc_ids.js +188 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +29 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.js +960 -0
- package/build/ts/kcmd/semantic/loader.d.ts +258 -0
- package/build/ts/kcmd/semantic/loader.js +3576 -0
- package/build/ts/kcmd/semantic/names.d.ts +10 -0
- package/build/ts/kcmd/semantic/names.js +85 -0
- package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
- package/build/ts/kcmd/semantic/osi_converter.js +910 -0
- package/build/ts/kcmd/semantic/parameters.d.ts +19 -0
- package/build/ts/kcmd/semantic/parameters.js +171 -0
- package/build/ts/kcmd/semantic/pull_kc.d.ts +10 -0
- package/build/ts/kcmd/semantic/pull_kc.js +272 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +59 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.js +346 -0
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +291 -0
- package/build/ts/kcmd/semantic/resolve_profiles.js +2003 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
- package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
- package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +88 -0
- package/build/ts/kcmd/semantic/runtime/runtime.js +301 -0
- package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
- package/build/ts/kcmd/semantic/runtime/store.js +344 -0
- package/build/ts/kcmd/semantic/skills.d.ts +110 -0
- package/build/ts/kcmd/semantic/skills.js +697 -0
- package/build/ts/kcmd/semantic/source_dialect.d.ts +17 -0
- package/build/ts/kcmd/semantic/source_dialect.js +311 -0
- package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
- package/build/ts/kcmd/semantic/source_entries.js +341 -0
- package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
- package/build/ts/kcmd/semantic/spanner.js +60 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +23 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.js +394 -0
- package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
- package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
- package/build/ts/kcmd/semantic/sql_parser.d.ts +47 -0
- package/build/ts/kcmd/semantic/sql_parser.js +400 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +76 -0
- package/build/ts/kcmd/semantic/transpile.js +581 -0
- package/build/ts/kcmd/semantic/validate.d.ts +70 -0
- package/build/ts/kcmd/semantic/validate.js +2266 -0
- package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
- package/build/ts/kcmd/semantic/yaml_options.js +58 -0
- package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
- package/build/ts/kcmd/semantic/yaml_update.js +782 -0
- package/build/ts/kcmd/snapshot.d.ts +37 -0
- package/build/ts/kcmd/snapshot.js +386 -0
- package/build/ts/kcmd/source.d.ts +31 -0
- package/build/ts/kcmd/source.js +119 -0
- package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
- package/build/ts/kcmd/sources/bq-dataset.js +131 -0
- package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
- package/build/ts/kcmd/sources/entrygroup.js +83 -0
- package/build/ts/kcmd/sources/kb.d.ts +21 -0
- package/build/ts/kcmd/sources/kb.js +82 -0
- package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
- package/build/ts/kcmd/sources/semantic-model.js +48 -0
- package/build/ts/kcmd/sync.d.ts +35 -0
- package/build/ts/kcmd/sync.js +734 -0
- package/package.json +62 -4
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Helpers for reading and rewriting entity-qualified SQL expressions across the
|
|
3
|
+
// semantic-model layer.
|
|
4
|
+
//
|
|
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
|
|
7
|
+
// (`columnReferences`, `entitiesReached`, `fieldsReadOn`) reads column and
|
|
8
|
+
// entity references across dialects for the loader, validator, profile checks,
|
|
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.
|
|
15
|
+
//
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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;
|
|
25
|
+
exports.sqlWithoutComments = sqlWithoutComments;
|
|
26
|
+
exports.fieldBindingWithoutComments = fieldBindingWithoutComments;
|
|
27
|
+
exports.columnReferences = columnReferences;
|
|
28
|
+
exports.columnsForSource = columnsForSource;
|
|
29
|
+
exports.entitiesReached = entitiesReached;
|
|
30
|
+
exports.fieldsReadOn = fieldsReadOn;
|
|
31
|
+
exports.singleAggregate = singleAggregate;
|
|
32
|
+
exports.isSingleAggregate = isSingleAggregate;
|
|
33
|
+
exports.isColumnName = isColumnName;
|
|
34
|
+
const ir_1 = require("./ir");
|
|
35
|
+
const sql_identifiers_1 = require("./sql_identifiers");
|
|
36
|
+
const sql_parser_1 = require("./sql_parser");
|
|
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);
|
|
116
|
+
let out = '';
|
|
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
|
+
}
|
|
130
|
+
}
|
|
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;
|
|
189
|
+
}
|
|
190
|
+
return out + chars.slice(end).join('');
|
|
191
|
+
}
|
|
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];
|
|
202
|
+
}
|
|
203
|
+
// `text`, written in `dialect`, with its comments removed. An ANSI_SQL text, or
|
|
204
|
+
// a text with no dialect, is read as BigQuery, as the loader and the graph
|
|
205
|
+
// generators read it.
|
|
206
|
+
function sqlWithoutComments(text, dialect) {
|
|
207
|
+
return (0, sql_parser_1.withoutComments)(text, graphReading(dialect));
|
|
208
|
+
}
|
|
209
|
+
// `field`'s binding, as fieldBinding gives it, with its comments removed.
|
|
210
|
+
// Undefined when the field is unbound. The target text is read in
|
|
211
|
+
// `targetDialect`, BigQuery unless given. The parser reads Spanner SQL as
|
|
212
|
+
// BigQuery. Imported vendor SQL, the binding only when there is no target
|
|
213
|
+
// text, is read in its imported dialect, so it keeps what that dialect does
|
|
214
|
+
// not read as a comment.
|
|
215
|
+
function fieldBindingWithoutComments(field, targetDialect) {
|
|
216
|
+
const text = (0, ir_1.fieldBinding)(field);
|
|
217
|
+
if (text === undefined)
|
|
218
|
+
return undefined;
|
|
219
|
+
return sqlWithoutComments(text, field.expression === undefined ? field.importedDialect : targetDialect);
|
|
220
|
+
}
|
|
221
|
+
// Results of columnReferences by dialect and text, so the checks one push
|
|
222
|
+
// runs read each text once. When full, the oldest entry goes.
|
|
223
|
+
const MAX_COLUMN_CACHE_SIZE = 65536;
|
|
224
|
+
const COLUMN_CACHE = new Map();
|
|
225
|
+
// Every column an expression reads, with the qualifier written before it, in
|
|
226
|
+
// the order written, or undefined when the SQL parser cannot read the
|
|
227
|
+
// expression in `dialect`, one of the dialect names a dialect list uses. The
|
|
228
|
+
// parser handles string literals, comments and each dialect's quoting, so
|
|
229
|
+
// `"orders"."amount"` in a PostgreSQL text reads the column `amount` of
|
|
230
|
+
// `orders`, while in BigQuery the same text is not SQL and gives undefined. A struct path reads
|
|
231
|
+
// as its first two parts: `orders.shipping_address.city` reads
|
|
232
|
+
// `shipping_address` of `orders`.
|
|
233
|
+
function columnReferences(expression, dialect) {
|
|
234
|
+
const key = `${dialect.toUpperCase()}\0${expression}`;
|
|
235
|
+
if (COLUMN_CACHE.has(key))
|
|
236
|
+
return COLUMN_CACHE.get(key);
|
|
237
|
+
const cols = (0, sql_parser_1.sqlColumns)(expression, dialect);
|
|
238
|
+
if (COLUMN_CACHE.size >= MAX_COLUMN_CACHE_SIZE) {
|
|
239
|
+
COLUMN_CACHE.delete(COLUMN_CACHE.keys().next().value);
|
|
240
|
+
}
|
|
241
|
+
COLUMN_CACHE.set(key, cols);
|
|
242
|
+
return cols;
|
|
243
|
+
}
|
|
244
|
+
// Reads the columns of `text` in `dialect`. An `ANSI_SQL` text is read in the
|
|
245
|
+
// dialect the model's sources imply (`impliedDialect`), since the database runs
|
|
246
|
+
// an `ANSI_SQL` entry as its own SQL. When the sources do not imply a single
|
|
247
|
+
// dialect, the generic parser reads the text, and any bare identifier that
|
|
248
|
+
// BigQuery's parser does not also read as a bare column (such as a `MONTH`
|
|
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.
|
|
251
|
+
function columnsForSource(text, dialect, impliedDialect) {
|
|
252
|
+
if (dialect === 'ANSI_SQL' && impliedDialect === null)
|
|
253
|
+
return undefined;
|
|
254
|
+
if (dialect === 'ANSI_SQL' && impliedDialect) {
|
|
255
|
+
return columnReferences(text, impliedDialect);
|
|
256
|
+
}
|
|
257
|
+
const cols = columnReferences(text, dialect);
|
|
258
|
+
if (!cols || dialect !== 'ANSI_SQL')
|
|
259
|
+
return cols;
|
|
260
|
+
const bqCols = columnReferences(text, 'BIGQUERY');
|
|
261
|
+
if (!bqCols)
|
|
262
|
+
return cols;
|
|
263
|
+
const bqBare = new Set(bqCols.filter(c => c.qualifier === undefined).map(c => c.name));
|
|
264
|
+
return cols.filter(c => c.qualifier !== undefined || bqBare.has(c.name));
|
|
265
|
+
}
|
|
266
|
+
// The entities from `entityNames` that `columns` read, deduplicated in
|
|
267
|
+
// first-seen order. Matching is case-sensitive.
|
|
268
|
+
function entitiesReached(columns, entityNames) {
|
|
269
|
+
const known = entityNames instanceof Set ? entityNames : new Set(entityNames);
|
|
270
|
+
const reached = [];
|
|
271
|
+
for (const c of columns) {
|
|
272
|
+
if (c.qualifier && known.has(c.qualifier) &&
|
|
273
|
+
!reached.includes(c.qualifier)) {
|
|
274
|
+
reached.push(c.qualifier);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return reached;
|
|
278
|
+
}
|
|
279
|
+
// The fields `columns` read on `entityName`, written `entityName.field`,
|
|
280
|
+
// deduplicated in first-seen order. Matching is case-sensitive.
|
|
281
|
+
function fieldsReadOn(columns, entityName) {
|
|
282
|
+
const fields = [];
|
|
283
|
+
for (const c of columns) {
|
|
284
|
+
if (c.qualifier === entityName && !fields.includes(c.name)) {
|
|
285
|
+
fields.push(c.name);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return fields;
|
|
289
|
+
}
|
|
290
|
+
// The aggregates BigQuery accepts in a MEASURE, its complete list. BigQuery
|
|
291
|
+
// rejects any other there, such as COUNTIF or APPROX_COUNT_DISTINCT.
|
|
292
|
+
exports.SUPPORTED_AGGREGATES = ['SUM', 'AVG', 'COUNT', 'MIN', 'MAX'];
|
|
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) {
|
|
300
|
+
const node = unwrapped((0, sql_parser_1.sqlExpressionTree)(expression, dialect));
|
|
301
|
+
const kind = node && Object.keys(node).length === 1 ? Object.keys(node)[0] :
|
|
302
|
+
undefined;
|
|
303
|
+
if (!kind || !exports.SUPPORTED_AGGREGATES.includes(kind.toUpperCase())) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const call = node[kind];
|
|
307
|
+
const star = kind === 'count' && call.star === true && !call.this;
|
|
308
|
+
const operand = unwrapped(call.this);
|
|
309
|
+
if (!star &&
|
|
310
|
+
(!operand || operand.star || operand.tuple ||
|
|
311
|
+
nestsAggregateOrSubquery(operand))) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
// The parser keeps the call's name in `name` or `original_name`.
|
|
315
|
+
const known = new Set(['this', 'star', 'distinct', 'name', 'original_name']);
|
|
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;
|
|
363
|
+
}
|
|
364
|
+
// The parser's tree `node` without the parentheses and comments around it.
|
|
365
|
+
function unwrapped(node) {
|
|
366
|
+
while (node?.paren || node?.annotated) {
|
|
367
|
+
node = (node.paren ?? node.annotated).this;
|
|
368
|
+
}
|
|
369
|
+
return node;
|
|
370
|
+
}
|
|
371
|
+
// Whether the parser's tree `node` holds an aggregate, a window or a subquery
|
|
372
|
+
// at any depth. The parser gives an aggregate call `distinct` and `filter`
|
|
373
|
+
// keys, or the kind `aggregate_function`.
|
|
374
|
+
function nestsAggregateOrSubquery(node) {
|
|
375
|
+
if (Array.isArray(node))
|
|
376
|
+
return node.some(nestsAggregateOrSubquery);
|
|
377
|
+
if (node === null || typeof node !== 'object')
|
|
378
|
+
return false;
|
|
379
|
+
return Object.entries(node).some(([kind, value]) => kind === 'aggregate_function' ||
|
|
380
|
+
kind === 'window_function' || kind === 'subquery' ||
|
|
381
|
+
kind === 'select' ||
|
|
382
|
+
(value !== null && typeof value === 'object' && 'distinct' in value &&
|
|
383
|
+
'filter' in value) ||
|
|
384
|
+
nestsAggregateOrSubquery(value));
|
|
385
|
+
}
|
|
386
|
+
// Whether a key or join column is a physical column name rather than an
|
|
387
|
+
// expression. A value that starts and ends with a backtick, with none between,
|
|
388
|
+
// passes whatever it holds. An unquoted value may not contain whitespace,
|
|
389
|
+
// parentheses, a dot, a comma, a backtick or a SQL operator. Nothing else about
|
|
390
|
+
// the character set is checked; the database rejects a bad name.
|
|
391
|
+
function isColumnName(value) {
|
|
392
|
+
return typeof value === 'string' &&
|
|
393
|
+
(/^`[^`]+`$/.test(value) || /^[^\s().+\-*/%=<>!|&^~,`]+$/.test(value));
|
|
394
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function isSimpleIdentifier(s: string): boolean;
|
|
2
|
+
export declare function isReservedKeyword(name: string): boolean;
|
|
3
|
+
export declare function quoteIdentifier(name: string): string;
|
|
4
|
+
export declare function quoteIfReserved(name: string): string;
|
|
5
|
+
export declare function referencedParameters(text: string): string[];
|
|
6
|
+
export declare function leadingDmlVerb(sql: string): string;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Quoting for GoogleSQL identifiers emitted into generated graph DDL.
|
|
3
|
+
//
|
|
4
|
+
// The graph generator (./graph_ddl) lowers logical names -- entity and
|
|
5
|
+
// relationship labels, property names, key columns -- into identifier
|
|
6
|
+
// positions of a `CREATE OR REPLACE PROPERTY GRAPH` statement. A name that is a
|
|
7
|
+
// GoogleSQL RESERVED KEYWORD (e.g. an entity named `Order`) is a valid model
|
|
8
|
+
// name but, emitted bare as `... AS Order`, makes the DDL a syntax error
|
|
9
|
+
// ("Unexpected keyword ORDER"). Backticking it (`... AS `Order``) is always
|
|
10
|
+
// safe, so these helpers quote a name exactly when it would otherwise be
|
|
11
|
+
// misread. Both BigQuery and Spanner Graph share the GoogleSQL grammar, so one
|
|
12
|
+
// reserved set serves both.
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.isSimpleIdentifier = isSimpleIdentifier;
|
|
15
|
+
exports.isReservedKeyword = isReservedKeyword;
|
|
16
|
+
exports.quoteIdentifier = quoteIdentifier;
|
|
17
|
+
exports.quoteIfReserved = quoteIfReserved;
|
|
18
|
+
exports.referencedParameters = referencedParameters;
|
|
19
|
+
exports.leadingDmlVerb = leadingDmlVerb;
|
|
20
|
+
// The GoogleSQL reserved keywords. A reserved keyword may not appear as an
|
|
21
|
+
// identifier unless quoted. Non-reserved keywords are deliberately absent: they
|
|
22
|
+
// are legal bare identifiers, so quoting them would churn existing output for no
|
|
23
|
+
// reason. That absence extends to the graph-DDL CONTEXTUAL keywords (KEY, VALUE,
|
|
24
|
+
// SOURCE, DESTINATION, LABEL, NODE, EDGE, PROPERTIES, MEASURE): they are
|
|
25
|
+
// keywords only inside a CREATE PROPERTY GRAPH clause, and BigQuery accepts them
|
|
26
|
+
// bare in every identifier position -- alias, property, KEY/SOURCE
|
|
27
|
+
// KEY/DESTINATION KEY column, and REFERENCES label (verified live 2026-08-30).
|
|
28
|
+
// Kept uppercase; lookups uppercase first.
|
|
29
|
+
//
|
|
30
|
+
// Source of truth: the GoogleSQL reserved-keyword lists published for BigQuery
|
|
31
|
+
// and Spanner (their lexical-structure docs). This set is their UNION so one
|
|
32
|
+
// table serves both engines: the two lists differ only in QUALIFY (BigQuery
|
|
33
|
+
// reserves it; Spanner does not, and over-quoting it on Spanner is harmless),
|
|
34
|
+
// and both reserve GRAPH_TABLE. NOTE: the @polyglot-sql/sdk transpiler is
|
|
35
|
+
// deliberately NOT the oracle -- its reserved set diverges from BigQuery's in
|
|
36
|
+
// both directions (misses EXTRACT, over-quotes user/view/column), so delegating
|
|
37
|
+
// would emit incorrect DDL. See sql_identifiers.test.ts.
|
|
38
|
+
const RESERVED_KEYWORDS = new Set([
|
|
39
|
+
'ALL',
|
|
40
|
+
'AND',
|
|
41
|
+
'ANY',
|
|
42
|
+
'ARRAY',
|
|
43
|
+
'AS',
|
|
44
|
+
'ASC',
|
|
45
|
+
'ASSERT_ROWS_MODIFIED',
|
|
46
|
+
'AT',
|
|
47
|
+
'BETWEEN',
|
|
48
|
+
'BY',
|
|
49
|
+
'CASE',
|
|
50
|
+
'CAST',
|
|
51
|
+
'COLLATE',
|
|
52
|
+
'CONTAINS',
|
|
53
|
+
'CREATE',
|
|
54
|
+
'CROSS',
|
|
55
|
+
'CUBE',
|
|
56
|
+
'CURRENT',
|
|
57
|
+
'DEFAULT',
|
|
58
|
+
'DEFINE',
|
|
59
|
+
'DESC',
|
|
60
|
+
'DISTINCT',
|
|
61
|
+
'ELSE',
|
|
62
|
+
'END',
|
|
63
|
+
'ENUM',
|
|
64
|
+
'ESCAPE',
|
|
65
|
+
'EXCEPT',
|
|
66
|
+
'EXCLUDE',
|
|
67
|
+
'EXISTS',
|
|
68
|
+
'EXTRACT',
|
|
69
|
+
'FALSE',
|
|
70
|
+
'FETCH',
|
|
71
|
+
'FOLLOWING',
|
|
72
|
+
'FOR',
|
|
73
|
+
'FROM',
|
|
74
|
+
'FULL',
|
|
75
|
+
'GRAPH_TABLE',
|
|
76
|
+
'GROUP',
|
|
77
|
+
'GROUPING',
|
|
78
|
+
'GROUPS',
|
|
79
|
+
'HASH',
|
|
80
|
+
'HAVING',
|
|
81
|
+
'IF',
|
|
82
|
+
'IGNORE',
|
|
83
|
+
'IN',
|
|
84
|
+
'INNER',
|
|
85
|
+
'INTERSECT',
|
|
86
|
+
'INTERVAL',
|
|
87
|
+
'INTO',
|
|
88
|
+
'IS',
|
|
89
|
+
'JOIN',
|
|
90
|
+
'LATERAL',
|
|
91
|
+
'LEFT',
|
|
92
|
+
'LIKE',
|
|
93
|
+
'LIMIT',
|
|
94
|
+
'LOOKUP',
|
|
95
|
+
'MERGE',
|
|
96
|
+
'NATURAL',
|
|
97
|
+
'NEW',
|
|
98
|
+
'NO',
|
|
99
|
+
'NOT',
|
|
100
|
+
'NULL',
|
|
101
|
+
'NULLS',
|
|
102
|
+
'OF',
|
|
103
|
+
'ON',
|
|
104
|
+
'OR',
|
|
105
|
+
'ORDER',
|
|
106
|
+
'OUTER',
|
|
107
|
+
'OVER',
|
|
108
|
+
'PARTITION',
|
|
109
|
+
'PRECEDING',
|
|
110
|
+
'PROTO',
|
|
111
|
+
'QUALIFY',
|
|
112
|
+
'RANGE',
|
|
113
|
+
'RECURSIVE',
|
|
114
|
+
'RESPECT',
|
|
115
|
+
'RIGHT',
|
|
116
|
+
'ROLLUP',
|
|
117
|
+
'ROWS',
|
|
118
|
+
'SELECT',
|
|
119
|
+
'SET',
|
|
120
|
+
'SOME',
|
|
121
|
+
'STRUCT',
|
|
122
|
+
'TABLESAMPLE',
|
|
123
|
+
'THEN',
|
|
124
|
+
'TO',
|
|
125
|
+
'TREAT',
|
|
126
|
+
'TRUE',
|
|
127
|
+
'UNBOUNDED',
|
|
128
|
+
'UNION',
|
|
129
|
+
'UNNEST',
|
|
130
|
+
'USING',
|
|
131
|
+
'WHEN',
|
|
132
|
+
'WHERE',
|
|
133
|
+
'WINDOW',
|
|
134
|
+
'WITH',
|
|
135
|
+
'WITHIN',
|
|
136
|
+
]);
|
|
137
|
+
// A bare (unquoted) GoogleSQL identifier: a letter or underscore followed by
|
|
138
|
+
// letters, digits, or underscores. Anything else (a hyphen, a dot, a leading
|
|
139
|
+
// digit) must be quoted to appear as an identifier.
|
|
140
|
+
function isSimpleIdentifier(s) {
|
|
141
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(s);
|
|
142
|
+
}
|
|
143
|
+
// Whether `name` is a GoogleSQL reserved keyword (case-insensitive), and so
|
|
144
|
+
// cannot appear as a bare identifier.
|
|
145
|
+
function isReservedKeyword(name) {
|
|
146
|
+
return RESERVED_KEYWORDS.has(name.toUpperCase());
|
|
147
|
+
}
|
|
148
|
+
// Backticks `name`, escaping any embedded backtick.
|
|
149
|
+
function backtick(name) {
|
|
150
|
+
return `\`${name.replace(/`/g, '\\`')}\``;
|
|
151
|
+
}
|
|
152
|
+
// Quotes a name that would otherwise be MISREAD as a bare identifier: a simple
|
|
153
|
+
// identifier is left bare unless it is a reserved keyword; anything non-simple
|
|
154
|
+
// (a hyphen, a dot, a leading digit) is backticked. Use for a full identifier
|
|
155
|
+
// token -- a table or graph name.
|
|
156
|
+
function quoteIdentifier(name) {
|
|
157
|
+
if (isSimpleIdentifier(name)) {
|
|
158
|
+
return isReservedKeyword(name) ? backtick(name) : name;
|
|
159
|
+
}
|
|
160
|
+
return backtick(name);
|
|
161
|
+
}
|
|
162
|
+
// Quotes ONLY a simple identifier that collides with a reserved keyword, and
|
|
163
|
+
// passes everything else through unchanged. Use at identifier positions whose
|
|
164
|
+
// input is expected to be a simple name already (an alias, a label reference, a
|
|
165
|
+
// key column): a non-simple value there is a separate error the generators
|
|
166
|
+
// detect and warn about, so this must not rewrite it. A non-reserved name is
|
|
167
|
+
// returned byte-for-byte, so existing output is unaffected.
|
|
168
|
+
function quoteIfReserved(name) {
|
|
169
|
+
return isSimpleIdentifier(name) && isReservedKeyword(name) ? backtick(name) :
|
|
170
|
+
name;
|
|
171
|
+
}
|
|
172
|
+
// The distinct `@name` references in a SQL statement, ignoring the ones inside
|
|
173
|
+
// a single-quoted string so a literal containing an '@' is not read as a
|
|
174
|
+
// binding. Used to check that every parameter a statement reads is one the
|
|
175
|
+
// caller will bind, and to bind only the parameters a statement actually reads.
|
|
176
|
+
function referencedParameters(text) {
|
|
177
|
+
const withoutStrings = text.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
178
|
+
const names = new Set();
|
|
179
|
+
for (const m of withoutStrings.matchAll(/@([A-Za-z_]\w*)/g))
|
|
180
|
+
names.add(m[1]);
|
|
181
|
+
return [...names];
|
|
182
|
+
}
|
|
183
|
+
// The DML verb a statement leads with, or '' when none can be read.
|
|
184
|
+
//
|
|
185
|
+
// Reading the first six characters is not enough: a statement opening with a
|
|
186
|
+
// `--` comment, or with a `WITH` clause ahead of its UPDATE, reports a verb
|
|
187
|
+
// that is not the statement's. So this walks the text instead, skipping what
|
|
188
|
+
// cannot hold the verb -- comments, quoted strings, and anything nested in
|
|
189
|
+
// parentheses -- and returns the first DML keyword left standing at the top
|
|
190
|
+
// level. For `WITH x AS (SELECT ...) UPDATE ...` that is the UPDATE, because
|
|
191
|
+
// the CTE body is inside parentheses; for `UPDATE t SET note = 'DELETE'` it is
|
|
192
|
+
// the UPDATE, because the literal is not read as a keyword.
|
|
193
|
+
//
|
|
194
|
+
// BOTH line-comment forms are skipped. GoogleSQL takes `#` as well as `--`, and
|
|
195
|
+
// action DML runs against Spanner, so `# credit the account` ahead of an INSERT
|
|
196
|
+
// would otherwise hand back whatever verb the prose happened to use.
|
|
197
|
+
//
|
|
198
|
+
// It lives here, rather than beside the one caller that refuses a zero-row
|
|
199
|
+
// write, because validation reads the same statements to decide what a model
|
|
200
|
+
// may PUBLISH. Two readers meant the library accepted an authored `sql`
|
|
201
|
+
// executor -- a leading comment, a CTE -- that `kcmd push` and `kcmd action
|
|
202
|
+
// run` then refused before the runtime ever saw it. The verb SET stays wider
|
|
203
|
+
// than what an executor may declare: MERGE is read so a caller can be told a
|
|
204
|
+
// MERGE matched nothing, and rejected at publish time by SQL_EXECUTOR_VERBS,
|
|
205
|
+
// which does not list it.
|
|
206
|
+
//
|
|
207
|
+
// WHAT A MISREAD COSTS runs one way only, and it is worth knowing which. Since
|
|
208
|
+
// noRowMatched refuses everything except a recognized INSERT, failing to read a
|
|
209
|
+
// verb cannot hide a write that did nothing -- it can only refuse one that was
|
|
210
|
+
// fine. The expensive direction is therefore a real INSERT this misses, and the
|
|
211
|
+
// cheap direction is anything else it cannot parse. That is deliberate: a false
|
|
212
|
+
// refusal is a failed run someone looks at, and a missed refusal is a caller
|
|
213
|
+
// told its write landed when it did not.
|
|
214
|
+
//
|
|
215
|
+
// This is a scanner, not a parser, and the repo does bundle a real one
|
|
216
|
+
// (`@polyglot-sql/sdk`, used by transpile.ts and sql_identifiers.ts). It is not
|
|
217
|
+
// used here because it has no Spanner dialect and action DML targets Spanner
|
|
218
|
+
// and AlloyDB, so it would have to parse Spanner statements as something else
|
|
219
|
+
// and would reject valid ones. Given which direction a misread now falls, a
|
|
220
|
+
// scanner that recognizes a leading INSERT is enough; if that stops being true,
|
|
221
|
+
// the parser is the thing to reach for rather than more cases here.
|
|
222
|
+
function leadingDmlVerb(sql) {
|
|
223
|
+
const verbs = new Set(['INSERT', 'UPDATE', 'DELETE', 'MERGE']);
|
|
224
|
+
let depth = 0;
|
|
225
|
+
for (let i = 0; i < sql.length; i++) {
|
|
226
|
+
const c = sql[i];
|
|
227
|
+
if ((c === '-' && sql[i + 1] === '-') || c === '#') {
|
|
228
|
+
const end = sql.indexOf('\n', i);
|
|
229
|
+
if (end < 0)
|
|
230
|
+
break;
|
|
231
|
+
i = end;
|
|
232
|
+
}
|
|
233
|
+
else if (c === '/' && sql[i + 1] === '*') {
|
|
234
|
+
const end = sql.indexOf('*/', i + 2);
|
|
235
|
+
if (end < 0)
|
|
236
|
+
break;
|
|
237
|
+
i = end + 1;
|
|
238
|
+
}
|
|
239
|
+
else if (c === '\'' || c === '"' || c === '`') {
|
|
240
|
+
for (i++; i < sql.length; i++) {
|
|
241
|
+
if (sql[i] === '\\') {
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
else if (sql[i] === c) {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (c === '(') {
|
|
250
|
+
depth++;
|
|
251
|
+
}
|
|
252
|
+
else if (c === ')') {
|
|
253
|
+
if (depth > 0)
|
|
254
|
+
depth--;
|
|
255
|
+
}
|
|
256
|
+
else if (depth === 0 && /[A-Za-z_]/.test(c)) {
|
|
257
|
+
let j = i;
|
|
258
|
+
while (j < sql.length && /[A-Za-z0-9_]/.test(sql[j]))
|
|
259
|
+
j++;
|
|
260
|
+
const word = sql.slice(i, j).toUpperCase();
|
|
261
|
+
if (verbs.has(word))
|
|
262
|
+
return word;
|
|
263
|
+
i = j - 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return '';
|
|
267
|
+
}
|