@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,525 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Generates BigQuery property-graph DDL (with inline measures) from the
|
|
3
|
+
// Semantic Model IR.
|
|
4
|
+
//
|
|
5
|
+
// The shared GoogleSQL generator (./graph_ddl) writes the graph structure: node
|
|
6
|
+
// tables, edge tables, labels, properties, and key and join columns. This
|
|
7
|
+
// module supplies its BigQuery engine: qualified names, measures, the
|
|
8
|
+
// per-element OPTIONS clause, and the SQL text a field binds to.
|
|
9
|
+
//
|
|
10
|
+
// BigQuery graph measures bind an aggregate to exactly one table's KEY; the
|
|
11
|
+
// cross-entity rollup happens at query time via GRAPH_EXPAND(...) + AGG(...).
|
|
12
|
+
// A metric therefore lands on whichever single table its aggregate columns
|
|
13
|
+
// reference; a metric whose aggregate genuinely spans multiple tables cannot be
|
|
14
|
+
// expressed as one MEASURE and is skipped (reported in `warnings`).
|
|
15
|
+
//
|
|
16
|
+
// A measure can only aggregate an EXPOSED PROPERTY of its node (not a raw
|
|
17
|
+
// column or an inline expression), so each metric is lowered into a derived
|
|
18
|
+
// operand property plus a MEASURE over it (see placeMetric / exposeOperand).
|
|
19
|
+
//
|
|
20
|
+
// See: https://docs.cloud.google.com/bigquery/docs/graph-measures
|
|
21
|
+
//
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.generatePropertyGraph = generatePropertyGraph;
|
|
24
|
+
exports.elementDescription = elementDescription;
|
|
25
|
+
exports.fieldDescription = fieldDescription;
|
|
26
|
+
const graph_ddl_1 = require("./graph_ddl");
|
|
27
|
+
const ir_1 = require("./ir");
|
|
28
|
+
const sql_expr_utils_1 = require("./sql_expr_utils");
|
|
29
|
+
const sql_identifiers_1 = require("./sql_identifiers");
|
|
30
|
+
/**
|
|
31
|
+
* Generates the BigQuery property-graph DDL for a semantic model.
|
|
32
|
+
*
|
|
33
|
+
* The generated statement references the entities' existing base tables; it
|
|
34
|
+
* does not create them. Returns the DDL plus any warnings collected while
|
|
35
|
+
* mapping the IR (e.g. metrics that could not be placed on a single table).
|
|
36
|
+
*
|
|
37
|
+
* Throws when the model yields no valid node table -- it declares no entities,
|
|
38
|
+
* or every entity lacks a KEY -- since a property graph with an empty NODE
|
|
39
|
+
* TABLES block is invalid DDL that BigQuery would reject.
|
|
40
|
+
*/
|
|
41
|
+
function generatePropertyGraph(model, opts = {}) {
|
|
42
|
+
return (0, graph_ddl_1.generateGraphDdl)(model, bigQueryEngine(opts));
|
|
43
|
+
}
|
|
44
|
+
// The BigQuery engine for the shared generator.
|
|
45
|
+
function bigQueryEngine(opts) {
|
|
46
|
+
return {
|
|
47
|
+
name: 'BigQuery',
|
|
48
|
+
table: qualifyTable,
|
|
49
|
+
graphName: model => qualifyGraph(model, opts),
|
|
50
|
+
placeMetrics,
|
|
51
|
+
elementOptions: (description, ai) => optionsClause(elementDescription(description, ai), ai?.synonyms),
|
|
52
|
+
fieldOptions: field => optionsClause(fieldDescription(field), field.aiContext?.synonyms),
|
|
53
|
+
dialect: 'BIGQUERY',
|
|
54
|
+
fieldExpression,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Places each metric on the single entity its aggregate references, lowering
|
|
58
|
+
// it to a MEASURE over an exposed property (see placeMetric).
|
|
59
|
+
function placeMetrics(metrics, input, warnings) {
|
|
60
|
+
const loweringByEntity = new Map();
|
|
61
|
+
for (const metric of metrics) {
|
|
62
|
+
placeMetric(metric, input, loweringByEntity, warnings);
|
|
63
|
+
}
|
|
64
|
+
// The operand properties come first: the measures aggregate them.
|
|
65
|
+
const placed = new Map();
|
|
66
|
+
for (const [name, lowering] of loweringByEntity) {
|
|
67
|
+
placed.set(name, [...lowering.derivedProperties, ...lowering.measures]);
|
|
68
|
+
}
|
|
69
|
+
return placed;
|
|
70
|
+
}
|
|
71
|
+
// Builds the lowering state for an entity, seeded with its declared fields so
|
|
72
|
+
// an operand equal to an existing field reuses that property instead of
|
|
73
|
+
// duplicating.
|
|
74
|
+
function newLowering(entity) {
|
|
75
|
+
const taken = new Set();
|
|
76
|
+
const fieldNames = new Set();
|
|
77
|
+
const byLocalExpr = new Map();
|
|
78
|
+
for (const f of entity.fields) {
|
|
79
|
+
taken.add(f.name);
|
|
80
|
+
fieldNames.add(f.name);
|
|
81
|
+
const expr = fieldExpression(f);
|
|
82
|
+
if (expr === undefined)
|
|
83
|
+
continue;
|
|
84
|
+
const local = (0, sql_expr_utils_1.stripQualifier)(expr, entity.name);
|
|
85
|
+
if (!byLocalExpr.has(local))
|
|
86
|
+
byLocalExpr.set(local, f.name);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
derivedProperties: [],
|
|
90
|
+
measures: [],
|
|
91
|
+
taken,
|
|
92
|
+
fieldNames,
|
|
93
|
+
byLocalExpr,
|
|
94
|
+
operandToName: new Map()
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Assigns a metric to the node table of the single entity its aggregate
|
|
98
|
+
// references, lowering it to a MEASURE over an exposed property. Records a
|
|
99
|
+
// warning (and skips) when it references zero/multiple entities or is not a
|
|
100
|
+
// single supported aggregate over one operand.
|
|
101
|
+
function placeMetric(metric, input, loweringByEntity, warnings) {
|
|
102
|
+
const { entityByName, skipped, ancestorsUsed } = input;
|
|
103
|
+
// A measure is emitted from the metric's BigQuery SQL (see
|
|
104
|
+
// metricExpression). A metric with SQL only for other engines is skipped
|
|
105
|
+
// with a warning naming the dialects it lacks, rather than emitting that
|
|
106
|
+
// other engine's SQL into a BigQuery graph.
|
|
107
|
+
const expression = metricExpression(metric);
|
|
108
|
+
if (expression === undefined) {
|
|
109
|
+
warnings.push(metric.dialects?.length || metric.importedExpression !== undefined ?
|
|
110
|
+
`metric '${metric.name}' has no 'BIGQUERY' or 'ANSI_SQL' ` +
|
|
111
|
+
`expression; skipped (nothing to emit as a MEASURE)` :
|
|
112
|
+
`metric '${metric.name}' has no expression; skipped (nothing ` +
|
|
113
|
+
`to emit as a MEASURE)`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// The entities whose `<name>.` qualifier the expression uses. String literals
|
|
117
|
+
// are ignored, so a value like 'orders.x' does not reference `orders`.
|
|
118
|
+
let referenced = (0, sql_expr_utils_1.referencedEntityNames)(expression, [...entityByName.keys()]);
|
|
119
|
+
// A qualifier-free aggregate (e.g. COUNT(*)) names no entity in its
|
|
120
|
+
// expression; fall back to `metric.entity` when the IR provides one (an
|
|
121
|
+
// authored `entity` in `0.2.0.dev0/google`, an entity inferred from another
|
|
122
|
+
// dialect entry, or hand-built IR), so it can still be placed rather than
|
|
123
|
+
// dropped as "references no entity".
|
|
124
|
+
if (referenced.length === 0 && metric.entity) {
|
|
125
|
+
referenced = [metric.entity];
|
|
126
|
+
}
|
|
127
|
+
if (referenced.length !== 1) {
|
|
128
|
+
const detail = referenced.length === 0 ?
|
|
129
|
+
'references no known entity' :
|
|
130
|
+
`spans multiple tables (${referenced.join(', ')})`;
|
|
131
|
+
warnings.push(`metric '${metric.name}' ${detail}; skipped (cannot be a single MEASURE)`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const entityName = referenced[0];
|
|
135
|
+
// The IR also declares a single attach entity (metric.entity). Placement is
|
|
136
|
+
// driven by the qualifiers actually present in the expression (that is what
|
|
137
|
+
// we strip and attach), but a disagreement with the declared entity signals
|
|
138
|
+
// an inconsistent model, so surface it. Emit this only now that we know the
|
|
139
|
+
// metric will actually be placed, so a metric that is subsequently dropped
|
|
140
|
+
// does not carry a contradictory "placing per the expression" note.
|
|
141
|
+
if (metric.entity && entityName !== metric.entity) {
|
|
142
|
+
warnings.push(`metric '${metric.name}' declares entity '${metric.entity}' but its ` +
|
|
143
|
+
`expression references '${entityName}'; placing per the expression`);
|
|
144
|
+
}
|
|
145
|
+
const entity = entityByName.get(entityName);
|
|
146
|
+
if (!entity) {
|
|
147
|
+
warnings.push(`metric '${metric.name}' references unknown entity '${entityName}'; skipped (cannot be a single MEASURE)`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// An abstract entity is a table-less supertype with no node table, so it can
|
|
151
|
+
// carry no MEASURE. Report that specifically -- it is also in `skipped`, so
|
|
152
|
+
// this must precede the generic no-KEY message below to stay accurate.
|
|
153
|
+
if (entity.abstract) {
|
|
154
|
+
warnings.push(`metric '${metric.name}' targets entity '${entityName}', which is ` +
|
|
155
|
+
`abstract (a table-less supertype with no node table to carry a ` +
|
|
156
|
+
`MEASURE); metric dropped`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// A metric can only become a MEASURE on a node table, but an entity that was
|
|
160
|
+
// skipped upstream (empty KEY) has no node table to carry it. Check the skip
|
|
161
|
+
// set already computed, and report the drop directly, instead of letting it
|
|
162
|
+
// fail later with a misleading "aggregate not supported" message.
|
|
163
|
+
if (skipped.has(entityName)) {
|
|
164
|
+
warnings.push(`metric '${metric.name}' targets entity '${entityName}', which has no KEY and was skipped; metric dropped`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// A measure is allowed only on a LEAF type -- one that no table-emitting
|
|
168
|
+
// entity extends. A metric lowers to a MEASURE on the target's DEFAULT LABEL;
|
|
169
|
+
// when that entity is a supertype its label is shared with every subtype
|
|
170
|
+
// table, and BigQuery forbids a MEASURE on a label carried by more than one
|
|
171
|
+
// element table (verified live: "defined as MEASURE, but there are other
|
|
172
|
+
// declarations with the same name"). Drop it with a warning rather than emit
|
|
173
|
+
// DDL BigQuery rejects. Push validation rejects such a metric before
|
|
174
|
+
// generation; this guard covers a caller that generates DDL without
|
|
175
|
+
// validating first.
|
|
176
|
+
if (ancestorsUsed.has(entityName)) {
|
|
177
|
+
warnings.push(`metric '${metric.name}' targets entity '${entityName}', which is not ` +
|
|
178
|
+
`a leaf type (another entity extends it); a measure is allowed only on ` +
|
|
179
|
+
`a leaf type, because a supertype's label is shared across its ` +
|
|
180
|
+
`subtype tables; skipped`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const body = (0, sql_expr_utils_1.stripQualifier)(expression, entityName);
|
|
184
|
+
// A graph measure must be one supported aggregate wrapping a single operand;
|
|
185
|
+
// anything else (a non-aggregate, or a compound of aggregates like a ratio)
|
|
186
|
+
// cannot be a single MEASURE. Flag it rather than emit DDL BigQuery rejects.
|
|
187
|
+
const agg = extractAggregate(body);
|
|
188
|
+
if (!agg) {
|
|
189
|
+
warnings.push(`metric '${metric.name}' expression '${body}' is not a single supported aggregate ` +
|
|
190
|
+
`(${sql_expr_utils_1.SUPPORTED_AGGREGATES.join(', ')}) over one operand; skipped (cannot be a single MEASURE)`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const lowering = loweringByEntity.get(entityName) ?? newLowering(entity);
|
|
194
|
+
loweringByEntity.set(entityName, lowering);
|
|
195
|
+
// Resolve the aggregate's operand to an exposed property. COUNT(*) has no
|
|
196
|
+
// operand column, so it counts the first column of the node's key instead:
|
|
197
|
+
// the primary key, else the first unique key (nodeKey).
|
|
198
|
+
let operandExpr = agg.operand;
|
|
199
|
+
const countsKey = agg.operand === '*';
|
|
200
|
+
if (countsKey) {
|
|
201
|
+
// The entity is already known to have a key (checked above), so only the
|
|
202
|
+
// aggregate itself can disqualify a `*` operand: BigQuery lowers `COUNT(*)`
|
|
203
|
+
// to a count over the key, but no other aggregate has a `*` form.
|
|
204
|
+
if (agg.fn.toUpperCase() !== 'COUNT') {
|
|
205
|
+
warnings.push(`metric '${metric.name}': '${agg.fn}(*)' is not supported; skipped ` +
|
|
206
|
+
`(only COUNT(*) over a keyed node can be lowered)`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
operandExpr = (0, graph_ddl_1.nodeKey)(entity)[0];
|
|
210
|
+
}
|
|
211
|
+
// A field with no BigQuery SQL is left out of the node table, so a measure
|
|
212
|
+
// over it would aggregate a property the table does not declare.
|
|
213
|
+
if (!countsKey &&
|
|
214
|
+
entity.fields.some(f => f.name === operandExpr && (0, ir_1.isFieldBound)(f) &&
|
|
215
|
+
fieldExpression(f) === undefined)) {
|
|
216
|
+
warnings.push(`metric '${metric.name}' aggregates field '${operandExpr}', which ` +
|
|
217
|
+
`has no 'BIGQUERY' or 'ANSI_SQL' expression; skipped (nothing to ` +
|
|
218
|
+
`emit as a MEASURE)`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// The measure is itself exposed as a node property named `metric.name`, so it
|
|
222
|
+
// must not collide with a field, a prior measure, or a synthesized operand
|
|
223
|
+
// property — a duplicate property makes BigQuery reject the whole node table.
|
|
224
|
+
// A clash with an already-taken name is a model error we cannot paper over by
|
|
225
|
+
// renaming (the measure name is exactly what a reader selects via
|
|
226
|
+
// GRAPH_EXPAND + AGG(<label>_<name>)), so skip + warn. Reserve it up front so
|
|
227
|
+
// the operand lowering below never synthesizes a property with the same name.
|
|
228
|
+
if (lowering.taken.has(metric.name)) {
|
|
229
|
+
warnings.push(`metric '${metric.name}' collides with an existing property of entity ` +
|
|
230
|
+
`'${entityName}'; skipped (rename the metric to avoid a duplicate graph property)`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
lowering.taken.add(metric.name);
|
|
234
|
+
const propName = countsKey ?
|
|
235
|
+
exposeKeyColumn(lowering, operandExpr, metric.name) :
|
|
236
|
+
exposeOperand(lowering, operandExpr, metric.name);
|
|
237
|
+
const aggregate = `${agg.fn}(${agg.distinct ? 'DISTINCT ' : ''}${(0, sql_identifiers_1.quoteIfReserved)(propName)})`;
|
|
238
|
+
const opts = optionsClause(elementDescription(metric.description, metric.aiContext), metric.aiContext?.synonyms);
|
|
239
|
+
const measure = `MEASURE(${aggregate}) AS ${(0, sql_identifiers_1.quoteIfReserved)(metric.name)}`;
|
|
240
|
+
lowering.measures.push(opts ? `${measure} ${opts}` : measure);
|
|
241
|
+
}
|
|
242
|
+
// Ensures `operandExpr` is exposed as a node property and returns its property
|
|
243
|
+
// name. Reuses an existing field (or a previously derived property) with the
|
|
244
|
+
// same expression; a bare column is exposed under its own name; any other
|
|
245
|
+
// expression gets a synthesized, unique name derived from the metric.
|
|
246
|
+
function exposeOperand(lowering, operandExpr, metricName) {
|
|
247
|
+
const existing = lowering.byLocalExpr.get(operandExpr) ??
|
|
248
|
+
lowering.operandToName.get(operandExpr);
|
|
249
|
+
if (existing)
|
|
250
|
+
return existing;
|
|
251
|
+
// An operand that names a declared field is already an exposed property, so a
|
|
252
|
+
// MEASURE may aggregate it directly by name -- even when a profile bound that
|
|
253
|
+
// field to a differently named physical column (the property is
|
|
254
|
+
// `<column> AS <field>`, and a MEASURE may reference a sibling alias).
|
|
255
|
+
// Synthesizing an input property here would emit `<field> AS ..._input`, and
|
|
256
|
+
// an alias is illegal inside a property expression -- BigQuery rejects it with
|
|
257
|
+
// "Unrecognized name". (A raw column that is not a field still falls through
|
|
258
|
+
// to be exposed under its own name, which BigQuery requires.)
|
|
259
|
+
if (lowering.fieldNames.has(operandExpr))
|
|
260
|
+
return operandExpr;
|
|
261
|
+
let name;
|
|
262
|
+
if ((0, sql_identifiers_1.isSimpleIdentifier)(operandExpr) && !lowering.taken.has(operandExpr)) {
|
|
263
|
+
// A bare column not already declared: expose it under its own name.
|
|
264
|
+
name = operandExpr;
|
|
265
|
+
lowering.derivedProperties.push((0, sql_identifiers_1.quoteIfReserved)(name));
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
name = uniqueName(`${metricName}_input`, lowering.taken);
|
|
269
|
+
lowering.derivedProperties.push(`${(0, sql_identifiers_1.quoteIfReserved)(operandExpr)} AS ${name}`);
|
|
270
|
+
}
|
|
271
|
+
lowering.taken.add(name);
|
|
272
|
+
lowering.operandToName.set(operandExpr, name);
|
|
273
|
+
return name;
|
|
274
|
+
}
|
|
275
|
+
// Exposes the key column COUNT(*) counts and returns its property name,
|
|
276
|
+
// reusing a field whose expression is exactly that column, or the property an
|
|
277
|
+
// earlier metric exposed for it.
|
|
278
|
+
function exposeKeyColumn(lowering, column, metricName) {
|
|
279
|
+
const existing = lowering.byLocalExpr.get(column) ?? lowering.operandToName.get(column);
|
|
280
|
+
if (existing)
|
|
281
|
+
return existing;
|
|
282
|
+
const bare = (0, sql_identifiers_1.isSimpleIdentifier)(column) && !lowering.taken.has(column);
|
|
283
|
+
const name = bare ? column : uniqueName(`${metricName}_input`, lowering.taken);
|
|
284
|
+
lowering.derivedProperties.push(bare ? (0, sql_identifiers_1.quoteIfReserved)(column) : `${(0, graph_ddl_1.physicalColumn)(column)} AS ${name}`);
|
|
285
|
+
lowering.taken.add(name);
|
|
286
|
+
// An operand named like a field reads that field (exposeOperand), so only a
|
|
287
|
+
// column no field is named after is shared with a later metric's operand.
|
|
288
|
+
if (!lowering.fieldNames.has(column))
|
|
289
|
+
lowering.operandToName.set(column, name);
|
|
290
|
+
return name;
|
|
291
|
+
}
|
|
292
|
+
// Extracts the function name and operand from a single-aggregate expression, or
|
|
293
|
+
// returns null when the body is not exactly one supported aggregate wrapping
|
|
294
|
+
// one operand (a non-aggregate, a compound like `SUM(x)/SUM(y)`, a call with no
|
|
295
|
+
// operand, or a multi-argument call). `COUNT(DISTINCT x)` is recognized,
|
|
296
|
+
// yielding operand `x` with distinct.
|
|
297
|
+
function extractAggregate(body) {
|
|
298
|
+
const s = body.trim();
|
|
299
|
+
const head = s.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
300
|
+
if (!head)
|
|
301
|
+
return null;
|
|
302
|
+
const fn = head[1];
|
|
303
|
+
if (!sql_expr_utils_1.SUPPORTED_AGGREGATES.includes(fn.toUpperCase()))
|
|
304
|
+
return null;
|
|
305
|
+
// Find the operand bounded by the aggregate's outer parentheses, tracking
|
|
306
|
+
// string literals and backtick-quoted identifiers and nesting so inner
|
|
307
|
+
// parens/quotes/backticks don't end it early.
|
|
308
|
+
let depth = 0, inStr = false, inTick = false, start = -1, end = -1;
|
|
309
|
+
for (let i = head[0].length - 1; i < s.length; i++) {
|
|
310
|
+
const c = s[i];
|
|
311
|
+
if (inStr) {
|
|
312
|
+
if (c === '\'')
|
|
313
|
+
inStr = false;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (inTick) {
|
|
317
|
+
if (c === '`')
|
|
318
|
+
inTick = false;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (c === '\'') {
|
|
322
|
+
inStr = true;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (c === '`') {
|
|
326
|
+
inTick = true;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (c === '(') {
|
|
330
|
+
if (depth === 0)
|
|
331
|
+
start = i + 1;
|
|
332
|
+
depth++;
|
|
333
|
+
}
|
|
334
|
+
else if (c === ')') {
|
|
335
|
+
if (--depth === 0) {
|
|
336
|
+
end = i;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (start < 0 || end < 0)
|
|
342
|
+
return null;
|
|
343
|
+
if (s.slice(end + 1).trim() !== '')
|
|
344
|
+
return null; // trailing ops => not a single aggregate
|
|
345
|
+
let operand = s.slice(start, end).trim();
|
|
346
|
+
let distinct = false;
|
|
347
|
+
const dm = operand.match(/^DISTINCT(?:\s+(.*))?$/is);
|
|
348
|
+
if (dm) {
|
|
349
|
+
distinct = true;
|
|
350
|
+
operand = (dm[1] ?? '').trim();
|
|
351
|
+
}
|
|
352
|
+
if (operand === '')
|
|
353
|
+
return null; // no operand, as in SUM() or SUM(DISTINCT)
|
|
354
|
+
if (operand !== '*' && hasTopLevelComma(operand))
|
|
355
|
+
return null; // multi-arg aggregate
|
|
356
|
+
return { fn, operand, distinct };
|
|
357
|
+
}
|
|
358
|
+
// True if `expr` contains a comma outside any nested parentheses, string
|
|
359
|
+
// literal, or backtick-quoted identifier (i.e. it is really several arguments,
|
|
360
|
+
// not one operand). A backtick-quoted column like `` `weird,name` `` must not
|
|
361
|
+
// be mistaken for a multi-argument aggregate.
|
|
362
|
+
function hasTopLevelComma(expr) {
|
|
363
|
+
let depth = 0, inStr = false, inTick = false;
|
|
364
|
+
for (const c of expr) {
|
|
365
|
+
if (inStr) {
|
|
366
|
+
if (c === '\'')
|
|
367
|
+
inStr = false;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (inTick) {
|
|
371
|
+
if (c === '`')
|
|
372
|
+
inTick = false;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (c === '\'')
|
|
376
|
+
inStr = true;
|
|
377
|
+
else if (c === '`')
|
|
378
|
+
inTick = true;
|
|
379
|
+
else if (c === '(')
|
|
380
|
+
depth++;
|
|
381
|
+
else if (c === ')')
|
|
382
|
+
depth--;
|
|
383
|
+
else if (c === ',' && depth === 0)
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
// Returns `base` if free, else the first `base_2`, `base_3`, ... not in
|
|
389
|
+
// `taken`.
|
|
390
|
+
function uniqueName(base, taken) {
|
|
391
|
+
if (!taken.has(base))
|
|
392
|
+
return base;
|
|
393
|
+
for (let i = 2;; i++) {
|
|
394
|
+
const candidate = `${base}_${i}`;
|
|
395
|
+
if (!taken.has(candidate))
|
|
396
|
+
return candidate;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// The SQL BigQuery runs for a field or metric: its `BIGQUERY` dialect entry,
|
|
400
|
+
// else its `ANSI_SQL` one (see expressionForDialect), read as BigQuery with its
|
|
401
|
+
// comments removed. Another engine's vendor SQL is never returned. The DDL puts
|
|
402
|
+
// ` AS <name>` and a comma after the expression on the same line, and a line
|
|
403
|
+
// comment left in would swallow them.
|
|
404
|
+
function fieldExpression(f) {
|
|
405
|
+
return bigQuerySql(f);
|
|
406
|
+
}
|
|
407
|
+
function metricExpression(m) {
|
|
408
|
+
return bigQuerySql(m);
|
|
409
|
+
}
|
|
410
|
+
function bigQuerySql(item) {
|
|
411
|
+
const text = (0, ir_1.expressionForDialect)(item, 'BIGQUERY');
|
|
412
|
+
return text === undefined ? undefined : (0, sql_expr_utils_1.sqlWithoutComments)(text);
|
|
413
|
+
}
|
|
414
|
+
// Builds a backtick-quoted table reference from the IR's `dataSource` string.
|
|
415
|
+
// The IR contract guarantees `dataSource` is already canonical and
|
|
416
|
+
// fully-qualified: the producer (loader) normalizes a BigQuery or BigLake
|
|
417
|
+
// source into a dotted `project.dataset.table` (or four-part BigLake catalog
|
|
418
|
+
// name). For BigQuery that canonical form IS the target addressing scheme, so
|
|
419
|
+
// this emits it verbatim and NEVER prepends the graph's own project/dataset --
|
|
420
|
+
// those name where the graph is CREATED, not where a source table lives, and
|
|
421
|
+
// the two can differ (e.g. a graph in `proj.demo` over source tables in
|
|
422
|
+
// `samples.tpch`). Qualifying a source table is the producer's job, not the
|
|
423
|
+
// emitter's.
|
|
424
|
+
function qualifyTable(dataSource, warnings, context) {
|
|
425
|
+
const trimmed = (dataSource ?? '').trim();
|
|
426
|
+
if (!trimmed) {
|
|
427
|
+
warnings.push(`${context}: empty data source; the table reference will be invalid`);
|
|
428
|
+
return '``';
|
|
429
|
+
}
|
|
430
|
+
return `\`${trimmed}\``;
|
|
431
|
+
}
|
|
432
|
+
// Builds the dataset-qualified graph name, filling a missing project/dataset
|
|
433
|
+
// from opts and then from the first entity's qualified dataSource.
|
|
434
|
+
function qualifyGraph(model, opts) {
|
|
435
|
+
const name = opts.graphName ?? model.name;
|
|
436
|
+
let project = opts.project;
|
|
437
|
+
let dataset = opts.dataset;
|
|
438
|
+
// Pick the first entity with a non-empty `dataSource` to derive the graph's
|
|
439
|
+
// location; skip abstract entities (empty source).
|
|
440
|
+
const first = (model.entities ?? []).find(e => e.dataSource);
|
|
441
|
+
if ((!project || !dataset) && first) {
|
|
442
|
+
const p = first.dataSource.trim().split('.');
|
|
443
|
+
if (p.length >= 3) {
|
|
444
|
+
project = project ?? p[0];
|
|
445
|
+
dataset = dataset ?? p[1];
|
|
446
|
+
}
|
|
447
|
+
else if (p.length === 2) {
|
|
448
|
+
dataset = dataset ?? p[0];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// A graph name is `name`, `dataset.name`, or `project.dataset.name`; a
|
|
452
|
+
// project with no dataset cannot be expressed (`project.name` would be read
|
|
453
|
+
// as `dataset.name`), so drop a lone project rather than emit a malformed
|
|
454
|
+
// reference.
|
|
455
|
+
if (project && !dataset)
|
|
456
|
+
project = undefined;
|
|
457
|
+
const parts = [project, dataset, name].filter((p) => !!p);
|
|
458
|
+
return `\`${parts.join('.')}\``;
|
|
459
|
+
}
|
|
460
|
+
// Composes the folded `description` text for a graph element from its base
|
|
461
|
+
// description and the AI-first annotations that have no dedicated BigQuery
|
|
462
|
+
// option: instructions and examples. Synonyms are NOT folded in here --
|
|
463
|
+
// BigQuery's PropertyGraph{Label,Property}Options carry a structured `synonyms`
|
|
464
|
+
// array, so they are emitted as their own option (see optionsClause and the
|
|
465
|
+
// call sites).
|
|
466
|
+
function elementDescription(description, ai) {
|
|
467
|
+
return composeText([
|
|
468
|
+
description,
|
|
469
|
+
ai?.instructions,
|
|
470
|
+
examplesLine(ai?.examples),
|
|
471
|
+
]);
|
|
472
|
+
}
|
|
473
|
+
// A field additionally carries a human `label` and a temporal-dimension role,
|
|
474
|
+
// which lead the composed text (matching the authored order) ahead of its
|
|
475
|
+
// description and instructions/examples. Synonyms are emitted structurally, as
|
|
476
|
+
// for elementDescription.
|
|
477
|
+
function fieldDescription(field) {
|
|
478
|
+
return composeText([
|
|
479
|
+
field.label,
|
|
480
|
+
(0, ir_1.isTimeDimension)(field) ? 'Time dimension.' : undefined,
|
|
481
|
+
field.description,
|
|
482
|
+
field.aiContext?.instructions,
|
|
483
|
+
examplesLine(field.aiContext?.examples),
|
|
484
|
+
]);
|
|
485
|
+
}
|
|
486
|
+
function examplesLine(examples) {
|
|
487
|
+
return examples && examples.length ? `Examples: ${examples.join('; ')}` :
|
|
488
|
+
undefined;
|
|
489
|
+
}
|
|
490
|
+
// Joins the non-empty parts into one description, separated by blank lines so
|
|
491
|
+
// each reads as its own paragraph in the emitted metadata.
|
|
492
|
+
function composeText(parts) {
|
|
493
|
+
const kept = parts.map(p => (p === undefined ? undefined : p.trim()))
|
|
494
|
+
.filter((p) => !!p);
|
|
495
|
+
return kept.length ? kept.join('\n\n') : undefined;
|
|
496
|
+
}
|
|
497
|
+
// Renders the trailing `OPTIONS(...)` clause for a graph element -- a folded
|
|
498
|
+
// `description` string and/or a structured `synonyms` array -- or undefined
|
|
499
|
+
// when there is nothing to emit. Both map to BigQuery's
|
|
500
|
+
// PropertyGraph{Label,Property}Options (`description` singular, `synonyms`
|
|
501
|
+
// repeated), honored on element-table labels (DEFAULT LABEL, before PROPERTIES)
|
|
502
|
+
// and on derived properties and measures (after the alias). Statement-level
|
|
503
|
+
// graph OPTIONS is intentionally not emitted: it parses but BigQuery silently
|
|
504
|
+
// drops it (verified live; create_property_graph_options is off for BigQuery).
|
|
505
|
+
function optionsClause(description, synonyms) {
|
|
506
|
+
const parts = [];
|
|
507
|
+
if (description)
|
|
508
|
+
parts.push(`description=${quote(description)}`);
|
|
509
|
+
if (synonyms && synonyms.length) {
|
|
510
|
+
parts.push(`synonyms=[${synonyms.map(quote).join(', ')}]`);
|
|
511
|
+
}
|
|
512
|
+
return parts.length ? `OPTIONS(${parts.join(', ')})` : undefined;
|
|
513
|
+
}
|
|
514
|
+
// Renders a value as a BigQuery double-quoted string literal. Backslash and the
|
|
515
|
+
// quote are escaped, and control characters that cannot appear raw inside a
|
|
516
|
+
// quoted literal (newline, carriage return, tab) are escaped too, so a
|
|
517
|
+
// multi-line description does not produce a broken literal.
|
|
518
|
+
function quote(s) {
|
|
519
|
+
const escaped = s.replace(/\\/g, '\\\\')
|
|
520
|
+
.replace(/"/g, '\\"')
|
|
521
|
+
.replace(/\n/g, '\\n')
|
|
522
|
+
.replace(/\r/g, '\\r')
|
|
523
|
+
.replace(/\t/g, '\\t');
|
|
524
|
+
return `"${escaped}"`;
|
|
525
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type QuoteFn = (identifier: string) => string;
|
|
2
|
+
export declare function boundTable(dataSource: string, warnings: string[], context: string, quote: QuoteFn): string;
|
|
3
|
+
export declare function spannerTable(dataSource: string, warnings: string[], context: string): string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Which physical table an entity is bound to.
|
|
3
|
+
//
|
|
4
|
+
// A binding profile gives an entity a `source`, and every caller that touches
|
|
5
|
+
// real rows has to turn that string into a table name: the DDL generator
|
|
6
|
+
// naming an input table, the runtime building a statement, the agent tool
|
|
7
|
+
// derivation describing what a lookup reads. They must agree, so the rule
|
|
8
|
+
// lives in one place rather than in each of them.
|
|
9
|
+
//
|
|
10
|
+
// This is a binding concern, not a Spanner one. It sits outside the deploy
|
|
11
|
+
// path deliberately -- `runtime/` reads it, and a runtime that had to import a
|
|
12
|
+
// DDL generator to learn a table name would be describing a layering that is
|
|
13
|
+
// not real.
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.boundTable = boundTable;
|
|
16
|
+
exports.spannerTable = spannerTable;
|
|
17
|
+
const sql_identifiers_1 = require("./sql_identifiers");
|
|
18
|
+
// Maps the IR's `dataSource` to the BARE table name a statement references. A
|
|
19
|
+
// property graph names input tables within its own database, so only the final
|
|
20
|
+
// segment of a qualified `project.dataset.table` (or any dotted source) is
|
|
21
|
+
// meaningful; the leading qualifiers name where a BigQuery copy lives and have
|
|
22
|
+
// no bearing on the operational table. A resource-name URI
|
|
23
|
+
// (`//spanner.googleapis.com/.../tables/<t>`, or any other `scheme://.../<t>`)
|
|
24
|
+
// names its table by the final PATH segment for the same reason — the leading
|
|
25
|
+
// path locates the store, not the table. A verbatim query (contains whitespace)
|
|
26
|
+
// cannot back a graph element table, so it is passed through parenthesized with
|
|
27
|
+
// a warning.
|
|
28
|
+
function boundTable(dataSource, warnings, context, quote) {
|
|
29
|
+
const trimmed = (dataSource ?? '').trim();
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
warnings.push(`${context}: empty data source; the table reference will be invalid`);
|
|
32
|
+
return quote('');
|
|
33
|
+
}
|
|
34
|
+
if (/\s/.test(trimmed)) {
|
|
35
|
+
warnings.push(`${context}: data source '${trimmed}' looks like a query, not a table reference; ` +
|
|
36
|
+
`emitting it verbatim (a graph element table requires a table)`);
|
|
37
|
+
return `(${trimmed})`;
|
|
38
|
+
}
|
|
39
|
+
// A BigQuery or BigLake source reaches here dotted, but a Spanner, AlloyDB or
|
|
40
|
+
// Cloud SQL source as its resource URI (see isResourceUri), so reduce a URI
|
|
41
|
+
// to its final path segment before the dotted reduction below.
|
|
42
|
+
const source = isResourceUri(trimmed) ? finalPathSegment(trimmed) : trimmed;
|
|
43
|
+
const last = splitDotted(source).map(unquote).pop() ?? source;
|
|
44
|
+
return quote(last);
|
|
45
|
+
}
|
|
46
|
+
// `boundTable` in GoogleSQL, which is what the Spanner DDL generator wants and
|
|
47
|
+
// what every caller wanted when there was one backend.
|
|
48
|
+
function spannerTable(dataSource, warnings, context) {
|
|
49
|
+
return boundTable(dataSource, warnings, context, sql_identifiers_1.quoteIdentifier);
|
|
50
|
+
}
|
|
51
|
+
// A resource URI (`//host/...`) or any `scheme://...` URI, as opposed to a bare
|
|
52
|
+
// or dotted table reference. The loader gives the IR a BigQuery or BigLake
|
|
53
|
+
// table, or a legacy bare name, as its dotted name, a Spanner, AlloyDB or Cloud
|
|
54
|
+
// SQL table as its resource URI, with the leading `//`, and a third-party table
|
|
55
|
+
// as its entry name (see `parseSource` and `sourceToDataSource`). It rejects
|
|
56
|
+
// any `scheme://` source, so only a hand-built IR reaches that arm.
|
|
57
|
+
function isResourceUri(source) {
|
|
58
|
+
return source.startsWith('//') || /^[a-z][\w+.-]*:\/\//i.test(source);
|
|
59
|
+
}
|
|
60
|
+
// The final non-empty path segment of a URI (`.../tables/Customer` →
|
|
61
|
+
// `Customer`).
|
|
62
|
+
function finalPathSegment(uri) {
|
|
63
|
+
const segments = uri.split('/').filter(s => s.length > 0);
|
|
64
|
+
return segments.pop() ?? uri;
|
|
65
|
+
}
|
|
66
|
+
// Splits a dotted source into its segments, treating a backtick- or
|
|
67
|
+
// double-quote-delimited segment as opaque so a dot INSIDE quotes does not
|
|
68
|
+
// split an identifier: `proj.ds.`weird.name`` yields ['proj', 'ds',
|
|
69
|
+
// '`weird.name`'], not a spurious break inside the quoted table name.
|
|
70
|
+
function splitDotted(source) {
|
|
71
|
+
const parts = [];
|
|
72
|
+
let cur = '';
|
|
73
|
+
let quote = null;
|
|
74
|
+
for (const ch of source) {
|
|
75
|
+
if (quote) {
|
|
76
|
+
cur += ch;
|
|
77
|
+
if (ch === quote)
|
|
78
|
+
quote = null;
|
|
79
|
+
}
|
|
80
|
+
else if (ch === '`' || ch === '"') {
|
|
81
|
+
quote = ch;
|
|
82
|
+
cur += ch;
|
|
83
|
+
}
|
|
84
|
+
else if (ch === '.') {
|
|
85
|
+
parts.push(cur);
|
|
86
|
+
cur = '';
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
cur += ch;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
parts.push(cur);
|
|
93
|
+
return parts;
|
|
94
|
+
}
|
|
95
|
+
function unquote(part) {
|
|
96
|
+
return part.replace(/^[`"]/, '').replace(/[`"]$/, '');
|
|
97
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ApiResult } from '../gcp/api';
|
|
2
|
+
export interface EntryLookup {
|
|
3
|
+
lookupEntry(project: string, location: string, name: string): Promise<ApiResult<{
|
|
4
|
+
entryType?: string;
|
|
5
|
+
fullyQualifiedName?: string;
|
|
6
|
+
entrySource?: {
|
|
7
|
+
system?: string;
|
|
8
|
+
};
|
|
9
|
+
}>>;
|
|
10
|
+
}
|
|
11
|
+
export type Read<T> = {
|
|
12
|
+
result: T;
|
|
13
|
+
} | {
|
|
14
|
+
unreadable: true;
|
|
15
|
+
status: 403 | 404;
|
|
16
|
+
message?: string;
|
|
17
|
+
} | {
|
|
18
|
+
error: string;
|
|
19
|
+
status?: number;
|
|
20
|
+
};
|
|
21
|
+
export declare function read<T>(what: string, call: () => Promise<ApiResult<T>>): Promise<Read<T>>;
|
|
22
|
+
export declare function lookupSystemEntry(catalog: EntryLookup, system: string, project: string, location: string, resourceName: string): Promise<Read<{
|
|
23
|
+
entryType?: string;
|
|
24
|
+
}>>;
|