@google-cloud/kcmd 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +203 -0
- package/README.md +266 -2
- package/build/ts/kcmd/cli.js +19578 -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 +16 -0
- package/build/ts/kcmd/gcp/biglake.js +50 -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 +63 -0
- package/build/ts/kcmd/gcp/spanner.js +123 -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 +344 -0
- package/build/ts/kcmd/layouts/semantic-model.d.ts +61 -0
- package/build/ts/kcmd/layouts/semantic-model.js +542 -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 +104 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +8 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +254 -0
- package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
- package/build/ts/kcmd/okf/init_okf_workspace.js +115 -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 +82 -0
- package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
- package/build/ts/kcmd/semantic/bigquery.js +525 -0
- package/build/ts/kcmd/semantic/binding.d.ts +3 -0
- package/build/ts/kcmd/semantic/binding.js +97 -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 +129 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1396 -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 +34 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +563 -0
- package/build/ts/kcmd/semantic/import_model.d.ts +50 -0
- package/build/ts/kcmd/semantic/import_model.js +1275 -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 +31 -0
- package/build/ts/kcmd/semantic/kc_converter.js +824 -0
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +70 -0
- package/build/ts/kcmd/semantic/kc_custom_types.js +828 -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 +103 -0
- package/build/ts/kcmd/semantic/kc_ids.js +191 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +32 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.js +1043 -0
- package/build/ts/kcmd/semantic/loader.d.ts +262 -0
- package/build/ts/kcmd/semantic/loader.js +3440 -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 +911 -0
- package/build/ts/kcmd/semantic/parameters.d.ts +18 -0
- package/build/ts/kcmd/semantic/parameters.js +152 -0
- package/build/ts/kcmd/semantic/pull_kc.d.ts +14 -0
- package/build/ts/kcmd/semantic/pull_kc.js +346 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +54 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.js +330 -0
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +250 -0
- package/build/ts/kcmd/semantic/resolve_profiles.js +1822 -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 +94 -0
- package/build/ts/kcmd/semantic/runtime/runtime.js +318 -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 +10 -0
- package/build/ts/kcmd/semantic/source_dialect.js +261 -0
- package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
- package/build/ts/kcmd/semantic/source_entries.js +282 -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 +17 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.js +221 -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 +44 -0
- package/build/ts/kcmd/semantic/sql_parser.js +378 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +74 -0
- package/build/ts/kcmd/semantic/transpile.js +563 -0
- package/build/ts/kcmd/semantic/validate.d.ts +35 -0
- package/build/ts/kcmd/semantic/validate.js +1899 -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 +33 -0
- package/build/ts/kcmd/snapshot.js +379 -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 +697 -0
- package/package.json +62 -4
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Helpers for reading and rewriting entity-qualified SQL expressions across the
|
|
3
|
+
// semantic-model layer.
|
|
4
|
+
//
|
|
5
|
+
// Expressions reference columns as `<Entity>.<column>`. The SQL parser
|
|
6
|
+
// (`columnReferences`, `entitiesReached`, `fieldsReadOn`) reads column and
|
|
7
|
+
// 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`).
|
|
10
|
+
//
|
|
11
|
+
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;
|
|
18
|
+
exports.sqlWithoutComments = sqlWithoutComments;
|
|
19
|
+
exports.fieldBindingWithoutComments = fieldBindingWithoutComments;
|
|
20
|
+
exports.columnReferences = columnReferences;
|
|
21
|
+
exports.columnsForSource = columnsForSource;
|
|
22
|
+
exports.entitiesReached = entitiesReached;
|
|
23
|
+
exports.fieldsReadOn = fieldsReadOn;
|
|
24
|
+
exports.isSingleAggregate = isSingleAggregate;
|
|
25
|
+
exports.isColumnName = isColumnName;
|
|
26
|
+
const ir_1 = require("./ir");
|
|
27
|
+
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) {
|
|
44
|
+
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;
|
|
50
|
+
}
|
|
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 });
|
|
72
|
+
}
|
|
73
|
+
return hits.sort((a, b) => a.at - b.at).map(h => h.name);
|
|
74
|
+
}
|
|
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, ''));
|
|
80
|
+
}
|
|
81
|
+
// `text`, written in `dialect`, with its comments removed. An ANSI_SQL text, or
|
|
82
|
+
// a text with no dialect, is read as BigQuery, as the loader and the graph
|
|
83
|
+
// generators read it.
|
|
84
|
+
function sqlWithoutComments(text, dialect) {
|
|
85
|
+
return (0, sql_parser_1.withoutComments)(text, !dialect || dialect === 'ANSI_SQL' ? 'BIGQUERY' : dialect);
|
|
86
|
+
}
|
|
87
|
+
// `field`'s binding, as fieldBinding gives it, with its comments removed.
|
|
88
|
+
// Undefined when the field is unbound. The target text is read in
|
|
89
|
+
// `targetDialect`, BigQuery unless given. The parser reads Spanner SQL as
|
|
90
|
+
// BigQuery. Imported vendor SQL, the binding only when there is no target
|
|
91
|
+
// text, is read in its imported dialect, so it keeps what that dialect does
|
|
92
|
+
// not read as a comment.
|
|
93
|
+
function fieldBindingWithoutComments(field, targetDialect) {
|
|
94
|
+
const text = (0, ir_1.fieldBinding)(field);
|
|
95
|
+
if (text === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
return sqlWithoutComments(text, field.expression === undefined ? field.importedDialect : targetDialect);
|
|
98
|
+
}
|
|
99
|
+
// Results of columnReferences by dialect and text, so the checks one push
|
|
100
|
+
// runs read each text once. When full, the oldest entry goes.
|
|
101
|
+
const MAX_COLUMN_CACHE_SIZE = 65536;
|
|
102
|
+
const COLUMN_CACHE = new Map();
|
|
103
|
+
// Every column an expression reads, with the qualifier written before it, in
|
|
104
|
+
// the order written, or undefined when the SQL parser cannot read the
|
|
105
|
+
// expression in `dialect`, one of the dialect names a dialect list uses. The
|
|
106
|
+
// parser handles string literals, comments and each dialect's quoting, so
|
|
107
|
+
// `"orders"."amount"` in a PostgreSQL text reads the column `amount` of
|
|
108
|
+
// `orders`, while in BigQuery the same text is not SQL and gives undefined. A struct path reads
|
|
109
|
+
// as its first two parts: `orders.shipping_address.city` reads
|
|
110
|
+
// `shipping_address` of `orders`.
|
|
111
|
+
function columnReferences(expression, dialect) {
|
|
112
|
+
const key = `${dialect.toUpperCase()}\0${expression}`;
|
|
113
|
+
if (COLUMN_CACHE.has(key))
|
|
114
|
+
return COLUMN_CACHE.get(key);
|
|
115
|
+
const cols = (0, sql_parser_1.sqlColumns)(expression, dialect);
|
|
116
|
+
if (COLUMN_CACHE.size >= MAX_COLUMN_CACHE_SIZE) {
|
|
117
|
+
COLUMN_CACHE.delete(COLUMN_CACHE.keys().next().value);
|
|
118
|
+
}
|
|
119
|
+
COLUMN_CACHE.set(key, cols);
|
|
120
|
+
return cols;
|
|
121
|
+
}
|
|
122
|
+
// Reads the columns of `text` in `dialect`. An `ANSI_SQL` text is read in the
|
|
123
|
+
// dialect the model's sources imply (`impliedDialect`), since the database runs
|
|
124
|
+
// an `ANSI_SQL` entry as its own SQL. When the sources do not imply a single
|
|
125
|
+
// dialect, the generic parser reads the text, and any bare identifier that
|
|
126
|
+
// 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.
|
|
128
|
+
function columnsForSource(text, dialect, impliedDialect) {
|
|
129
|
+
if (dialect === 'ANSI_SQL' && impliedDialect) {
|
|
130
|
+
return columnReferences(text, impliedDialect);
|
|
131
|
+
}
|
|
132
|
+
const cols = columnReferences(text, dialect);
|
|
133
|
+
if (!cols || dialect !== 'ANSI_SQL')
|
|
134
|
+
return cols;
|
|
135
|
+
const bqCols = columnReferences(text, 'BIGQUERY');
|
|
136
|
+
if (!bqCols)
|
|
137
|
+
return cols;
|
|
138
|
+
const bqBare = new Set(bqCols.filter(c => c.qualifier === undefined).map(c => c.name));
|
|
139
|
+
return cols.filter(c => c.qualifier !== undefined || bqBare.has(c.name));
|
|
140
|
+
}
|
|
141
|
+
// The entities from `entityNames` that `columns` read, deduplicated in
|
|
142
|
+
// first-seen order. Matching is case-sensitive.
|
|
143
|
+
function entitiesReached(columns, entityNames) {
|
|
144
|
+
const known = entityNames instanceof Set ? entityNames : new Set(entityNames);
|
|
145
|
+
const reached = [];
|
|
146
|
+
for (const c of columns) {
|
|
147
|
+
if (c.qualifier && known.has(c.qualifier) &&
|
|
148
|
+
!reached.includes(c.qualifier)) {
|
|
149
|
+
reached.push(c.qualifier);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return reached;
|
|
153
|
+
}
|
|
154
|
+
// The fields `columns` read on `entityName`, written `entityName.field`,
|
|
155
|
+
// deduplicated in first-seen order. Matching is case-sensitive.
|
|
156
|
+
function fieldsReadOn(columns, entityName) {
|
|
157
|
+
const fields = [];
|
|
158
|
+
for (const c of columns) {
|
|
159
|
+
if (c.qualifier === entityName && !fields.includes(c.name)) {
|
|
160
|
+
fields.push(c.name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return fields;
|
|
164
|
+
}
|
|
165
|
+
// The aggregates BigQuery accepts in a MEASURE, its complete list. BigQuery
|
|
166
|
+
// rejects any other there, such as COUNTIF or APPROX_COUNT_DISTINCT.
|
|
167
|
+
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) {
|
|
172
|
+
const node = unwrapped((0, sql_parser_1.sqlExpressionTree)(expression, dialect));
|
|
173
|
+
const kind = node && Object.keys(node).length === 1 ? Object.keys(node)[0] :
|
|
174
|
+
undefined;
|
|
175
|
+
if (!kind || !exports.SUPPORTED_AGGREGATES.includes(kind.toUpperCase())) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const call = node[kind];
|
|
179
|
+
const star = kind === 'count' && call.star === true && !call.this;
|
|
180
|
+
const operand = unwrapped(call.this);
|
|
181
|
+
if (!star &&
|
|
182
|
+
(!operand || operand.star || operand.tuple ||
|
|
183
|
+
nestsAggregateOrSubquery(operand))) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
// The parser keeps the call's name in `name` or `original_name`.
|
|
187
|
+
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));
|
|
190
|
+
}
|
|
191
|
+
// The parser's tree `node` without the parentheses and comments around it.
|
|
192
|
+
function unwrapped(node) {
|
|
193
|
+
while (node?.paren || node?.annotated) {
|
|
194
|
+
node = (node.paren ?? node.annotated).this;
|
|
195
|
+
}
|
|
196
|
+
return node;
|
|
197
|
+
}
|
|
198
|
+
// Whether the parser's tree `node` holds an aggregate, a window or a subquery
|
|
199
|
+
// at any depth. The parser gives an aggregate call `distinct` and `filter`
|
|
200
|
+
// keys, or the kind `aggregate_function`.
|
|
201
|
+
function nestsAggregateOrSubquery(node) {
|
|
202
|
+
if (Array.isArray(node))
|
|
203
|
+
return node.some(nestsAggregateOrSubquery);
|
|
204
|
+
if (node === null || typeof node !== 'object')
|
|
205
|
+
return false;
|
|
206
|
+
return Object.entries(node).some(([kind, value]) => kind === 'aggregate_function' ||
|
|
207
|
+
kind === 'window_function' || kind === 'subquery' ||
|
|
208
|
+
kind === 'select' ||
|
|
209
|
+
(value !== null && typeof value === 'object' && 'distinct' in value &&
|
|
210
|
+
'filter' in value) ||
|
|
211
|
+
nestsAggregateOrSubquery(value));
|
|
212
|
+
}
|
|
213
|
+
// Whether a key or join column is a physical column name rather than an
|
|
214
|
+
// expression. A value that starts and ends with a backtick, with none between,
|
|
215
|
+
// passes whatever it holds. An unquoted value may not contain whitespace,
|
|
216
|
+
// parentheses, a dot, a comma, a backtick or a SQL operator. Nothing else about
|
|
217
|
+
// the character set is checked; the database rejects a bad name.
|
|
218
|
+
function isColumnName(value) {
|
|
219
|
+
return typeof value === 'string' &&
|
|
220
|
+
(/^`[^`]+`$/.test(value) || /^[^\s().+\-*/%=<>!|&^~,`]+$/.test(value));
|
|
221
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface PolyglotEngine {
|
|
2
|
+
init(opts: {
|
|
3
|
+
wasmUrl: string;
|
|
4
|
+
}): Promise<void>;
|
|
5
|
+
transpile(sql: string, read: string, write: string): {
|
|
6
|
+
success: boolean;
|
|
7
|
+
sql?: string[];
|
|
8
|
+
error?: string;
|
|
9
|
+
};
|
|
10
|
+
parse(sql: string, dialect: string): {
|
|
11
|
+
success: boolean;
|
|
12
|
+
ast?: unknown[];
|
|
13
|
+
error?: string;
|
|
14
|
+
};
|
|
15
|
+
generate(ast: unknown[], dialect: string): {
|
|
16
|
+
success: boolean;
|
|
17
|
+
sql?: string[];
|
|
18
|
+
error?: string;
|
|
19
|
+
};
|
|
20
|
+
tokenize(sql: string, dialect: string): {
|
|
21
|
+
success: boolean;
|
|
22
|
+
tokens?: SqlToken[];
|
|
23
|
+
error?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
interface SqlToken {
|
|
27
|
+
text: string;
|
|
28
|
+
span: {
|
|
29
|
+
start: number;
|
|
30
|
+
end: number;
|
|
31
|
+
};
|
|
32
|
+
comments: string[];
|
|
33
|
+
trailing_comments: string[];
|
|
34
|
+
}
|
|
35
|
+
export declare function loadSqlEngine(): Promise<PolyglotEngine>;
|
|
36
|
+
export interface SqlColumn {
|
|
37
|
+
qualifier?: string;
|
|
38
|
+
name: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function sqlColumns(expression: string, dialect: string): SqlColumn[] | undefined;
|
|
41
|
+
export declare function sqlExpressionTree(expression: string, dialect: string): any;
|
|
42
|
+
export declare function holdsNoSql(expression: string, dialect: string): boolean;
|
|
43
|
+
export declare function withoutComments(expression: string, dialect: string): string;
|
|
44
|
+
export {};
|