@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.
Files changed (150) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +264 -2
  3. package/build/ts/kcmd/cli.js +20252 -0
  4. package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
  5. package/build/ts/kcmd/gcp/alloydb.js +70 -0
  6. package/build/ts/kcmd/gcp/api.d.ts +29 -0
  7. package/build/ts/kcmd/gcp/api.js +162 -0
  8. package/build/ts/kcmd/gcp/biglake.d.ts +18 -0
  9. package/build/ts/kcmd/gcp/biglake.js +59 -0
  10. package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
  11. package/build/ts/kcmd/gcp/bigquery.js +134 -0
  12. package/build/ts/kcmd/gcp/context.d.ts +10 -0
  13. package/build/ts/kcmd/gcp/context.js +74 -0
  14. package/build/ts/kcmd/gcp/crm.d.ts +13 -0
  15. package/build/ts/kcmd/gcp/crm.js +91 -0
  16. package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
  17. package/build/ts/kcmd/gcp/dataplex.js +406 -0
  18. package/build/ts/kcmd/gcp/index.d.ts +3 -0
  19. package/build/ts/kcmd/gcp/index.js +21 -0
  20. package/build/ts/kcmd/gcp/spanner.d.ts +73 -0
  21. package/build/ts/kcmd/gcp/spanner.js +126 -0
  22. package/build/ts/kcmd/index.d.ts +7 -0
  23. package/build/ts/kcmd/index.js +46 -0
  24. package/build/ts/kcmd/layout.d.ts +16 -0
  25. package/build/ts/kcmd/layout.js +31 -0
  26. package/build/ts/kcmd/layouts/documents.d.ts +24 -0
  27. package/build/ts/kcmd/layouts/documents.js +264 -0
  28. package/build/ts/kcmd/layouts/okf.d.ts +31 -0
  29. package/build/ts/kcmd/layouts/okf.js +386 -0
  30. package/build/ts/kcmd/layouts/semantic-model.d.ts +74 -0
  31. package/build/ts/kcmd/layouts/semantic-model.js +621 -0
  32. package/build/ts/kcmd/layouts/standard.d.ts +13 -0
  33. package/build/ts/kcmd/layouts/standard.js +108 -0
  34. package/build/ts/kcmd/manifest.d.ts +34 -0
  35. package/build/ts/kcmd/manifest.js +262 -0
  36. package/build/ts/kcmd/metadata.d.ts +25 -0
  37. package/build/ts/kcmd/metadata.js +8 -0
  38. package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
  39. package/build/ts/kcmd/okf/catalog_residue.js +124 -0
  40. package/build/ts/kcmd/okf/concept.d.ts +14 -0
  41. package/build/ts/kcmd/okf/concept.js +110 -0
  42. package/build/ts/kcmd/okf/concept_to_entry.d.ts +10 -0
  43. package/build/ts/kcmd/okf/concept_to_entry.js +281 -0
  44. package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
  45. package/build/ts/kcmd/okf/init_okf_workspace.js +113 -0
  46. package/build/ts/kcmd/okf/links.d.ts +23 -0
  47. package/build/ts/kcmd/okf/links.js +468 -0
  48. package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
  49. package/build/ts/kcmd/okf/okf_metadata.js +99 -0
  50. package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
  51. package/build/ts/kcmd/semantic/bigquery.js +469 -0
  52. package/build/ts/kcmd/semantic/binding.d.ts +3 -0
  53. package/build/ts/kcmd/semantic/binding.js +92 -0
  54. package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
  55. package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
  56. package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
  57. package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
  58. package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
  59. package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
  60. package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
  61. package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
  62. package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
  63. package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
  64. package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
  65. package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
  66. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +104 -0
  67. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1367 -0
  68. package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
  69. package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
  70. package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
  71. package/build/ts/kcmd/semantic/deployment_target.js +379 -0
  72. package/build/ts/kcmd/semantic/graph_ddl.d.ts +44 -0
  73. package/build/ts/kcmd/semantic/graph_ddl.js +656 -0
  74. package/build/ts/kcmd/semantic/import_model.d.ts +72 -0
  75. package/build/ts/kcmd/semantic/import_model.js +1698 -0
  76. package/build/ts/kcmd/semantic/ir.d.ts +589 -0
  77. package/build/ts/kcmd/semantic/ir.js +247 -0
  78. package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
  79. package/build/ts/kcmd/semantic/kc_actions.js +531 -0
  80. package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
  81. package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
  82. package/build/ts/kcmd/semantic/kc_converter.d.ts +37 -0
  83. package/build/ts/kcmd/semantic/kc_converter.js +839 -0
  84. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +63 -0
  85. package/build/ts/kcmd/semantic/kc_custom_types.js +751 -0
  86. package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
  87. package/build/ts/kcmd/semantic/kc_entries.js +284 -0
  88. package/build/ts/kcmd/semantic/kc_ids.d.ts +102 -0
  89. package/build/ts/kcmd/semantic/kc_ids.js +188 -0
  90. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +29 -0
  91. package/build/ts/kcmd/semantic/knowledge_catalog.js +960 -0
  92. package/build/ts/kcmd/semantic/loader.d.ts +258 -0
  93. package/build/ts/kcmd/semantic/loader.js +3576 -0
  94. package/build/ts/kcmd/semantic/names.d.ts +10 -0
  95. package/build/ts/kcmd/semantic/names.js +85 -0
  96. package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
  97. package/build/ts/kcmd/semantic/osi_converter.js +910 -0
  98. package/build/ts/kcmd/semantic/parameters.d.ts +19 -0
  99. package/build/ts/kcmd/semantic/parameters.js +171 -0
  100. package/build/ts/kcmd/semantic/pull_kc.d.ts +10 -0
  101. package/build/ts/kcmd/semantic/pull_kc.js +272 -0
  102. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +59 -0
  103. package/build/ts/kcmd/semantic/resolve_inheritance.js +346 -0
  104. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +291 -0
  105. package/build/ts/kcmd/semantic/resolve_profiles.js +2003 -0
  106. package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
  107. package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
  108. package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
  109. package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
  110. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +88 -0
  111. package/build/ts/kcmd/semantic/runtime/runtime.js +301 -0
  112. package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
  113. package/build/ts/kcmd/semantic/runtime/store.js +344 -0
  114. package/build/ts/kcmd/semantic/skills.d.ts +110 -0
  115. package/build/ts/kcmd/semantic/skills.js +697 -0
  116. package/build/ts/kcmd/semantic/source_dialect.d.ts +17 -0
  117. package/build/ts/kcmd/semantic/source_dialect.js +311 -0
  118. package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
  119. package/build/ts/kcmd/semantic/source_entries.js +341 -0
  120. package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
  121. package/build/ts/kcmd/semantic/spanner.js +60 -0
  122. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +23 -0
  123. package/build/ts/kcmd/semantic/sql_expr_utils.js +394 -0
  124. package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
  125. package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
  126. package/build/ts/kcmd/semantic/sql_parser.d.ts +47 -0
  127. package/build/ts/kcmd/semantic/sql_parser.js +400 -0
  128. package/build/ts/kcmd/semantic/transpile.d.ts +76 -0
  129. package/build/ts/kcmd/semantic/transpile.js +581 -0
  130. package/build/ts/kcmd/semantic/validate.d.ts +70 -0
  131. package/build/ts/kcmd/semantic/validate.js +2266 -0
  132. package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
  133. package/build/ts/kcmd/semantic/yaml_options.js +58 -0
  134. package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
  135. package/build/ts/kcmd/semantic/yaml_update.js +782 -0
  136. package/build/ts/kcmd/snapshot.d.ts +37 -0
  137. package/build/ts/kcmd/snapshot.js +386 -0
  138. package/build/ts/kcmd/source.d.ts +31 -0
  139. package/build/ts/kcmd/source.js +119 -0
  140. package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
  141. package/build/ts/kcmd/sources/bq-dataset.js +131 -0
  142. package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
  143. package/build/ts/kcmd/sources/entrygroup.js +83 -0
  144. package/build/ts/kcmd/sources/kb.d.ts +21 -0
  145. package/build/ts/kcmd/sources/kb.js +82 -0
  146. package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
  147. package/build/ts/kcmd/sources/semantic-model.js +48 -0
  148. package/build/ts/kcmd/sync.d.ts +35 -0
  149. package/build/ts/kcmd/sync.js +734 -0
  150. 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
+ }