@google-cloud/kcmd 0.1.0 → 0.2.1

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 (67) hide show
  1. package/README.md +18 -20
  2. package/build/ts/kcmd/cli.js +6164 -5631
  3. package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
  4. package/build/ts/kcmd/gcp/biglake.js +13 -4
  5. package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
  6. package/build/ts/kcmd/gcp/spanner.js +6 -3
  7. package/build/ts/kcmd/layouts/okf.js +49 -7
  8. package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
  9. package/build/ts/kcmd/layouts/semantic-model.js +129 -50
  10. package/build/ts/kcmd/okf/concept.d.ts +1 -1
  11. package/build/ts/kcmd/okf/concept.js +6 -0
  12. package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
  13. package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
  14. package/build/ts/kcmd/okf/init_okf_workspace.js +7 -9
  15. package/build/ts/kcmd/okf/okf_metadata.js +17 -0
  16. package/build/ts/kcmd/semantic/bigquery.js +139 -195
  17. package/build/ts/kcmd/semantic/binding.js +14 -19
  18. package/build/ts/kcmd/semantic/deploy_bigquery.js +6 -13
  19. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
  20. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +105 -162
  21. package/build/ts/kcmd/semantic/deploy_spanner.js +7 -14
  22. package/build/ts/kcmd/semantic/deployment_target.d.ts +1 -4
  23. package/build/ts/kcmd/semantic/deployment_target.js +6 -62
  24. package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
  25. package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
  26. package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
  27. package/build/ts/kcmd/semantic/import_model.js +531 -111
  28. package/build/ts/kcmd/semantic/ir.d.ts +2 -4
  29. package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
  30. package/build/ts/kcmd/semantic/kc_converter.js +32 -27
  31. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
  32. package/build/ts/kcmd/semantic/kc_custom_types.js +43 -114
  33. package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
  34. package/build/ts/kcmd/semantic/kc_ids.js +4 -7
  35. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
  36. package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
  37. package/build/ts/kcmd/semantic/loader.d.ts +7 -11
  38. package/build/ts/kcmd/semantic/loader.js +282 -171
  39. package/build/ts/kcmd/semantic/osi_converter.js +14 -65
  40. package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
  41. package/build/ts/kcmd/semantic/parameters.js +22 -3
  42. package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
  43. package/build/ts/kcmd/semantic/pull_kc.js +38 -112
  44. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
  45. package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
  46. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
  47. package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
  48. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +4 -12
  49. package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
  50. package/build/ts/kcmd/semantic/runtime/store.d.ts +14 -19
  51. package/build/ts/kcmd/semantic/runtime/store.js +21 -37
  52. package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
  53. package/build/ts/kcmd/semantic/source_dialect.js +83 -33
  54. package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
  55. package/build/ts/kcmd/semantic/source_entries.js +95 -36
  56. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
  57. package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
  58. package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
  59. package/build/ts/kcmd/semantic/sql_parser.js +22 -0
  60. package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
  61. package/build/ts/kcmd/semantic/transpile.js +79 -61
  62. package/build/ts/kcmd/semantic/validate.d.ts +37 -2
  63. package/build/ts/kcmd/semantic/validate.js +467 -126
  64. package/build/ts/kcmd/snapshot.d.ts +4 -0
  65. package/build/ts/kcmd/snapshot.js +7 -0
  66. package/build/ts/kcmd/sync.js +44 -7
  67. package/package.json +1 -1
@@ -50,13 +50,13 @@ export type ChosenStore = SpannerStore | AlloyDbDatabase | BigQueryStore;
50
50
  * `named` is set when the model's deployments name a target and still no
51
51
  * store results, so a caller that runs against the store fails. `rejected` is
52
52
  * set with it when the deployments give no store whatever the model is used
53
- * for: a target in no accepted form in any deployment, a GOOGLE block whose
54
- * data is not valid JSON, or several targets for the profile. `named` alone
55
- * means the model binds a table outside its Spanner or AlloyDB store.
53
+ * for: a target in no accepted form in any deployment, or several targets for
54
+ * the profile. `named` alone means the model binds a table outside its Spanner
55
+ * or AlloyDB store.
56
56
  *
57
57
  * Neither is set when the profile names no store, unless a target is in no
58
- * accepted form, or a GOOGLE block is not valid JSON. Neither is set either
59
- * when a BigQuery model binds a table outside the graph's dataset.
58
+ * accepted form. Neither is set either when a BigQuery model binds a table
59
+ * outside the graph's dataset.
60
60
  */
61
61
  export interface StoreError {
62
62
  error: string;
@@ -67,21 +67,18 @@ export interface StoreError {
67
67
  * The store a model runs against under binding profile `profile`, which
68
68
  * defaults to `default`, the model file's own bindings. It is the one the
69
69
  * deployments for that profile name: those that name it in `profile`, or that
70
- * name none or `default` for `default`. A model with no `deployments` list
71
- * names no profile for any of its targets, so they are all for `default`.
72
- * With several targets, the store is the single operational one, Spanner or
73
- * AlloyDB.
70
+ * name none or `default` for `default`. With several targets, the store is the
71
+ * single operational one, Spanner or AlloyDB.
74
72
  *
75
- * Fails six ways:
73
+ * Fails five ways:
76
74
  *
77
- * 1. A GOOGLE block's data is not valid JSON, so its targets cannot be read.
78
- * 2. A target names no supported destination. Push checks the target of every
75
+ * 1. A target names no supported destination. Push checks the target of every
79
76
  * deployment, so this fails the store under every profile.
80
- * 3. No deployment uses the profile.
81
- * 4. There are several targets and no single operational one, so which store
77
+ * 2. No deployment uses the profile.
78
+ * 3. There are several targets and no single operational one, so which store
82
79
  * is meant is ambiguous.
83
- * 5. There is no target, so there is nothing to run against.
84
- * 6. The model binds a table outside the store. For a Spanner or AlloyDB
80
+ * 4. There is no target, so there is nothing to run against.
81
+ * 5. The model binds a table outside the store. For a Spanner or AlloyDB
85
82
  * store, a write would land in the wrong place, and the error is named. A
86
83
  * BigQuery graph may read tables in any dataset, so
87
84
  * for a BigQuery store the error is not named.
@@ -89,9 +86,7 @@ export interface StoreError {
89
86
  export declare function resolveStore(model: SemanticModel, profile?: string): ChosenStore | StoreError;
90
87
  /**
91
88
  * The targets binding profile `profile` uses, as resolveStore reads them: its
92
- * deployments' targets, or every target in a GOOGLE block for `default` when
93
- * the model has no `deployments` list. Empty when no deployment is for the
94
- * profile. Throws when a GOOGLE block's data is not valid JSON.
89
+ * deployments' targets. Empty when no deployment is for the profile.
95
90
  */
96
91
  export declare function profileTargets(model: SemanticModel, profile: string): string[];
97
92
  /**
@@ -101,21 +101,18 @@ function strayBindings(model, store) {
101
101
  * The store a model runs against under binding profile `profile`, which
102
102
  * defaults to `default`, the model file's own bindings. It is the one the
103
103
  * deployments for that profile name: those that name it in `profile`, or that
104
- * name none or `default` for `default`. A model with no `deployments` list
105
- * names no profile for any of its targets, so they are all for `default`.
106
- * With several targets, the store is the single operational one, Spanner or
107
- * AlloyDB.
104
+ * name none or `default` for `default`. With several targets, the store is the
105
+ * single operational one, Spanner or AlloyDB.
108
106
  *
109
- * Fails six ways:
107
+ * Fails five ways:
110
108
  *
111
- * 1. A GOOGLE block's data is not valid JSON, so its targets cannot be read.
112
- * 2. A target names no supported destination. Push checks the target of every
109
+ * 1. A target names no supported destination. Push checks the target of every
113
110
  * deployment, so this fails the store under every profile.
114
- * 3. No deployment uses the profile.
115
- * 4. There are several targets and no single operational one, so which store
111
+ * 2. No deployment uses the profile.
112
+ * 3. There are several targets and no single operational one, so which store
116
113
  * is meant is ambiguous.
117
- * 5. There is no target, so there is nothing to run against.
118
- * 6. The model binds a table outside the store. For a Spanner or AlloyDB
114
+ * 4. There is no target, so there is nothing to run against.
115
+ * 5. The model binds a table outside the store. For a Spanner or AlloyDB
119
116
  * store, a write would land in the wrong place, and the error is named. A
120
117
  * BigQuery graph may read tables in any dataset, so
121
118
  * for a BigQuery store the error is not named.
@@ -123,15 +120,7 @@ function strayBindings(model, store) {
123
120
  function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
124
121
  // Push checks the target of every deployment the model declares, so the
125
122
  // targets of every deployment are read before the profile's are picked.
126
- let all;
127
- try {
128
- all = (0, deployment_target_1.googleDeploymentTargets)(model);
129
- }
130
- catch (err) {
131
- // It throws when a GOOGLE block's data is not valid JSON. Push reports
132
- // that as an error, so the store fails.
133
- return { error: `${err?.message || err}`, named: true, rejected: true };
134
- }
123
+ const all = (0, deployment_target_1.googleDeploymentTargets)(model);
135
124
  // The loader reads a vanilla model's `deployments` out of its GOOGLE block,
136
125
  // so a message names the block's `deployments`, as push does.
137
126
  const vanilla = model.version === loader_1.OSSIE_VERSION;
@@ -141,17 +130,19 @@ function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
141
130
  // A target in no accepted form is reported, alone or beside one that gives
142
131
  // a store, whichever profile its deployment is for.
143
132
  if (all.malformed.length) {
144
- const unparsed = model.deployments?.find(d => d.target === all.malformed[0]);
145
- const error = unparsed ? (0, deployment_target_1.deploymentTargetError)(model, unparsed) :
146
- (0, deployment_target_1.googleBlockTargetError)(model, all.malformed[0]);
147
- return { error, named: true, rejected: true };
133
+ const unparsed = model.deployments.find(d => d.target === all.malformed[0]);
134
+ return {
135
+ error: (0, deployment_target_1.deploymentTargetError)(model, unparsed),
136
+ named: true,
137
+ rejected: true,
138
+ };
148
139
  }
149
140
  const used = deploymentsFor(model, profile);
150
141
  if ('error' in used)
151
142
  return used;
152
143
  const scoped = used.model;
153
- // The profile's targets are among those read above. So this does not
154
- // throw, and none of them is in no accepted form.
144
+ // The profile's targets are among those read above, so none of them is in no
145
+ // accepted form.
155
146
  const targets = (0, deployment_target_1.googleDeploymentTargets)(scoped);
156
147
  const { spanner, alloyDb, bigQuery } = targets;
157
148
  const declared = spanner.length + alloyDb.length + bigQuery.length;
@@ -202,9 +193,7 @@ function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
202
193
  return store;
203
194
  }
204
195
  // The model with only the deployments `profile` uses, or why there are none.
205
- // A model with no `deployments` list keeps every target in its GOOGLE block
206
- // for `default`. An empty list names no deployment. Either way, any other
207
- // profile has none.
196
+ // A model with no or empty `deployments` list names no deployment.
208
197
  function deploymentsFor(model, profile) {
209
198
  const nothingToRun = `so there is no database to run it against`;
210
199
  const namedProfile = `binding profile '${profile}' is a named profile, ` +
@@ -235,21 +224,16 @@ function deploymentsFor(model, profile) {
235
224
  }
236
225
  /**
237
226
  * The targets binding profile `profile` uses, as resolveStore reads them: its
238
- * deployments' targets, or every target in a GOOGLE block for `default` when
239
- * the model has no `deployments` list. Empty when no deployment is for the
240
- * profile. Throws when a GOOGLE block's data is not valid JSON.
227
+ * deployments' targets. Empty when no deployment is for the profile.
241
228
  */
242
229
  function profileTargets(model, profile) {
243
230
  const used = deploymentsFor(model, profile);
244
231
  return 'error' in used ? [] : (0, deployment_target_1.googleDeploymentTargets)(used.model).uris;
245
232
  }
246
- // The deployments whose targets are `uris`, for a message, or nothing when
247
- // the targets come from a GOOGLE block, which names none. `profile` is the
233
+ // The deployments whose targets are `uris`, for a message. `profile` is the
248
234
  // profile they are for.
249
235
  function deploymentNames(model, uris, profile) {
250
- const deployments = model.deployments;
251
- if (!deployments)
252
- return '';
236
+ const deployments = model.deployments ?? [];
253
237
  const names = uris.map(uri => deployments.find(d => d.target === uri)?.name ?? uri);
254
238
  return ` for profile '${profile}' (${names.join(', ')})`;
255
239
  }
@@ -1,10 +1,17 @@
1
+ import type { SpannerClient } from '../gcp/spanner';
1
2
  import { EntryLookup } from './catalog_entry';
2
3
  export interface SourceDialects {
3
4
  dialects: Map<string, string>;
4
5
  databases: Map<string, string>;
5
6
  errors: string[];
7
+ unread: string[];
6
8
  }
7
- export declare function sourceDialects(texts: Iterable<string>, catalog: EntryLookup, { thirdParty }?: {
8
- thirdParty?: boolean | undefined;
9
+ export declare function sourceDialects(texts: Iterable<string>, catalog: EntryLookup, { thirdParty, spanner }?: {
10
+ thirdParty?: boolean;
11
+ spanner?: Pick<SpannerClient, 'getDatabase'>;
9
12
  }): Promise<SourceDialects>;
13
+ export declare function tableFqnParts(fqn: string): {
14
+ prefix: string;
15
+ parts: string[];
16
+ } | undefined;
10
17
  export declare function sourcesToLookUp(texts: Iterable<string>): string[];
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // The dialect of a Cloud SQL table written as its resource name, or of a
3
3
  // third-party table written as its entry name, read from the table's
4
- // Knowledge Catalog entry.
4
+ // Knowledge Catalog entry, and the dialect of a Spanner table's database.
5
5
  //
6
6
  // The resource name cannot say the dialect: a MySQL table has no schema, but a
7
7
  // PostgreSQL table and a SQL Server table have the same shape. Dataplex keeps an
@@ -16,9 +16,12 @@
16
16
  // fullyQualifiedName, so its parts say whether the entry is a table's or, for
17
17
  // example, a schema's.
18
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).
19
+ // Knowledge Catalog's Spanner entries leave their dialect empty, so a Spanner
20
+ // table's dialect is its database's `databaseDialect`, read by databases.get.
21
+ //
22
+ // This is the only place kcmd looks up a source's dialect. A command looks
23
+ // every such source up once, before it loads the model, and passes the
24
+ // dialects to the load (LoadOptions.sourceDialects).
22
25
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
26
  if (k2 === undefined) k2 = k;
24
27
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -54,10 +57,12 @@ var __importStar = (this && this.__importStar) || (function () {
54
57
  })();
55
58
  Object.defineProperty(exports, "__esModule", { value: true });
56
59
  exports.sourceDialects = sourceDialects;
60
+ exports.tableFqnParts = tableFqnParts;
57
61
  exports.sourcesToLookUp = sourcesToLookUp;
58
62
  const yaml = __importStar(require("yaml"));
59
63
  const loader_1 = require("./loader");
60
64
  const catalog_entry_1 = require("./catalog_entry");
65
+ const parameters_1 = require("./parameters");
61
66
  const yaml_options_1 = require("./yaml_options");
62
67
  // The dialect each entrySource.system gives, in lower case.
63
68
  const DIALECT_OF_ENTRY_SYSTEM = new Map([...loader_1.DIALECT_OF_FQN_PREFIX, ['postgres', 'POSTGRES']]);
@@ -70,14 +75,16 @@ const TABLE_FQN_PARTS = new Map([
70
75
  ['postgresql', ['host', 'database', 'schema', 'table']],
71
76
  ['sqlserver', ['host', 'database', 'schema', 'table']],
72
77
  ]);
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.
78
+ // Looks up, once each and in parallel, the dialect of every Cloud SQL and
79
+ // third-party table the files `texts` write (see sourcesToLookUp), and of every
80
+ // Spanner database they name. Without `spanner`, a Spanner database gives
81
+ // SPANNER, the dialect kcmd's Spanner store speaks, with no call.
82
+ // `thirdParty: false` skips third-party tables. Never throws: a failed lookup
83
+ // goes to `errors`.
84
+ async function sourceDialects(texts, catalog, { thirdParty = true, spanner } = {}) {
85
+ // Keyed as dialectLookupKey gives, so both spellings of one table, and the
86
+ // tables of one Spanner database, are one lookup. A database's lookup names
87
+ // the first of its tables.
81
88
  const lookups = new Map();
82
89
  for (const source of sourcesToLookUp(texts)) {
83
90
  const table = (0, loader_1.cloudSqlTableOf)(source);
@@ -87,32 +94,42 @@ async function sourceDialects(texts, catalog, { thirdParty = true } = {}) {
87
94
  const entry = thirdParty ? (0, loader_1.thirdPartyEntryOf)(source) : undefined;
88
95
  if (entry)
89
96
  lookups.set(entry.name, () => thirdPartyDialect(catalog, entry));
97
+ const database = (0, loader_1.spannerDatabaseOf)(source);
98
+ if (database && !lookups.has(database)) {
99
+ lookups.set(database, spanner ? () => spannerDialect(spanner, database, source) :
100
+ async () => ({ dialect: 'SPANNER' }));
101
+ }
90
102
  }
91
103
  const dialects = new Map();
92
104
  const databases = new Map();
93
105
  const errors = [];
106
+ const unread = [];
94
107
  await Promise.all([...lookups].map(async ([key, lookUp]) => {
95
108
  const got = await lookUp();
96
109
  if ('error' in got)
97
110
  errors.push(got.error);
98
111
  else
99
112
  dialects.set(key, got.dialect);
113
+ if ('unread' in got)
114
+ unread.push(got.error);
100
115
  if ('database' in got && got.database)
101
116
  databases.set(key, got.database);
102
117
  }));
103
118
  errors.sort();
104
- return { dialects, databases, errors };
119
+ unread.sort();
120
+ return { dialects, databases, errors, unread };
105
121
  }
106
122
  // The dialect of a Cloud SQL table, by its entry's type.
107
123
  async function cloudSqlDialect(catalog, table) {
108
124
  const at = `source '${table.resourceName}'`;
109
125
  const res = await (0, catalog_entry_1.lookupSystemEntry)(catalog, 'cloudsql', table.project, table.location, table.resourceName);
110
126
  if ('error' in res)
111
- return { error: `${at}: ${res.error}` };
127
+ return { error: `${at}: ${res.error}`, unread: true };
112
128
  // Dataplex answers 403 both when the entry does not exist and when the
113
129
  // caller may not read it, so the message names both causes.
114
130
  if ('unreadable' in res) {
115
131
  return {
132
+ unread: true,
116
133
  error: `${at}: kcmd reads a Cloud SQL table's dialect from its ` +
117
134
  `Knowledge Catalog entry, and could not read it. Turn on Dataplex ` +
118
135
  `integration for instance '${table.instance}' (gcloud sql ` +
@@ -131,6 +148,27 @@ async function cloudSqlDialect(catalog, table) {
131
148
  `not a Cloud SQL table or view type kcmd knows (${Object.keys(loader_1.DIALECT_OF_ENTRY_TYPE).join(', ')}).`,
132
149
  };
133
150
  }
151
+ // The dialect of the Spanner database `database`, which `source` names a
152
+ // table of, by its databaseDialect.
153
+ async function spannerDialect(spanner, database, source) {
154
+ const [, , project, , instance, , name] = database.split('/');
155
+ const failed = (why) => ({
156
+ unread: true,
157
+ error: `source '${source}': kcmd reads its Spanner database's SQL ` +
158
+ `dialect, and could not read database ${project}/${instance}/${name}: ${why}`,
159
+ });
160
+ try {
161
+ const res = await spanner.getDatabase(project, instance, name);
162
+ if (res.status !== 200) {
163
+ return failed((0, parameters_1.storeMessage)(res.message, res.status));
164
+ }
165
+ const postgres = res.result?.databaseDialect === 'POSTGRESQL';
166
+ return { dialect: postgres ? 'POSTGRES' : 'SPANNER' };
167
+ }
168
+ catch (err) {
169
+ return failed(err?.message ?? String(err));
170
+ }
171
+ }
134
172
  // The dialect of a third-party table, by its entry's fullyQualifiedName or
135
173
  // entrySource.system, and the database the fullyQualifiedName names. An entry
136
174
  // whose fullyQualifiedName names no table or view is rejected.
@@ -138,9 +176,10 @@ async function thirdPartyDialect(catalog, { project, location, name }) {
138
176
  const at = `source '${name}'`;
139
177
  const res = await (0, catalog_entry_1.read)('looking up its Knowledge Catalog entry', () => catalog.lookupEntry(project, location, name));
140
178
  if ('error' in res)
141
- return { error: `${at}: ${res.error}` };
179
+ return { error: `${at}: ${res.error}`, unread: true };
142
180
  if ('unreadable' in res) {
143
181
  return {
182
+ unread: true,
144
183
  error: `${at} names no table Knowledge Catalog knows about. The entry ` +
145
184
  `may not exist, or you may not be allowed to read it. kcmd reads a ` +
146
185
  `third-party table's dialect from its entry, so add the table to ` +
@@ -177,14 +216,12 @@ async function thirdPartyDialect(catalog, { project, location, name }) {
177
216
  `fullyQualifiedName.`,
178
217
  };
179
218
  }
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) {
219
+ // The prefix of the fullyQualifiedName `fqn` of a third-party table or view,
220
+ // and its unquoted parts, such as `snowflake` and ['acme', 'retail', 'public',
221
+ // 'orders'] for `snowflake:acme.retail.public.orders`. Undefined when `fqn`
222
+ // has another number of parts than TABLE_FQN_PARTS gives, or a subtype other
223
+ // than Databricks' `table:` and `view:`.
224
+ function tableFqnParts(fqn) {
188
225
  // The subtype holds no backtick, so a colon inside a quoted host and port
189
226
  // stays in the path. A colon in an unquoted database, schema or table
190
227
  // name stays in its segment when it comes after the name's first backtick.
@@ -195,19 +232,31 @@ function databaseOfFqn(fqn) {
195
232
  (prefix === 'databricks' && (subtype === 'table' || subtype === 'view'));
196
233
  if (!parts || !tableSubtype || seg.length !== parts.length)
197
234
  return undefined;
235
+ return { prefix, parts: seg };
236
+ }
237
+ // The database the fullyQualifiedName `fqn` of a third-party table or view
238
+ // names: a Snowflake account, a MySQL or SQL Server host, a PostgreSQL host
239
+ // and database, or a Databricks metastore. Each account and host is in lower
240
+ // case. Undefined when `fqn` names no table or view.
241
+ function databaseOfFqn(fqn) {
242
+ const table = tableFqnParts(fqn);
243
+ if (!table)
244
+ return undefined;
245
+ const { prefix, parts } = table;
198
246
  const n = prefix === 'postgresql' ? 2 : 1;
199
247
  // A Snowflake account and a host name ignore letter case.
200
248
  if (prefix !== 'databricks')
201
- seg[0] = seg[0].toLowerCase();
202
- return [prefix, ...seg.slice(0, n)].join('/');
249
+ parts[0] = parts[0].toLowerCase();
250
+ return [prefix, ...parts.slice(0, n)].join('/');
203
251
  }
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.
252
+ // Every Cloud SQL table, Spanner table and third-party entry the model and
253
+ // profile files `texts` write. A Cloud SQL table counts as a resource name or
254
+ // resource URI anywhere, JSON strings included. A Spanner table or an entry
255
+ // name counts as a `source` value in the YAML or in a GOOGLE block's JSON
256
+ // `data`, unless it sits in ai_context, an action parameter's default or
257
+ // another vendor's block. Values are read from the parsed YAML, so a name in a
258
+ // comment is not collected. A file that does not parse collects nothing; the
259
+ // load reports it.
211
260
  function sourcesToLookUp(texts) {
212
261
  const found = new Set();
213
262
  // `key` names the member that holds `value`. `opaque` marks a value whose
@@ -217,7 +266,8 @@ function sourcesToLookUp(texts) {
217
266
  const visit = (value, key, opaque = false) => {
218
267
  if (typeof value === 'string') {
219
268
  if ((0, loader_1.cloudSqlTableOf)(value) ||
220
- (key === 'source' && !opaque && (0, loader_1.thirdPartyEntryOf)(value))) {
269
+ (key === 'source' && !opaque &&
270
+ ((0, loader_1.thirdPartyEntryOf)(value) || (0, loader_1.spannerDatabaseOf)(value)))) {
221
271
  found.add(value.trim());
222
272
  }
223
273
  else if (/^\s*[[{]/.test(value)) {
@@ -6,7 +6,7 @@ import type { SemanticModel } from './ir';
6
6
  export interface SourceReaders {
7
7
  catalog: EntryLookup;
8
8
  bigquery: Pick<BigQueryClient, 'getTable'>;
9
- biglake: Pick<BigLakeClient, 'getCatalog'>;
9
+ biglake: Pick<BigLakeClient, 'getCatalog' | 'getHiveCatalog'>;
10
10
  spanner: Pick<SpannerClient, 'getInstance' | 'getInstanceConfig'>;
11
11
  }
12
12
  export declare function sourceEntryErrors(model: Pick<SemanticModel, 'name' | 'entities' | 'profiles'>, readers: SourceReaders): Promise<string[]>;