@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.
Files changed (150) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +266 -2
  3. package/build/ts/kcmd/cli.js +19578 -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 +16 -0
  9. package/build/ts/kcmd/gcp/biglake.js +50 -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 +63 -0
  21. package/build/ts/kcmd/gcp/spanner.js +123 -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 +344 -0
  30. package/build/ts/kcmd/layouts/semantic-model.d.ts +61 -0
  31. package/build/ts/kcmd/layouts/semantic-model.js +542 -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 +104 -0
  42. package/build/ts/kcmd/okf/concept_to_entry.d.ts +8 -0
  43. package/build/ts/kcmd/okf/concept_to_entry.js +254 -0
  44. package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
  45. package/build/ts/kcmd/okf/init_okf_workspace.js +115 -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 +82 -0
  50. package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
  51. package/build/ts/kcmd/semantic/bigquery.js +525 -0
  52. package/build/ts/kcmd/semantic/binding.d.ts +3 -0
  53. package/build/ts/kcmd/semantic/binding.js +97 -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 +129 -0
  67. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1396 -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 +34 -0
  73. package/build/ts/kcmd/semantic/graph_ddl.js +563 -0
  74. package/build/ts/kcmd/semantic/import_model.d.ts +50 -0
  75. package/build/ts/kcmd/semantic/import_model.js +1275 -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 +31 -0
  83. package/build/ts/kcmd/semantic/kc_converter.js +824 -0
  84. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +70 -0
  85. package/build/ts/kcmd/semantic/kc_custom_types.js +828 -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 +103 -0
  89. package/build/ts/kcmd/semantic/kc_ids.js +191 -0
  90. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +32 -0
  91. package/build/ts/kcmd/semantic/knowledge_catalog.js +1043 -0
  92. package/build/ts/kcmd/semantic/loader.d.ts +262 -0
  93. package/build/ts/kcmd/semantic/loader.js +3440 -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 +911 -0
  98. package/build/ts/kcmd/semantic/parameters.d.ts +18 -0
  99. package/build/ts/kcmd/semantic/parameters.js +152 -0
  100. package/build/ts/kcmd/semantic/pull_kc.d.ts +14 -0
  101. package/build/ts/kcmd/semantic/pull_kc.js +346 -0
  102. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +54 -0
  103. package/build/ts/kcmd/semantic/resolve_inheritance.js +330 -0
  104. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +250 -0
  105. package/build/ts/kcmd/semantic/resolve_profiles.js +1822 -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 +94 -0
  111. package/build/ts/kcmd/semantic/runtime/runtime.js +318 -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 +10 -0
  117. package/build/ts/kcmd/semantic/source_dialect.js +261 -0
  118. package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
  119. package/build/ts/kcmd/semantic/source_entries.js +282 -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 +17 -0
  123. package/build/ts/kcmd/semantic/sql_expr_utils.js +221 -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 +44 -0
  127. package/build/ts/kcmd/semantic/sql_parser.js +378 -0
  128. package/build/ts/kcmd/semantic/transpile.d.ts +74 -0
  129. package/build/ts/kcmd/semantic/transpile.js +563 -0
  130. package/build/ts/kcmd/semantic/validate.d.ts +35 -0
  131. package/build/ts/kcmd/semantic/validate.js +1899 -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 +33 -0
  137. package/build/ts/kcmd/snapshot.js +379 -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 +697 -0
  150. package/package.json +62 -4
@@ -0,0 +1,10 @@
1
+ import { EntryLookup } from './catalog_entry';
2
+ export interface SourceDialects {
3
+ dialects: Map<string, string>;
4
+ databases: Map<string, string>;
5
+ errors: string[];
6
+ }
7
+ export declare function sourceDialects(texts: Iterable<string>, catalog: EntryLookup, { thirdParty }?: {
8
+ thirdParty?: boolean | undefined;
9
+ }): Promise<SourceDialects>;
10
+ export declare function sourcesToLookUp(texts: Iterable<string>): string[];
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ // The dialect of a Cloud SQL table written as its resource name, or of a
3
+ // third-party table written as its entry name, read from the table's
4
+ // Knowledge Catalog entry.
5
+ //
6
+ // The resource name cannot say the dialect: a MySQL table has no schema, but a
7
+ // PostgreSQL table and a SQL Server table have the same shape. Dataplex keeps an
8
+ // entry for each table of an instance with Dataplex integration on, in the
9
+ // system entry group `@cloudsql`, and the entry's id is the table's resource
10
+ // name. Its entry type names the engine.
11
+ //
12
+ // A third-party table's entry names its system twice: in the prefix of its
13
+ // fullyQualifiedName, which Dataplex checks, and in entrySource.system, which
14
+ // is free text. So the prefix decides, and the system only when the entry has
15
+ // no fullyQualifiedName. Dataplex also checks the rest of the
16
+ // fullyQualifiedName, so its parts say whether the entry is a table's or, for
17
+ // example, a schema's.
18
+ //
19
+ // This is the only place kcmd makes a network call to learn a dialect. A
20
+ // command looks every such source up once, before it loads the model, and
21
+ // passes the dialects to the load (LoadOptions.sourceDialects).
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.sourceDialects = sourceDialects;
57
+ exports.sourcesToLookUp = sourcesToLookUp;
58
+ const yaml = __importStar(require("yaml"));
59
+ const loader_1 = require("./loader");
60
+ const catalog_entry_1 = require("./catalog_entry");
61
+ const yaml_options_1 = require("./yaml_options");
62
+ // The dialect each entrySource.system gives, in lower case.
63
+ const DIALECT_OF_ENTRY_SYSTEM = new Map([...loader_1.DIALECT_OF_FQN_PREFIX, ['postgres', 'POSTGRES']]);
64
+ // The parts of a table's fullyQualifiedName after each prefix, as Dataplex
65
+ // reads it. A view's name has the same parts as a table's.
66
+ const TABLE_FQN_PARTS = new Map([
67
+ ['snowflake', ['account', 'database', 'schema', 'table']],
68
+ ['databricks', ['metastore', 'catalog', 'schema', 'table']],
69
+ ['mysql', ['host', 'schema', 'table']],
70
+ ['postgresql', ['host', 'database', 'schema', 'table']],
71
+ ['sqlserver', ['host', 'database', 'schema', 'table']],
72
+ ]);
73
+ // Looks up the dialect of every Cloud SQL and third-party table the model and
74
+ // profile files `texts` write (see sourcesToLookUp), once per table, in
75
+ // parallel. With `thirdParty` false, looks up Cloud SQL tables only. Makes no
76
+ // call when they write none. Never throws: a lookup that fails is reported in
77
+ // `errors`.
78
+ async function sourceDialects(texts, catalog, { thirdParty = true } = {}) {
79
+ // Keyed as dialectLookupKey gives, so both spellings of one table are one
80
+ // lookup.
81
+ const lookups = new Map();
82
+ for (const source of sourcesToLookUp(texts)) {
83
+ const table = (0, loader_1.cloudSqlTableOf)(source);
84
+ if (table) {
85
+ lookups.set(table.resourceName, () => cloudSqlDialect(catalog, table));
86
+ }
87
+ const entry = thirdParty ? (0, loader_1.thirdPartyEntryOf)(source) : undefined;
88
+ if (entry)
89
+ lookups.set(entry.name, () => thirdPartyDialect(catalog, entry));
90
+ }
91
+ const dialects = new Map();
92
+ const databases = new Map();
93
+ const errors = [];
94
+ await Promise.all([...lookups].map(async ([key, lookUp]) => {
95
+ const got = await lookUp();
96
+ if ('error' in got)
97
+ errors.push(got.error);
98
+ else
99
+ dialects.set(key, got.dialect);
100
+ if ('database' in got && got.database)
101
+ databases.set(key, got.database);
102
+ }));
103
+ errors.sort();
104
+ return { dialects, databases, errors };
105
+ }
106
+ // The dialect of a Cloud SQL table, by its entry's type.
107
+ async function cloudSqlDialect(catalog, table) {
108
+ const at = `source '${table.resourceName}'`;
109
+ const res = await (0, catalog_entry_1.lookupSystemEntry)(catalog, 'cloudsql', table.project, table.location, table.resourceName);
110
+ if ('error' in res)
111
+ return { error: `${at}: ${res.error}` };
112
+ // Dataplex answers 403 both when the entry does not exist and when the
113
+ // caller may not read it, so the message names both causes.
114
+ if ('unreadable' in res) {
115
+ return {
116
+ error: `${at}: kcmd reads a Cloud SQL table's dialect from its ` +
117
+ `Knowledge Catalog entry, and could not read it. Turn on Dataplex ` +
118
+ `integration for instance '${table.instance}' (gcloud sql ` +
119
+ `instances patch ${table.instance} --enable-dataplex-integration ` +
120
+ `--project ${table.project}) and wait for the table to appear in ` +
121
+ `the catalog, or check that you can read the table. If integration ` +
122
+ `is on, check the name: the project ID, the instance's region, and ` +
123
+ `a schema only for PostgreSQL and SQL Server.`,
124
+ };
125
+ }
126
+ // The type comes back as a full name; its last part says the engine.
127
+ const type = (res.result.entryType ?? '').split('/').pop() ?? '';
128
+ const dialect = loader_1.DIALECT_OF_ENTRY_TYPE[type];
129
+ return dialect ? { dialect } : {
130
+ error: `${at}: its Knowledge Catalog entry has type '${type}', which is ` +
131
+ `not a Cloud SQL table or view type kcmd knows (${Object.keys(loader_1.DIALECT_OF_ENTRY_TYPE).join(', ')}).`,
132
+ };
133
+ }
134
+ // The dialect of a third-party table, by its entry's fullyQualifiedName or
135
+ // entrySource.system, and the database the fullyQualifiedName names. An entry
136
+ // whose fullyQualifiedName names no table or view is rejected.
137
+ async function thirdPartyDialect(catalog, { project, location, name }) {
138
+ const at = `source '${name}'`;
139
+ const res = await (0, catalog_entry_1.read)('looking up its Knowledge Catalog entry', () => catalog.lookupEntry(project, location, name));
140
+ if ('error' in res)
141
+ return { error: `${at}: ${res.error}` };
142
+ if ('unreadable' in res) {
143
+ return {
144
+ error: `${at} names no table Knowledge Catalog knows about. The entry ` +
145
+ `may not exist, or you may not be allowed to read it. kcmd reads a ` +
146
+ `third-party table's dialect from its entry, so add the table to ` +
147
+ `Knowledge Catalog, or fix the source.`,
148
+ };
149
+ }
150
+ const { fullyQualifiedName: fqn, entrySource } = res.result;
151
+ const system = entrySource?.system;
152
+ const prefix = fqn?.match(/^([a-z]+):/)?.[1] ?? '';
153
+ const dialect = fqn ? loader_1.DIALECT_OF_FQN_PREFIX.get(prefix) :
154
+ DIALECT_OF_ENTRY_SYSTEM.get(system?.toLowerCase() ?? '');
155
+ if (dialect && fqn) {
156
+ const database = databaseOfFqn(fqn);
157
+ if (database)
158
+ return { dialect, database };
159
+ const parts = TABLE_FQN_PARTS.get(prefix).map(p => `<${p}>`).join('.');
160
+ return {
161
+ error: `${at}: its Knowledge Catalog entry has fullyQualifiedName ` +
162
+ `'${fqn}', which names no table or view. A table's ` +
163
+ `fullyQualifiedName has the form ${prefix}:${parts}. Write the ` +
164
+ `entry name of the table's own entry.`,
165
+ };
166
+ }
167
+ if (dialect)
168
+ return { dialect };
169
+ const has = fqn ? `fullyQualifiedName '${fqn}'` :
170
+ system ? `entrySource.system '${system}' and no fullyQualifiedName` :
171
+ 'neither a fullyQualifiedName nor an entrySource.system';
172
+ return {
173
+ error: `${at}: kcmd cannot tell the table's SQL dialect from its ` +
174
+ `Knowledge Catalog entry, which has ${has}. kcmd reads the prefix of ` +
175
+ `the entry's fullyQualifiedName (snowflake:, databricks:, mysql:, ` +
176
+ `postgresql: or sqlserver:), or entrySource.system when there is no ` +
177
+ `fullyQualifiedName.`,
178
+ };
179
+ }
180
+ // The database the fullyQualifiedName `fqn` of a third-party table or view
181
+ // names: a Snowflake account, a MySQL or SQL Server host, a PostgreSQL host
182
+ // and database, or a Databricks metastore. Each account and host is in lower
183
+ // case. Undefined when `fqn` names no table or view. Such a name has another
184
+ // number of parts than TABLE_FQN_PARTS gives, or a subtype, such as the
185
+ // `routine:` of a routine. Dataplex reads a `databricks:table:` or
186
+ // `databricks:view:` name as the same table as the `databricks:` name.
187
+ function databaseOfFqn(fqn) {
188
+ // The subtype holds no backtick, so a colon inside a quoted host and port
189
+ // stays in the path. A colon in an unquoted database, schema or table
190
+ // name stays in its segment when it comes after the name's first backtick.
191
+ const [, prefix, subtype, path] = fqn.match(/^([a-z]+):(?:([a-z0-9_-]+):)?(.*)$/s) ?? [];
192
+ const parts = TABLE_FQN_PARTS.get(prefix ?? '');
193
+ const seg = (0, loader_1.catalogNameSegments)(path ?? '', true);
194
+ const tableSubtype = subtype === undefined ||
195
+ (prefix === 'databricks' && (subtype === 'table' || subtype === 'view'));
196
+ if (!parts || !tableSubtype || seg.length !== parts.length)
197
+ return undefined;
198
+ const n = prefix === 'postgresql' ? 2 : 1;
199
+ // A Snowflake account and a host name ignore letter case.
200
+ if (prefix !== 'databricks')
201
+ seg[0] = seg[0].toLowerCase();
202
+ return [prefix, ...seg.slice(0, n)].join('/');
203
+ }
204
+ // Every Cloud SQL table and third-party entry the model and profile files
205
+ // `texts` write. A Cloud SQL table counts as a resource name or resource URI
206
+ // anywhere, JSON strings included. An entry name counts as a `source` value in
207
+ // the YAML or in a GOOGLE block's JSON `data`, unless it sits in ai_context,
208
+ // an action parameter's default or another vendor's block. Values are read
209
+ // from the parsed YAML, so a name in a comment is not collected. A file that
210
+ // does not parse collects nothing; the load reports it.
211
+ function sourcesToLookUp(texts) {
212
+ const found = new Set();
213
+ // `key` names the member that holds `value`. `opaque` marks a value whose
214
+ // `source` names no table: one inside ai_context, an action parameter's
215
+ // default or another vendor's block, or in JSON written anywhere but a
216
+ // `data` member.
217
+ const visit = (value, key, opaque = false) => {
218
+ if (typeof value === 'string') {
219
+ if ((0, loader_1.cloudSqlTableOf)(value) ||
220
+ (key === 'source' && !opaque && (0, loader_1.thirdPartyEntryOf)(value))) {
221
+ found.add(value.trim());
222
+ }
223
+ else if (/^\s*[[{]/.test(value)) {
224
+ try {
225
+ visit(JSON.parse(value), undefined, opaque || key !== 'data');
226
+ }
227
+ catch {
228
+ // Not JSON: an ordinary string.
229
+ }
230
+ }
231
+ }
232
+ else if (Array.isArray(value)) {
233
+ value.forEach(v => visit(v, undefined, opaque));
234
+ }
235
+ else if (value && typeof value === 'object') {
236
+ const vendor = 'vendor_name' in value && !(0, loader_1.isGoogleVendor)(value.vendor_name);
237
+ for (const [k, v] of Object.entries(value)) {
238
+ visit(v, k, opaque || vendor || k === 'ai_context' || k === 'default');
239
+ }
240
+ }
241
+ };
242
+ for (const text of texts) {
243
+ // Parsed as the loader parses, so both read the same values.
244
+ for (const doc of yaml.parseAllDocuments(text, yaml_options_1.YAML_OPTIONS)) {
245
+ if (doc.errors.length)
246
+ continue;
247
+ // Reading the values can still fail: on an alias whose anchor is not
248
+ // set, on more aliases than a load expands, or, while walking them, on
249
+ // an alias that refers to itself, which makes the values cyclic. The
250
+ // load reports that file in its own words, so the rest of it is skipped
251
+ // here rather than stopping the command.
252
+ try {
253
+ visit(doc.toJS());
254
+ }
255
+ catch {
256
+ continue;
257
+ }
258
+ }
259
+ }
260
+ return [...found];
261
+ }
@@ -0,0 +1,12 @@
1
+ import type { BigLakeClient } from '../gcp/biglake';
2
+ import type { BigQueryClient } from '../gcp/bigquery';
3
+ import type { SpannerClient } from '../gcp/spanner';
4
+ import { EntryLookup } from './catalog_entry';
5
+ import type { SemanticModel } from './ir';
6
+ export interface SourceReaders {
7
+ catalog: EntryLookup;
8
+ bigquery: Pick<BigQueryClient, 'getTable'>;
9
+ biglake: Pick<BigLakeClient, 'getCatalog'>;
10
+ spanner: Pick<SpannerClient, 'getInstance' | 'getInstanceConfig'>;
11
+ }
12
+ export declare function sourceEntryErrors(model: Pick<SemanticModel, 'name' | 'entities' | 'profiles'>, readers: SourceReaders): Promise<string[]>;
@@ -0,0 +1,282 @@
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
+ // - Spanner: the default leader region of the instance's config, which
20
+ // getInstance reads the config's name from. A regional config is named for
21
+ // its region, `regional-us-central1` for us-central1, so it is not read;
22
+ // any other config is, with getInstanceConfig: `nam3` gives us-east4. That
23
+ // read needs spanner.instanceConfigs.get, which the Spanner database roles
24
+ // lack. A database's own default leader does not move the entry.
25
+ // - AlloyDB: the location the source names.
26
+ //
27
+ // Dataplex keeps locations in lower case. A lookup in the wrong location
28
+ // answers 403, as a missing entry does (live, 2026-10-27), so each location
29
+ // must be read, not guessed. Every read is a direct lookup: Dataplex search
30
+ // misses entries a lookup finds, and is not exact.
31
+ //
32
+ // When a location read answers 404, the source names a table, catalog or
33
+ // instance that does not exist. When it answers 403, the read was refused and
34
+ // the entry was never looked up, so the message says which read it was, and
35
+ // what the service said: Spanner answers 403 for a project the caller may not
36
+ // read, whether or not the instance exists, so a 403 does not say the table
37
+ // is missing. A read that fails another way says to try again when a retry
38
+ // may pass (a thrown call, a 429 or a 5xx), and to fix the source otherwise.
39
+ //
40
+ // A Cloud SQL or third-party source is not looked up here: sourceDialects
41
+ // already looks up its entry directly, and fails the push when it is missing.
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.sourceEntryErrors = sourceEntryErrors;
44
+ const api_1 = require("../gcp/api");
45
+ const catalog_entry_1 = require("./catalog_entry");
46
+ const loader_1 = require("./loader");
47
+ const resolve_profiles_1 = require("./resolve_profiles");
48
+ // One message for each entity, in the inline bindings (named `default`) or in
49
+ // a profile, whose source names a table with no Knowledge Catalog entry, in
50
+ // that order. Then one message for each of those profiles whose BigQuery and
51
+ // BigLake tables share no location (b/578688979): BigQuery runs each query in
52
+ // one location, so no query through the profile can join them.
53
+ //
54
+ // A named profile is judged by its own sources, since push requires it to
55
+ // bind every concrete entity. A BigLake table can be queried in any replica
56
+ // region of its catalog. A table whose location could not be read is
57
+ // reported above and left out.
58
+ //
59
+ // Seen live in kc-looker-external, for a US catalog whose replicas are
60
+ // us-central1 (primary) and us-east1:
61
+ // - A job in US cannot read its tables (2026-11-03).
62
+ // - Once a table has replicated, which took a few minutes, a job in either
63
+ // region can read it, join it with a dataset in that region, and create a
64
+ // property graph over both (2026-11-04).
65
+ // - A job with no location runs in the primary.
66
+ //
67
+ // Reads each table's location and looks its entry up once per location it
68
+ // may sit in, and reads each catalog and instance once; tables are checked in
69
+ // parallel, and a table's locations in turn. Skips an abstract entity, an
70
+ // entity with no source, and a source that names no table this module knows
71
+ // (a Cloud SQL or third-party table, or a legacy bare name). Never throws.
72
+ async function sourceEntryErrors(model, readers) {
73
+ const uses = [];
74
+ const use = (entity, profile, source) => {
75
+ const table = source ? (0, loader_1.sourceTableOf)(source) : undefined;
76
+ if (table)
77
+ uses.push({ entity, profile, table });
78
+ };
79
+ for (const e of model.entities) {
80
+ if (!e.abstract)
81
+ use(e.name, resolve_profiles_1.DEFAULT_PROFILE, (0, loader_1.writtenSource)(e));
82
+ }
83
+ for (const p of model.profiles ?? []) {
84
+ for (const b of p.entities)
85
+ use(b.name, p.name, b.source);
86
+ }
87
+ const catalogAndInstanceReads = new Map();
88
+ const locations = new Map();
89
+ const problems = new Map();
90
+ for (const { table } of uses) {
91
+ if (!problems.has(table.resourceName)) {
92
+ const located = locationsOf(table, readers, catalogAndInstanceReads);
93
+ locations.set(table.resourceName, located);
94
+ problems.set(table.resourceName, problemOf(table, located, readers));
95
+ }
96
+ }
97
+ const errors = [];
98
+ // By profile, each list of regions BigQuery can query one of its tables in,
99
+ // keyed by the list, with the entities bound to such tables, in the order
100
+ // bound.
101
+ const placed = new Map();
102
+ for (const { entity, profile, table } of uses) {
103
+ const problem = await problems.get(table.resourceName);
104
+ if (problem) {
105
+ errors.push(`entity '${entity}' in profile '${profile}': source '${table.resourceName}' ${problem}`);
106
+ }
107
+ if (table.system !== 'bigquery' && table.system !== 'biglake')
108
+ continue;
109
+ const located = await locations.get(table.resourceName);
110
+ if (!('result' in located))
111
+ continue;
112
+ const regions = located.result;
113
+ const key = regions.join(' ');
114
+ const at = placed.get(profile) ?? new Map();
115
+ at.set(key, { regions, entities: [...at.get(key)?.entities ?? [], entity] });
116
+ placed.set(profile, at);
117
+ }
118
+ for (const [profile, at] of placed) {
119
+ const [first, ...rest] = at.values();
120
+ if (first.regions.some(r => rest.every(g => g.regions.includes(r)))) {
121
+ continue;
122
+ }
123
+ const listed = [...at.values()].map(({ regions, entities }) => `${regions.map(shown).join(' or ')} (${entities.map(e => `'${e}'`).join(', ')})`);
124
+ errors.push(`profile '${profile}' of model '${model.name}' binds tables that ` +
125
+ `share no BigQuery location: ${listed.join(', ')}. ` +
126
+ `BigQuery runs each query in one location, so no query can join ` +
127
+ `them. Bind them to tables that share a location, or pass ` +
128
+ `--skip-source-check.`);
129
+ }
130
+ return errors;
131
+ }
132
+ // What is wrong with `table`'s source, or undefined when its catalog entry
133
+ // is there. `located` is what locationsOf gave for `table`. Looks the entry
134
+ // up in each of its locations in turn, and stops at the first that finds it:
135
+ // a lookup in the wrong location answers 403, so trying each cannot pass a
136
+ // missing entry.
137
+ async function problemOf(table, located, readers) {
138
+ const locations = await located;
139
+ if ('problem' in locations)
140
+ return locations.problem;
141
+ for (const location of locations.result) {
142
+ const res = await (0, catalog_entry_1.lookupSystemEntry)(readers.catalog, table.system, table.project, location, table.resourceName);
143
+ if ('result' in res)
144
+ return undefined;
145
+ if ('error' in res)
146
+ return failedProblem(res).problem;
147
+ }
148
+ return `names no table Knowledge Catalog knows about. The table may not ` +
149
+ `exist, or you may not be allowed to read its catalog entry.${table.system === 'alloydb' ?
150
+ ' A new AlloyDB cluster can take hours to appear in the ' +
151
+ 'catalog after Dataplex integration is turned on.' :
152
+ ''} Fix the source, or pass --skip-source-check.`;
153
+ }
154
+ // `location`, which is kept in lower case, as BigQuery writes it: a
155
+ // multi-region, which has no hyphen, in upper case (`US`, `EU`).
156
+ function shown(location) {
157
+ return location.includes('-') ? location : location.toUpperCase();
158
+ }
159
+ // Said when reading an instance config is refused (live on
160
+ // bigquery-public-data and dataplex-staging-types, 2026-10-27: Spanner answers
161
+ // 403 naming this permission, even for a Google-managed config such as nam3).
162
+ const CONFIG_REFUSED_HINT = ' Reading it needs the permission ' +
163
+ 'spanner.instanceConfigs.get on the project, which roles/spanner.viewer ' +
164
+ 'includes and the Spanner database roles do not.';
165
+ // The locations where `table`'s catalog entry may sit, as Dataplex writes
166
+ // them, in lower case, in the order to look in: one for a BigQuery, Spanner
167
+ // or AlloyDB table, and one per replica for a BigLake table. `cache` keeps
168
+ // each catalog and instance's read, so each is read once; each table is read
169
+ // once already.
170
+ async function locationsOf(table, readers, cache) {
171
+ const once = (key, readIt) => {
172
+ if (!cache.has(key))
173
+ cache.set(key, readIt());
174
+ return cache.get(key);
175
+ };
176
+ const { project } = table;
177
+ switch (table.system) {
178
+ case 'alloydb':
179
+ return { result: [table.location] };
180
+ case 'bigquery': {
181
+ const what = `reading table '${project}.${table.dataset}.${table.table}'`;
182
+ const res = await readLocation(what, 'a table', () => readers.bigquery.getTable(project, table.dataset, table.table));
183
+ return 'result' in res ?
184
+ located(res.result.location, what, 'no location') :
185
+ res;
186
+ }
187
+ case 'biglake':
188
+ return once(`biglake/${project}/${table.catalog}`, async () => {
189
+ const what = `reading the region of catalog '${table.catalog}'`;
190
+ const res = await readLocation(what, 'a catalog', () => readers.biglake.getCatalog(project, table.catalog));
191
+ return 'result' in res ? replicaRegions(res.result, what) : res;
192
+ });
193
+ case 'spanner':
194
+ return once(`spanner/${project}/${table.instance}`, async () => {
195
+ const what = `reading instance '${table.instance}'`;
196
+ const instance = await readLocation(what, 'an instance', () => readers.spanner.getInstance(project, table.instance));
197
+ if ('problem' in instance)
198
+ return instance;
199
+ const configName = instance.result.config;
200
+ if (!configName) {
201
+ return transientProblem(`${what} gave no instance config.`);
202
+ }
203
+ // A regional config keeps every replica, so its leader, in the region
204
+ // it is named for: `regional-us-central1` leads in us-central1 (live,
205
+ // 2026-10-27).
206
+ const region = configName.match(/\/instanceConfigs\/regional-(.+)$/);
207
+ if (region)
208
+ return { result: [region[1]] };
209
+ const configWhat = `reading instance config '${configName}'`;
210
+ const config = await readLocation(configWhat, 'an instance config', () => readers.spanner.getInstanceConfig(configName), CONFIG_REFUSED_HINT);
211
+ // A config marks exactly one replica as its default leader.
212
+ return 'result' in config ?
213
+ located(config.result.replicas?.find(r => r.defaultLeaderLocation)
214
+ ?.location, configWhat, 'no default leader') :
215
+ config;
216
+ });
217
+ }
218
+ }
219
+ // Makes the location read `call`, which `what` names. A 404 says the source
220
+ // names `kind`, such as `a table`, that does not exist (the services answer
221
+ // 404 for a missing table, dataset, catalog or instance; live, 2026-10-27 and
222
+ // 2026-11-03). A 403 says the read was refused, followed by the service's
223
+ // message and `refusedHint`.
224
+ async function readLocation(what, kind, call, refusedHint = '') {
225
+ const res = await (0, catalog_entry_1.read)(what, call);
226
+ if ('result' in res)
227
+ return res;
228
+ if ('error' in res)
229
+ return failedProblem(res);
230
+ return {
231
+ problem: res.status === 404 ?
232
+ `names ${kind} that does not exist: ${what} found nothing (HTTP ` +
233
+ `404). Fix the source, or pass --skip-source-check.` :
234
+ `could not be checked: ${what} was refused (HTTP 403).${res.message ? ` ${res.message}` : ''}${refusedHint} The ` +
235
+ `source's catalog entry was not looked up. Fix the access or ` +
236
+ `the source, or pass --skip-source-check.`,
237
+ };
238
+ }
239
+ // The regions of `catalog`'s replicas, in the order to look in: a replica
240
+ // marked STATE_PRIMARY first, then one in another primary state, such as
241
+ // STATE_PRIMARY_IN_PROGRESS, then the rest, each group in the order the
242
+ // replicas come. A regional or federated catalog lists one replica with no
243
+ // state.
244
+ //
245
+ // After a failover, a table's entry stays only in the region that was the
246
+ // catalog's primary when the table was created, until the table is next
247
+ // written; for a few seconds during a failover the new primary reads
248
+ // STATE_PRIMARY_IN_PROGRESS, so no replica is STATE_PRIMARY (from review on
249
+ // #623, 2026-11-03; not tested live by us).
250
+ function replicaRegions(catalog, what) {
251
+ const rank = (state = '') => state === 'STATE_PRIMARY' ? 0 : state.includes('PRIMARY') ? 1 : 2;
252
+ const regions = (catalog.replicas ?? [])
253
+ .filter(r => r.region)
254
+ .sort((a, b) => rank(a.state) - rank(b.state))
255
+ .map(r => r.region.toLowerCase());
256
+ return regions.length ? { result: regions } :
257
+ transientProblem(`${what} gave no region.`);
258
+ }
259
+ // `location` in lower case, or a problem when the read `what` gave none.
260
+ // The services have always given one; this keeps a response without it from
261
+ // stopping the push with a crash.
262
+ function located(location, what, missing) {
263
+ return location ? { result: [location.toLowerCase()] } :
264
+ transientProblem(`${what} gave ${missing}.`);
265
+ }
266
+ // The problem for the failed read `res`. A thrown call, a 429 or a 5xx may
267
+ // pass on retry; any other status, such as BigQuery's 400 for an invalid
268
+ // table ID, never will, so the source must change.
269
+ function failedProblem(res) {
270
+ return res.status === undefined || (0, api_1.isTransientStatus)(res.status) ?
271
+ transientProblem(res.error) :
272
+ {
273
+ problem: `could not be checked: ${res.error} Fix the source, or ` +
274
+ `pass --skip-source-check.`,
275
+ };
276
+ }
277
+ // The problem for a read that failed with `detail`, which may pass on retry.
278
+ function transientProblem(detail) {
279
+ return {
280
+ problem: `could not be checked; try again, or pass --skip-source-check: ${detail}`,
281
+ };
282
+ }
@@ -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,17 @@
1
+ import { Field } from './ir';
2
+ import { SqlColumn } from './sql_parser';
3
+ export declare const STRING_LITERAL: RegExp;
4
+ export declare function escapeRegExp(s: string): string;
5
+ export declare function blankStringLiterals(expression: string): string;
6
+ export declare function mapOutsideStringLiterals(expression: string, fn: (segment: string) => string): string;
7
+ export declare function referencedEntityNames(expression: string, entityNames: string[]): string[];
8
+ export declare function stripQualifier(expression: string, entity: string): string;
9
+ export declare function sqlWithoutComments(text: string, dialect?: string): string;
10
+ export declare function fieldBindingWithoutComments(field: Pick<Field, 'expression' | 'importedExpression' | 'importedDialect'>, targetDialect?: string): string | undefined;
11
+ export declare function columnReferences(expression: string, dialect: string): ReadonlyArray<SqlColumn> | undefined;
12
+ export declare function columnsForSource(text: string, dialect: string, impliedDialect?: string): ReadonlyArray<SqlColumn> | undefined;
13
+ export declare function entitiesReached(columns: ReadonlyArray<SqlColumn>, entityNames: ReadonlyArray<string> | ReadonlySet<string>): string[];
14
+ export declare function fieldsReadOn(columns: ReadonlyArray<SqlColumn>, entityName: string): string[];
15
+ export declare const SUPPORTED_AGGREGATES: string[];
16
+ export declare function isSingleAggregate(expression: string, dialect: string): boolean;
17
+ export declare function isColumnName(value: unknown): boolean;