@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,341 @@
1
+ "use strict";
2
+ // Checks that Knowledge Catalog has an entry for every table a push reads
3
+ // (b/568769073), so a source that names no table stops the push before it
4
+ // writes anything. The same reads find each profile whose BigQuery and
5
+ // BigLake tables share no location (b/578688979).
6
+ //
7
+ // Each first-party table's entry sits in its system's entry group, with its
8
+ // resource name as the entry id, in the table's own location:
9
+ //
10
+ // - BigQuery: the table's location, which getTable reads, such as `US`. The
11
+ // table read needs only access to the table, not to its dataset.
12
+ // - BigLake: the region of one of the catalog's replicas, which getCatalog
13
+ // reads: `us-central1` for a regional catalog. A federated catalog lists
14
+ // one replica, such as `US`, with no state. A US catalog of Cloud Storage
15
+ // tables keeps a table's entry in the region that was its primary when the
16
+ // table was created, so after a failover an older table's entry can sit in
17
+ // a former primary's region, now a secondary's. The entry is looked up in
18
+ // each replica's region, the primary's first, until one finds it.
19
+ // getHiveCatalog reads a Hive catalog. Its tables are written with
20
+ // `databases/`.
21
+ // - Spanner: the default leader region of the instance's config, which
22
+ // getInstance reads the config's name from. A regional config is named for
23
+ // its region, `regional-us-central1` for us-central1, so it is not read;
24
+ // any other config is, with getInstanceConfig: `nam3` gives us-east4. That
25
+ // read needs spanner.instanceConfigs.get, which the Spanner database roles
26
+ // lack. When it is refused, the entry is looked up in each region the
27
+ // instance has replicas in, which getInstance lists, in the order listed,
28
+ // until one finds it: the default leader's region is one of them, and the
29
+ // list marks no leader (live, 2026-11-04). The instance's
30
+ // `resourceLocation`, such as `us`, is not a Dataplex region. A database's
31
+ // own default leader does not move the entry.
32
+ // - AlloyDB: the location the source names.
33
+ //
34
+ // Dataplex keeps locations in lower case. A lookup in the wrong location
35
+ // answers 403, as a missing entry does (live, 2026-10-27), so each location
36
+ // must be read, not guessed. Every read is a direct lookup: Dataplex search
37
+ // misses entries a lookup finds, and is not exact.
38
+ //
39
+ // When a location read answers 404, the source names a table, catalog or
40
+ // instance that does not exist. When it answers 403, the read was refused and
41
+ // the entry was never looked up, so the message says which read it was, and
42
+ // what the service said: Spanner answers 403 for a project the caller may not
43
+ // read, whether or not the instance exists, so a 403 does not say the table
44
+ // is missing. A refused Spanner instance config read is the exception: the
45
+ // entry is then looked up in each replica region the instance lists. A read
46
+ // that fails another way says to try again when a retry may pass (a thrown
47
+ // call, a 429 or a 5xx), and to fix the source otherwise.
48
+ //
49
+ // A Cloud SQL or third-party source is not looked up here: sourceDialects
50
+ // already looks up its entry directly, and fails the push when it is missing.
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.sourceEntryErrors = sourceEntryErrors;
53
+ const api_1 = require("../gcp/api");
54
+ const catalog_entry_1 = require("./catalog_entry");
55
+ const loader_1 = require("./loader");
56
+ const resolve_profiles_1 = require("./resolve_profiles");
57
+ // One message for each entity, in the inline bindings (named `default`) or in
58
+ // a profile, whose source names a table with no Knowledge Catalog entry, in
59
+ // that order. Then one message for each of those profiles whose BigQuery and
60
+ // BigLake tables share no location (b/578688979): BigQuery runs each query in
61
+ // one location, so no query through the profile can join them.
62
+ //
63
+ // A named profile is judged by its own sources, since push requires it to
64
+ // bind every concrete entity. A BigLake table can be queried in any replica
65
+ // region of its catalog. A table whose location could not be read is
66
+ // reported above and left out.
67
+ //
68
+ // Seen live in kc-looker-external, for a US catalog whose replicas are
69
+ // us-central1 (primary) and us-east1:
70
+ // - A job in US cannot read its tables (2026-11-03).
71
+ // - Once a table has replicated, which took a few minutes, a job in either
72
+ // region can read it, join it with a dataset in that region, and create a
73
+ // property graph over both (2026-11-04).
74
+ // - A job with no location runs in the primary.
75
+ //
76
+ // Reads each table's location and looks its entry up once per location it
77
+ // may sit in, and reads each instance once and each catalog at most once
78
+ // through each API; tables are checked in parallel, and a table's locations in
79
+ // turn. Skips an abstract entity, an entity with no source, and a source that
80
+ // names no table this module knows (a Cloud SQL or third-party table). Never
81
+ // throws.
82
+ async function sourceEntryErrors(model, readers) {
83
+ const uses = [];
84
+ const use = (entity, profile, source) => {
85
+ const table = source ? (0, loader_1.sourceTableOf)(source) : undefined;
86
+ if (table)
87
+ uses.push({ entity, profile, table });
88
+ };
89
+ for (const e of model.entities) {
90
+ if (!e.abstract)
91
+ use(e.name, resolve_profiles_1.DEFAULT_PROFILE, (0, loader_1.writtenSource)(e));
92
+ }
93
+ for (const p of model.profiles ?? []) {
94
+ for (const b of p.entities)
95
+ use(b.name, p.name, b.source);
96
+ }
97
+ const catalogAndInstanceReads = new Map();
98
+ const locations = new Map();
99
+ const problems = new Map();
100
+ for (const { table } of uses) {
101
+ if (!problems.has(table.resourceName)) {
102
+ const located = locationsOf(table, readers, catalogAndInstanceReads);
103
+ locations.set(table.resourceName, located);
104
+ problems.set(table.resourceName, problemOf(table, located, readers));
105
+ }
106
+ }
107
+ const errors = [];
108
+ // By profile, each list of regions BigQuery can query one of its tables in,
109
+ // keyed by the list, with the entities bound to such tables, in the order
110
+ // bound.
111
+ const placed = new Map();
112
+ for (const { entity, profile, table } of uses) {
113
+ const problem = await problems.get(table.resourceName);
114
+ if (problem) {
115
+ errors.push(`entity '${entity}' in profile '${profile}': source '${table.resourceName}' ${problem}`);
116
+ }
117
+ if (table.system !== 'bigquery' && table.system !== 'biglake')
118
+ continue;
119
+ const located = await locations.get(table.resourceName);
120
+ if (!('result' in located))
121
+ continue;
122
+ const regions = located.result;
123
+ const key = regions.join(' ');
124
+ const at = placed.get(profile) ?? new Map();
125
+ at.set(key, { regions, entities: [...at.get(key)?.entities ?? [], entity] });
126
+ placed.set(profile, at);
127
+ }
128
+ for (const [profile, at] of placed) {
129
+ const [first, ...rest] = at.values();
130
+ if (first.regions.some(r => rest.every(g => g.regions.includes(r)))) {
131
+ continue;
132
+ }
133
+ const listed = [...at.values()].map(({ regions, entities }) => `${regions.map(shown).join(' or ')} (${entities.map(e => `'${e}'`).join(', ')})`);
134
+ errors.push(`profile '${profile}' of model '${model.name}' binds tables that ` +
135
+ `share no BigQuery location: ${listed.join(', ')}. ` +
136
+ `BigQuery runs each query in one location, so no query can join ` +
137
+ `them. Bind them to tables that share a location, or pass ` +
138
+ `--skip-source-check.`);
139
+ }
140
+ return errors;
141
+ }
142
+ // What is wrong with `table`'s source, or undefined when its catalog entry
143
+ // is there. `located` is what locationsOf gave for `table`. Looks the entry
144
+ // up in each of its locations in turn, and stops at the first that finds it:
145
+ // a lookup in the wrong location answers 403, so trying each cannot pass a
146
+ // missing entry.
147
+ async function problemOf(table, located, readers) {
148
+ const locations = await located;
149
+ if ('problem' in locations)
150
+ return locations.problem;
151
+ for (const location of locations.result) {
152
+ const res = await (0, catalog_entry_1.lookupSystemEntry)(readers.catalog, table.system, table.project, location, table.resourceName);
153
+ if ('result' in res)
154
+ return undefined;
155
+ if ('error' in res)
156
+ return failedProblem(res).problem;
157
+ }
158
+ return `names no table Knowledge Catalog knows about. The table may not ` +
159
+ `exist, or you may not be allowed to read its catalog entry.${table.system === 'alloydb' ?
160
+ ' A new AlloyDB cluster can take hours to appear in the ' +
161
+ 'catalog after Dataplex integration is turned on.' :
162
+ ''}${locations.unfound ?? ''} Fix the source, or pass ` +
163
+ `--skip-source-check.`;
164
+ }
165
+ // `location`, which is kept in lower case, as BigQuery writes it: a
166
+ // multi-region, which has no hyphen, in upper case (`US`, `EU`).
167
+ function shown(location) {
168
+ return location.includes('-') ? location : location.toUpperCase();
169
+ }
170
+ // Said when reading an instance config is refused and the instance lists no
171
+ // replica regions to look in instead (live on bigquery-public-data and
172
+ // dataplex-staging-types, 2026-10-27: Spanner answers 403 naming this
173
+ // permission, even for a Google-managed config such as nam3).
174
+ const CONFIG_REFUSED_HINT = ' Reading it needs the permission ' +
175
+ 'spanner.instanceConfigs.get on the project, which roles/spanner.viewer ' +
176
+ 'includes and the Spanner database roles do not.';
177
+ // Said when reading an instance is refused: a Spanner database role gives
178
+ // spanner.instances.get only when granted on the instance or its project
179
+ // (from review on #630, 2026-11-04).
180
+ const INSTANCE_REFUSED_HINT = ' The Spanner database roles include ' +
181
+ 'spanner.instances.get, but a role granted on one database does not ' +
182
+ 'apply to its instance.';
183
+ // Said when a BigLake catalog read fails in a way a retry cannot fix and the
184
+ // other kind of catalog's API finds the catalog. The status alone does not
185
+ // tell: live, the Hive API answers 400 for an Iceberg REST catalog id with a
186
+ // hyphen and for a Hive catalog id in capitals.
187
+ const HIVE_FORM_HINT = ` A table in a Hive catalog is written with 'databases/<database>'.`;
188
+ const ICEBERG_FORM_HINT = ` A table in an Iceberg REST catalog is written ` +
189
+ `with 'namespaces/<namespace>'.`;
190
+ // The locations where `table`'s catalog entry may sit, as Dataplex writes
191
+ // them, in lower case, in the order to look in: one for a BigQuery or AlloyDB
192
+ // table; one for a Spanner table, or one per replica region of its instance
193
+ // when reading its config is refused; and one per replica for a BigLake table.
194
+ // `cache` keeps each instance's read and each catalog's read through each
195
+ // API, so each is made once; each table is read once already.
196
+ async function locationsOf(table, readers, cache) {
197
+ const once = (key, readIt) => {
198
+ if (!cache.has(key))
199
+ cache.set(key, readIt());
200
+ return cache.get(key);
201
+ };
202
+ const { project } = table;
203
+ switch (table.system) {
204
+ case 'alloydb':
205
+ return { result: [table.location] };
206
+ case 'bigquery': {
207
+ const what = `reading table '${project}.${table.dataset}.${table.table}'`;
208
+ const res = await readLocation(what, 'a table', () => readers.bigquery.getTable(project, table.dataset, table.table));
209
+ return 'result' in res ?
210
+ located(res.result.location, what, 'no location') :
211
+ res;
212
+ }
213
+ case 'biglake': {
214
+ const readCatalog = (hive) => once(`biglake/${project}/${table.catalog}/${hive}`, () => hive ? readers.biglake.getHiveCatalog(project, table.catalog) :
215
+ readers.biglake.getCatalog(project, table.catalog));
216
+ const kind = table.hive ? 'Hive catalog' : 'catalog';
217
+ const what = `reading the region of ${kind} '${table.catalog}'`;
218
+ // When the read fails and the other API finds the catalog, the source
219
+ // names a table in the other kind of catalog.
220
+ const otherForm = async () => 'result' in await (0, catalog_entry_1.read)(what, () => readCatalog(!table.hive)) ?
221
+ (table.hive ? ICEBERG_FORM_HINT : HIVE_FORM_HINT) :
222
+ '';
223
+ const res = await readLocation(what, `a ${kind}`, () => readCatalog(table.hive), otherForm);
224
+ return 'result' in res ? replicaRegions(res.result, what) : res;
225
+ }
226
+ case 'spanner':
227
+ return once(`spanner/${project}/${table.instance}`, async () => {
228
+ const what = `reading instance '${table.instance}'`;
229
+ const instance = await readLocation(what, 'an instance', () => readers.spanner.getInstance(project, table.instance), status => status === 403 ? INSTANCE_REFUSED_HINT : '');
230
+ if ('problem' in instance)
231
+ return instance;
232
+ const configName = instance.result.config;
233
+ if (!configName) {
234
+ return transientProblem(`${what} gave no instance config.`);
235
+ }
236
+ // A regional config keeps every replica, so its leader, in the region
237
+ // it is named for: `regional-us-central1` leads in us-central1 (live,
238
+ // 2026-10-27).
239
+ const region = configName.match(/\/instanceConfigs\/regional-(.+)$/);
240
+ if (region)
241
+ return { result: [region[1]] };
242
+ const configWhat = `reading instance config '${configName}'`;
243
+ const res = await (0, catalog_entry_1.read)(configWhat, () => readers.spanner.getInstanceConfig(configName));
244
+ // The default leader's region is one of the instance's replica
245
+ // regions, so when the config read is refused, each is looked in.
246
+ if ('unreadable' in res && res.status === 403) {
247
+ const regions = (instance.result.replicaComputeCapacity ?? [])
248
+ .map(r => r.replicaSelection?.location)
249
+ .filter((l) => !!l);
250
+ if (regions.length) {
251
+ return {
252
+ result: regions,
253
+ unfound: ` Instance config '${configName}' could not be read ` +
254
+ `(HTTP 403), so the entry was looked up in each of the ` +
255
+ `instance's replica regions: ${regions.join(', ')}.`,
256
+ };
257
+ }
258
+ }
259
+ const config = await checked(res, configWhat, 'an instance config', status => status === 403 ? CONFIG_REFUSED_HINT : '');
260
+ // A config marks exactly one replica as its default leader.
261
+ return 'result' in config ?
262
+ located(config.result.replicas?.find(r => r.defaultLeaderLocation)
263
+ ?.location, configWhat, 'no default leader') :
264
+ config;
265
+ });
266
+ }
267
+ }
268
+ // Makes the location read `call`, which `what` names, and judges its answer
269
+ // with checked.
270
+ async function readLocation(what, kind, call, hintFor) {
271
+ return checked(await (0, catalog_entry_1.read)(what, call), what, kind, hintFor);
272
+ }
273
+ // What the location read `what` gave in `res`. A 404 says the source names
274
+ // `kind`, such as `a table`, that does not exist (the services answer 404 for
275
+ // a missing table, dataset, catalog or instance; live, 2026-10-27 and
276
+ // 2026-11-03). A 403 says the read was refused, followed by the service's
277
+ // message. When a retry cannot pass, `hintFor` gives a sentence to add for the
278
+ // status.
279
+ async function checked(res, what, kind, hintFor = () => '') {
280
+ if ('result' in res)
281
+ return res;
282
+ const { status } = res;
283
+ const hint = status === undefined || (0, api_1.isTransientStatus)(status) ?
284
+ '' :
285
+ await hintFor(status);
286
+ if ('error' in res)
287
+ return failedProblem(res, hint);
288
+ return {
289
+ problem: res.status === 404 ?
290
+ `names ${kind} that does not exist: ${what} found nothing (HTTP ` +
291
+ `404).${hint} Fix the source, or pass --skip-source-check.` :
292
+ `could not be checked: ${what} was refused (HTTP 403).${res.message ? ` ${res.message}` : ''}${hint} The ` +
293
+ `source's catalog entry was not looked up. Fix the access or ` +
294
+ `the source, or pass --skip-source-check.`,
295
+ };
296
+ }
297
+ // The regions of `catalog`'s replicas, in the order to look in: a replica
298
+ // marked STATE_PRIMARY first, then one in another primary state, such as
299
+ // STATE_PRIMARY_IN_PROGRESS, then the rest, each group in the order the
300
+ // replicas come. A regional or federated catalog lists one replica with no
301
+ // state.
302
+ //
303
+ // After a failover, a table's entry stays only in the region that was the
304
+ // catalog's primary when the table was created, until the table is next
305
+ // written; for a few seconds during a failover the new primary reads
306
+ // STATE_PRIMARY_IN_PROGRESS, so no replica is STATE_PRIMARY (from review on
307
+ // #623, 2026-11-03; not tested live by us).
308
+ function replicaRegions(catalog, what) {
309
+ const rank = (state = '') => state === 'STATE_PRIMARY' ? 0 : state.includes('PRIMARY') ? 1 : 2;
310
+ const regions = (catalog.replicas ?? [])
311
+ .filter(r => r.region)
312
+ .sort((a, b) => rank(a.state) - rank(b.state))
313
+ .map(r => r.region.toLowerCase());
314
+ return regions.length ? { result: regions } :
315
+ transientProblem(`${what} gave no region.`);
316
+ }
317
+ // `location` in lower case, or a problem when the read `what` gave none.
318
+ // The services have always given one; this keeps a response without it from
319
+ // stopping the push with a crash.
320
+ function located(location, what, missing) {
321
+ return location ? { result: [location.toLowerCase()] } :
322
+ transientProblem(`${what} gave ${missing}.`);
323
+ }
324
+ // The problem for the failed read `res`. A thrown call, a 429 or a 5xx may
325
+ // pass on retry; any other status, such as BigQuery's 400 for an invalid
326
+ // table ID, never will, so the source must change, and `hint` follows the
327
+ // service's answer.
328
+ function failedProblem(res, hint = '') {
329
+ return res.status === undefined || (0, api_1.isTransientStatus)(res.status) ?
330
+ transientProblem(res.error) :
331
+ {
332
+ problem: `could not be checked: ${res.error}${hint} Fix the source, ` +
333
+ `or pass --skip-source-check.`,
334
+ };
335
+ }
336
+ // The problem for a read that failed with `detail`, which may pass on retry.
337
+ function transientProblem(detail) {
338
+ return {
339
+ problem: `could not be checked; try again, or pass --skip-source-check: ${detail}`,
340
+ };
341
+ }
@@ -0,0 +1,18 @@
1
+ import { GenerateResult } from './graph_ddl';
2
+ import { SemanticModel } from './ir';
3
+ export interface GenerateOptions {
4
+ graphName?: string;
5
+ }
6
+ export type { GenerateResult };
7
+ /**
8
+ * Generates the Spanner Graph DDL for a semantic model.
9
+ *
10
+ * The generated statement references the entities' existing Spanner tables; it
11
+ * does not create them. Returns the DDL plus any warnings collected while
12
+ * mapping the IR (e.g. metrics dropped because Spanner Graph has no measure).
13
+ *
14
+ * Throws when the model yields no valid node table -- it declares no entities,
15
+ * or every entity lacks a KEY -- since a property graph with an empty NODE
16
+ * TABLES block is invalid DDL that Spanner would reject.
17
+ */
18
+ export declare function generateSpannerPropertyGraph(model: SemanticModel, opts?: GenerateOptions): GenerateResult;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ // Generates Spanner Graph DDL from the Semantic Model IR.
3
+ //
4
+ // The shared GoogleSQL generator (./graph_ddl) writes the graph structure. This
5
+ // module supplies its Spanner engine: bare table and graph names, no measures,
6
+ // no per-element OPTIONS, and a field's SPANNER text, else its ANSI_SQL text.
7
+ //
8
+ // See:
9
+ // https://docs.cloud.google.com/spanner/docs/reference/standard-sql/graph-schema-statements
10
+ //
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.generateSpannerPropertyGraph = generateSpannerPropertyGraph;
13
+ const binding_1 = require("./binding");
14
+ const graph_ddl_1 = require("./graph_ddl");
15
+ const ir_1 = require("./ir");
16
+ const sql_expr_utils_1 = require("./sql_expr_utils");
17
+ const sql_identifiers_1 = require("./sql_identifiers");
18
+ /**
19
+ * Generates the Spanner Graph DDL for a semantic model.
20
+ *
21
+ * The generated statement references the entities' existing Spanner tables; it
22
+ * does not create them. Returns the DDL plus any warnings collected while
23
+ * mapping the IR (e.g. metrics dropped because Spanner Graph has no measure).
24
+ *
25
+ * Throws when the model yields no valid node table -- it declares no entities,
26
+ * or every entity lacks a KEY -- since a property graph with an empty NODE
27
+ * TABLES block is invalid DDL that Spanner would reject.
28
+ */
29
+ function generateSpannerPropertyGraph(model, opts = {}) {
30
+ return (0, graph_ddl_1.generateGraphDdl)(model, spannerEngine(opts));
31
+ }
32
+ // The Spanner engine for the shared generator.
33
+ function spannerEngine(opts) {
34
+ return {
35
+ name: 'Spanner',
36
+ table: binding_1.spannerTable,
37
+ graphName: model => (0, sql_identifiers_1.quoteIdentifier)(opts.graphName ?? model.name),
38
+ placeMetrics: (metrics, _input, warnings) => skipMetrics(metrics, warnings),
39
+ elementOptions: () => undefined,
40
+ fieldOptions: () => undefined,
41
+ dialect: 'SPANNER',
42
+ fieldExpression,
43
+ };
44
+ }
45
+ // Warns that each metric is dropped, since Spanner Graph has no MEASURE.
46
+ function skipMetrics(metrics, warnings) {
47
+ for (const metric of metrics) {
48
+ warnings.push(`metric '${metric.name}' is not emitted: Spanner Graph has no ` +
49
+ `MEASURE, so model-level metrics have no home in it`);
50
+ }
51
+ return new Map();
52
+ }
53
+ // The field's SPANNER text, else its ANSI_SQL text, read as Spanner SQL with
54
+ // its comments removed. A line comment left in would swallow the ` AS <name>`
55
+ // and comma the DDL writes after the expression.
56
+ function fieldExpression(f) {
57
+ const text = (0, ir_1.expressionForDialect)(f, 'SPANNER');
58
+ return text === undefined ? undefined :
59
+ (0, sql_expr_utils_1.sqlWithoutComments)(text, 'SPANNER');
60
+ }
@@ -0,0 +1,23 @@
1
+ import { Field } from './ir';
2
+ import { SqlColumn } from './sql_parser';
3
+ export declare function unusedName(text: string, base: string): string;
4
+ export declare function withoutQualifier(expression: string, qualifier: string, dialect?: string): string;
5
+ export declare function withSiblingsSpliced(expression: string, qualifier: string, siblings: ReadonlyMap<string, string>): string;
6
+ export declare function qualifiedColumns(expression: string, qualifier: string, dialect?: string): string[] | undefined;
7
+ export declare function selfReferenceMessage(owner: string, cycle: string[], where?: string): string;
8
+ export declare function requalified(expression: string, from: string, to: string, dialect?: string): string;
9
+ export declare function measureEntities(expression: string, entityNames: ReadonlyArray<string>, entity?: string): string[];
10
+ export declare function sqlWithoutComments(text: string, dialect?: string): string;
11
+ export declare function fieldBindingWithoutComments(field: Pick<Field, 'expression' | 'importedExpression' | 'importedDialect'>, targetDialect?: string): string | undefined;
12
+ export declare function columnReferences(expression: string, dialect: string): ReadonlyArray<SqlColumn> | undefined;
13
+ export declare function columnsForSource(text: string, dialect: string, impliedDialect?: string | null): ReadonlyArray<SqlColumn> | undefined;
14
+ export declare function entitiesReached(columns: ReadonlyArray<SqlColumn>, entityNames: ReadonlyArray<string> | ReadonlySet<string>): string[];
15
+ export declare function fieldsReadOn(columns: ReadonlyArray<SqlColumn>, entityName: string): string[];
16
+ export declare const SUPPORTED_AGGREGATES: string[];
17
+ export declare function singleAggregate(expression: string, dialect: string): {
18
+ fn: string;
19
+ operand: string;
20
+ distinct: boolean;
21
+ } | undefined;
22
+ export declare function isSingleAggregate(expression: string, dialect: string): boolean;
23
+ export declare function isColumnName(value: unknown): boolean;