// Licensed to Cloudera, Inc. under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. Cloudera, Inc. licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import * as ko from 'knockout'; import KnockoutObservable from '@types/knockout'; import { Cancellable, CancellablePromise } from '../api/cancellablePromise'; import { addNavTags, deleteNavTags, fetchDescribe, fetchNavigatorMetadata, fetchPartitions, fetchSample, fetchSourceMetadata, searchEntities, updateNavigatorProperties, updateSourceMetadata } from '../catalog/api'; import MultiTableEntry, { TopAggs, TopFilters, TopJoins } from '../catalog/MultiTableEntry'; import { applyCancellable, forceSilencedErrors } from '../catalog/catalogUtils'; import { Compute, Connector, Namespace } from '../config/types'; import { hueWindow } from '../types/types'; import huePubSub from '../utils/huePubSub'; import I18n from '../utils/i18n'; import { executeSingleStatement } from '../apps/editor/execution/api'; import { SqlAnalyzer } from './analyzer/types'; import { CatalogGetOptions, DataCatalog, SqlAnalyzerPopularity, SqlAnalyzerResponse, SqlAnalyzerResponsePopularity, TimestampedData } from './dataCatalog'; export interface BaseDefinition extends TimestampedData { name?: string; comment?: string; index?: number; type?: string; isMapValue?: boolean; sqlAnalyzerLoaded?: boolean; partitionKey?: boolean; primaryKey?: boolean; foreignKey?: KeySpecification; } export type Definition = ExtendedColumn | BaseDefinition; export interface KeySpecification { name: string; } export interface ForeignKeySpecification extends KeySpecification { to: string; } export interface ExtendedColumn extends BaseDefinition, TimestampedData { name: string; type: string; } export interface RootSourceMeta extends TimestampedData { comment?: string | null; databases: string[]; notFound?: boolean; } export interface TablesMeta extends TimestampedData { comment?: string | null; index: number; name: string; type: string; } export interface DatabaseSourceMeta extends TimestampedData { comment?: string | null; notFound?: boolean; tables_meta: TablesMeta[]; } export interface TableSourceMeta extends TimestampedData { columns: string[]; comment?: string | null; extended_columns: ExtendedColumn[]; foreign_keys?: ForeignKeySpecification[]; fields?: string[]; // TODO: On FieldSourceMeta? hdfs_link?: string; is_view?: boolean; notFound?: boolean; partition_keys?: KeySpecification[]; primary_keys?: KeySpecification[]; support_updates?: boolean; } export interface ComplexDetails { index?: number; isMapValue: boolean; type: string; fields?: { index?: number; type: string; name: string; }[]; } export interface FieldSourceMeta extends TimestampedData { comment?: string | null; name: string; notFound?: boolean; sample: FieldSample[][]; type: string; key?: ComplexDetails; value?: ComplexDetails; item?: ComplexDetails; fields?: { index?: number; type: string; name: string; }[]; } export type SourceMeta = RootSourceMeta | DatabaseSourceMeta | TableSourceMeta | FieldSourceMeta; export type FieldSample = string | number | null | undefined; type ReloadOptions = Omit; export interface NavigatorMeta extends TimestampedData { clusteredByColNames: unknown; compressed: boolean; created: string; customProperties: unknown; deleteTime: unknown; deleted: boolean; description: string | null; extractorRunId: string; fileSystemPath: string; firstClassParentId: unknown; identity: string; inputFormat: string; internalType: string; lastAccessed: string | null; lastModified: string | null; lastModifiedBy: string | null; metaClassName: string; name: string | null; originalDescription: string | null; originalName: string; original_name?: string; outputFormat: string; owner: string; packageName: string; parentPath: string; partColNames: unknown; properties: { [key: string]: string }; serdeLibName: string; serdeName: string | null; serdeProps: string | null; sortByColNames: string | null; sourceId: string; sourceType: string; tags: unknown; technicalProperties: { [key: string]: string }; type: string; userEntity: boolean; } export interface TableAnalysis extends TimestampedData { cols: { comment?: string | null; type: string; name: string }[]; comment?: string | null; details: { properties: { [propertyKey: string]: string }; stats: { [statKey: string]: string }; }; hdfs_link: string; is_view: boolean; message: string; name: string; partition_keys: KeySpecification[]; path_location?: string; primary_keys: KeySpecification[]; properties: { col_name: string; comment?: string | null; data_type?: string | null }[]; stats: { col_name: string; comment?: string | null; data_type?: string | null }[]; } export type Analysis = TableAnalysis; export interface Partitions extends TimestampedData { partition_keys_json: string[]; partition_values_json: { browseUrl: string; columns: string[]; notebookUrl: string; partitionSpec: string; readUrl: string; }[]; } export interface SampleMeta { comment?: string | null; name: string; type: string; } export interface Sample { data: FieldSample[][]; has_more?: boolean; hueTimestamp?: number; isEscaped?: boolean; meta: SampleMeta[]; type: string; } export interface SqlAnalyzerMeta extends TimestampedData { hueTimestamp?: number; } const cachedOnly = (options?: CatalogGetOptions): boolean => !!(options && options.cachedOnly); const shouldReload = (options?: CatalogGetOptions & { refreshAnalysis?: boolean }): boolean => !!(!DataCatalog.cacheEnabled() || (options && (options.refreshCache || options.refreshAnalysis))); /** * Helper function to get the multi table catalog version of a catalog entry */ const getMultiTableEntry = async (catalogEntry: DataCatalogEntry): Promise => { if (!catalogEntry.isTableOrView()) { return Promise.reject(); } return catalogEntry.dataCatalog.getMultiTableEntry({ namespace: catalogEntry.namespace, compute: catalogEntry.compute, paths: [catalogEntry.path] }); }; export default class DataCatalogEntry { compute: Compute; dataCatalog: DataCatalog; definition?: Definition; isTemporary?: boolean; name: string; namespace: Namespace; path: string[]; saveTimeout = -1; commentObservable?: KnockoutObservable; analysis?: Analysis; analysisPromise?: CancellablePromise; childrenPromise?: CancellablePromise; navigatorMeta?: NavigatorMeta; navigatorMetaForChildrenPromise?: CancellablePromise; navigatorMetaPromise?: CancellablePromise; sqlAnalyzerMeta?: SqlAnalyzerMeta; sqlAnalyzerMetaPromise?: CancellablePromise; sqlAnalyzerPopularity?: SqlAnalyzerPopularity; sqlAnalyzerPopularityForChildrenPromise?: CancellablePromise; partitions?: Partitions; partitionsPromise?: CancellablePromise; sample?: Sample; samplePromise?: CancellablePromise; sourceMeta?: SourceMeta; sourceMetaPromise?: CancellablePromise; constructor(options: { compute: Compute; dataCatalog: DataCatalog; definition?: Definition; isTemporary?: boolean; namespace: Namespace; path?: string | string[]; }) { if (!options.dataCatalog.connector) { throw new Error('DataCatalogEntry created without connector'); } this.namespace = options.namespace; this.compute = options.compute; this.dataCatalog = options.dataCatalog; this.path = typeof options.path === 'string' ? options.path.split('.') : options.path || []; this.name = this.path.length ? this.path[this.path.length - 1] : this.getConnector().id; this.isTemporary = options.isTemporary; if (options.definition) { this.definition = options.definition; } else if (this.path.length === 0) { this.definition = { type: 'source' }; } else if (this.path.length === 1) { this.definition = { type: 'database' }; } else if (this.path.length === 2) { this.definition = { type: 'table' }; } this.reset(); } /** * Resets the entry to an empty state, it might still have some details cached */ reset(): void { this.saveTimeout = -1; this.analysis = undefined; this.analysisPromise = undefined; this.childrenPromise = undefined; this.navigatorMeta = undefined; this.navigatorMetaForChildrenPromise = undefined; this.navigatorMetaPromise = undefined; this.sqlAnalyzerMeta = undefined; this.sqlAnalyzerMetaPromise = undefined; this.sqlAnalyzerPopularity = undefined; this.sqlAnalyzerPopularityForChildrenPromise = undefined; this.partitions = undefined; this.partitionsPromise = undefined; this.sample = undefined; this.samplePromise = undefined; this.sourceMeta = undefined; this.sourceMetaPromise = undefined; if (this.path.length) { this.dataCatalog .getKnownEntry({ namespace: this.namespace, compute: this.compute, path: this.path.slice(0, this.path.length - 1) }) .then(parent => { if (parent) { parent.navigatorMetaForChildrenPromise = undefined; parent.sqlAnalyzerPopularityForChildrenPromise = undefined; } }) .catch(err => { console.warn(err); }); } } /** * Resets the entry and clears the cache */ async clearCache(options?: { cascade?: boolean; silenceErrors?: boolean; targetChild?: string; }): Promise { if (!options) { options = {}; } if (this.definition && this.definition.sqlAnalyzerLoaded) { delete this.definition.sqlAnalyzerLoaded; } this.reset(); try { if (options.cascade) { await this.dataCatalog.clearStorageCascade(this.namespace, this.compute, this.path); } else { await this.save(); } } catch (err) {} huePubSub.publish('data.catalog.entry.refreshed', { entry: this, cascade: !!options.cascade }); } private reloadAnalysis( options?: ReloadOptions & { refreshAnalysis?: boolean } ): CancellablePromise { this.analysisPromise = new CancellablePromise(async (resolve, reject, onCancel) => { const fetchPromise = fetchDescribe({ entry: this, ...options }); onCancel(() => { fetchPromise.cancel(); }); try { this.analysis = await fetchPromise; resolve(this.analysis); } catch (err) { reject(err || 'Fetch failed'); return; } this.saveLater(); }); return applyCancellable(this.analysisPromise, options); } private reloadNavigatorMeta(options?: ReloadOptions): CancellablePromise { if (this.canHaveNavigatorMetadata()) { this.navigatorMetaPromise = new CancellablePromise(async (resolve, reject) => { try { this.navigatorMeta = await fetchNavigatorMetadata({ ...options, entry: this }); resolve(this.navigatorMeta); } catch (err) { reject(err || 'Fetch failed'); return; } this.saveLater(); if (this.commentObservable) { this.commentObservable(this.getResolvedComment()); } }); } else { this.navigatorMetaPromise = CancellablePromise.reject(); } return applyCancellable(this.navigatorMetaPromise); } /** * Helper function to reload the nav opt metadata for the given entry */ private reloadSqlAnalyzerMeta({ cancellable, silenceErrors, sqlAnalyzer }: ReloadOptions & { sqlAnalyzer: SqlAnalyzer }): CancellablePromise { if (this.dataCatalog.canHaveSqlAnalyzerMeta()) { this.sqlAnalyzerMetaPromise = new CancellablePromise( async (resolve, reject, onCancel) => { const fetchPromise = sqlAnalyzer.fetchSqlAnalyzerMeta({ path: this.path, silenceErrors }); onCancel(() => { fetchPromise.cancel(); }); try { this.sqlAnalyzerMeta = await fetchPromise; resolve(this.sqlAnalyzerMeta); } catch (err) { reject(err || 'Fetch failed'); return; } this.saveLater(); } ); } else { this.sqlAnalyzerMetaPromise = CancellablePromise.reject(); } return applyCancellable(this.sqlAnalyzerMetaPromise, { cancellable }); } private reloadPartitions(options?: ReloadOptions): CancellablePromise { this.partitionsPromise = new CancellablePromise(async (resolve, reject) => { try { this.partitions = await fetchPartitions({ ...options, entry: this }); resolve(this.partitions); } catch (err) { reject(err || 'Fetch failed'); return; } this.saveLater(); }); return applyCancellable(this.partitionsPromise, options); } private reloadSample( options?: ReloadOptions & { operation?: string } ): CancellablePromise { this.samplePromise = new CancellablePromise(async (resolve, reject) => { try { this.sample = await fetchSample({ ...options, entry: this }); resolve(this.sample); } catch (err) { reject(err || 'Fetch failed'); return; } this.saveLater(); }); return applyCancellable(this.samplePromise, options); } private reloadSourceMeta(options?: ReloadOptions): CancellablePromise { this.sourceMetaPromise = new CancellablePromise(async (resolve, reject) => { if (this.dataCatalog.invalidatePromise) { try { await this.dataCatalog.invalidatePromise; } catch (err) {} } try { this.sourceMeta = await fetchSourceMetadata({ ...options, entry: this }); resolve(this.sourceMeta); } catch (err) { reject(err || 'Fetch failed'); return; } this.saveLater(); }); return applyCancellable(this.sourceMetaPromise, options); } drop(cascade?: boolean): CancellablePromise { if (!this.isDatabase() && !this.isTableOrView()) { return CancellablePromise.reject('Drop is only possible for a database, table or view.'); } const statement = `DROP ${ this.isDatabase() ? 'DATABASE' : this.isView() ? 'VIEW' : 'TABLE' } IF EXISTS \`${this.path.join('`.`')}\`${this.isDatabase() && cascade ? ' CASCADE;' : ';'}`; return new CancellablePromise((resolve, reject, onCancel) => { const executePromise = executeSingleStatement({ connector: this.getConnector(), namespace: this.namespace, compute: this.compute, statement }); onCancel(() => { executePromise.cancel(); }); executePromise .then(() => { this.clearCache({ cascade: true }).catch(); resolve(); }) .catch(reject); }); } /** * Save the entry to cache */ async save(): Promise { window.clearTimeout(this.saveTimeout); try { await this.dataCatalog.persistCatalogEntry(this); } catch (err) {} } /** * Save the entry at a later point of time */ saveLater(): void { if (((window).CACHEABLE_TTL?.default || 0) > 0) { window.clearTimeout(this.saveTimeout); this.saveTimeout = window.setTimeout(async () => { await this.save(); }, 1000); } } /** * Gets the parent entry, rejected if there's no parent. */ getParent(): Promise { if (!this.path.length) { return Promise.reject(); } return this.dataCatalog.getEntry({ namespace: this.namespace, compute: this.compute, path: this.path.slice(0, this.path.length - 1) }); } /** * Get the children of the catalog entry, columns for a table entry etc. */ getChildren(options?: CatalogGetOptions): CancellablePromise { if (this.childrenPromise && this.childrenPromise.cancelled) { this.childrenPromise = undefined; } if (!this.childrenPromise && cachedOnly(options)) { return CancellablePromise.reject(); } if (this.childrenPromise && !shouldReload(options)) { return applyCancellable(this.childrenPromise, options); } this.childrenPromise = new CancellablePromise( async (resolve, reject, onCancel) => { let sourceMeta: SourceMeta | undefined; let cancelled = false; onCancel(() => { cancelled = true; }); try { sourceMeta = await this.getSourceMeta(options); } catch (err) {} if (cancelled) { reject('Cancelled'); return; } if (!sourceMeta) { reject('No source meta found'); return; } if (sourceMeta.notFound) { resolve([]); return; } const partitionKeys: { [key: string]: boolean } = {}; const tableSourceMeta = sourceMeta; if (tableSourceMeta.partition_keys) { tableSourceMeta.partition_keys.forEach(partitionKey => { partitionKeys[partitionKey.name] = true; }); } const primaryKeys: { [key: string]: boolean } = {}; if (tableSourceMeta.primary_keys) { tableSourceMeta.primary_keys.forEach(primaryKey => { primaryKeys[primaryKey.name] = true; }); } const foreignKeys: { [key: string]: KeySpecification } = {}; if (tableSourceMeta.foreign_keys) { tableSourceMeta.foreign_keys.forEach(foreignKey => { foreignKeys[foreignKey.name] = foreignKey; }); } const entities: string[] | ExtendedColumn[] = (sourceMeta).databases || (sourceMeta).tables_meta || (sourceMeta).extended_columns || (sourceMeta).fields || (sourceMeta).columns || []; const promises: Promise[] = []; let index = 0; entities.forEach((entity: string | ExtendedColumn) => { if (!(sourceMeta).databases || entity !== '_impala_builtins') { const name = (entity).name || entity; const promise = this.dataCatalog.getEntry({ namespace: this.namespace, compute: this.compute, path: [...this.path, name] }); promise .then(catalogEntry => { if ( !catalogEntry.definition || typeof catalogEntry.definition.index === 'undefined' ) { const definition: BaseDefinition = typeof entity === 'object' ? entity : { name: entity }; if (!definition.type) { if (this.path.length === 0) { definition.type = 'database'; } else if (this.path.length === 1) { definition.type = 'table'; } else if (this.path.length === 2) { definition.type = 'column'; } } if ((sourceMeta).partition_keys) { definition.partitionKey = partitionKeys[name]; } if ((sourceMeta).primary_keys) { definition.primaryKey = primaryKeys[name]; } if ((sourceMeta).foreign_keys) { definition.foreignKey = foreignKeys[name]; } definition.index = index++; catalogEntry.definition = definition; catalogEntry.saveLater(); } }) .catch(err => { console.warn(err); }); promises.push(promise); } }); // TODO: Move to connector attributes if ( sourceMeta && (this.getDialect() === 'impala' || this.getDialect() === 'hive') && this.isComplex() ) { const fieldSourceMeta = sourceMeta; const complexAttributes: (keyof Pick)[] = fieldSourceMeta.type === 'map' ? ['key', 'value'] : ['item']; complexAttributes.forEach(path => { const definition = fieldSourceMeta[path]; if (definition) { const promise = this.dataCatalog.getEntry({ namespace: this.namespace, compute: this.compute, path: [...this.path, path] }); promise .then(catalogEntry => { if ( !catalogEntry.definition || typeof catalogEntry.definition.index === 'undefined' ) { definition.index = index++; definition.isMapValue = path === 'value'; catalogEntry.definition = definition; catalogEntry.saveLater(); } }) .catch(err => { console.warn(err); }); promises.push(promise); } }); } Promise.all(promises).then(resolve).catch(reject); } ); return applyCancellable(this.childrenPromise, options); } /** * Loads navigator metadata for children, only applicable to databases and tables */ loadNavigatorMetaForChildren( options?: Omit ): CancellablePromise { if (this.navigatorMetaForChildrenPromise && this.navigatorMetaForChildrenPromise.cancelled) { this.navigatorMetaPromise = undefined; } options = forceSilencedErrors(options); if (!this.canHaveNavigatorMetadata() || this.isField()) { return CancellablePromise.resolve([]); } if (this.navigatorMetaForChildrenPromise && !shouldReload(options)) { return applyCancellable(this.navigatorMetaForChildrenPromise, options); } this.navigatorMetaForChildrenPromise = new CancellablePromise( async (resolve, reject, onCancel) => { const cancellablePromises: Cancellable[] = []; onCancel(() => { cancellablePromises.forEach(cancellable => { cancellable.cancel(); }); }); try { const childPromise = this.getChildren(options); cancellablePromises.push(childPromise); const children = await childPromise; const someHaveNavMeta = children.some(childEntry => childEntry.navigatorMeta); if (someHaveNavMeta && !shouldReload(options)) { resolve(children); return; } // TODO: Add sourceType to nav search query const query = this.path.length ? `parentPath:"/${this.path.join('/')}" AND type:(table view field)` : 'type:database'; const rejectUnknown = () => { children.forEach(childEntry => { if (!childEntry.navigatorMeta) { childEntry.navigatorMeta = undefined; childEntry.navigatorMetaPromise = undefined; } }); }; const searchPromise = searchEntities({ query, rawQuery: true, limit: children.length, silenceErrors: options?.silenceErrors }); cancellablePromises.push(searchPromise); searchPromise .then(result => { if (result && result.entities) { const childEntryIndex: { [name: string]: DataCatalogEntry } = {}; children.forEach(childEntry => { childEntryIndex[childEntry.name.toLowerCase()] = childEntry; }); result.entities.forEach(entity => { const matchingChildEntry = childEntryIndex[(entity.original_name || entity.originalName).toLowerCase()]; if (matchingChildEntry) { matchingChildEntry.navigatorMeta = entity; entity.hueTimestamp = Date.now(); matchingChildEntry.navigatorMetaPromise = CancellablePromise.resolve( matchingChildEntry.navigatorMeta ); if (entity && matchingChildEntry.commentObservable) { matchingChildEntry.commentObservable(matchingChildEntry.getResolvedComment()); } matchingChildEntry.saveLater(); } }); } }) .catch(() => resolve([])) .finally(() => { rejectUnknown(); resolve(children); }); } catch (err) { resolve([]); return; } } ); return applyCancellable(this.navigatorMetaForChildrenPromise, options); } /** * Helper function used when loading navopt metdata for children */ applySqlAnalyzerResponseToChildren( response: SqlAnalyzerResponse, options?: { silenceErrors?: boolean } ): CancellablePromise { if (!this.definition) { this.definition = {}; } this.definition.sqlAnalyzerLoaded = true; this.saveLater(); return new CancellablePromise(async (resolve, reject, onCancel) => { const childPromise = this.getChildren(options); onCancel(() => { childPromise.cancel(); }); try { const childEntries = await childPromise; const entriesByName: { [name: string]: DataCatalogEntry } = {}; childEntries.forEach(childEntry => { entriesByName[childEntry.name.toLowerCase()] = childEntry; }); const updatedIndex: { [path: string]: DataCatalogEntry } = {}; if (this.isDatabase() && response.top_tables) { response.top_tables.forEach(topTable => { if (!topTable.name) { return; } const matchingChild = entriesByName[topTable.name.toLowerCase()]; if (matchingChild) { matchingChild.sqlAnalyzerPopularity = topTable; matchingChild.saveLater(); updatedIndex[matchingChild.getQualifiedPath()] = matchingChild; } }); } else if (this.isTableOrView() && response.values) { const addSqlAnalyzerPopularity = ( columns: SqlAnalyzerResponsePopularity[] | undefined, type: keyof Pick< SqlAnalyzerPopularity, 'filterColumn' | 'groupByColumn' | 'joinColumn' | 'orderByColumn' | 'selectColumn' > ) => { if (columns) { columns.forEach(column => { if (!column.columnName) { return; } const matchingChild = entriesByName[column.columnName.toLowerCase()]; if (matchingChild) { if (!matchingChild.sqlAnalyzerPopularity) { matchingChild.sqlAnalyzerPopularity = { column_count: 0, columnCount: 0 }; } matchingChild.sqlAnalyzerPopularity[type] = column; matchingChild.saveLater(); updatedIndex[matchingChild.getQualifiedPath()] = matchingChild; } }); } }; addSqlAnalyzerPopularity(response.values.filterColumns, 'filterColumn'); addSqlAnalyzerPopularity(response.values.groupbyColumns, 'groupByColumn'); addSqlAnalyzerPopularity(response.values.joinColumns, 'joinColumn'); addSqlAnalyzerPopularity(response.values.orderbyColumns, 'orderByColumn'); addSqlAnalyzerPopularity(response.values.selectColumns, 'selectColumn'); } const popularEntries: DataCatalogEntry[] = []; Object.keys(updatedIndex).forEach(path => { popularEntries.push(updatedIndex[path]); }); resolve(popularEntries); } catch (err) { reject(err); } }); } /** * Loads SQL Analyzer popularity for the children of this entry. */ loadSqlAnalyzerPopularityForChildren( options: CatalogGetOptions & { sqlAnalyzer: SqlAnalyzer } ): CancellablePromise { if ( this.sqlAnalyzerPopularityForChildrenPromise && this.sqlAnalyzerPopularityForChildrenPromise.cancelled ) { this.sqlAnalyzerPopularityForChildrenPromise = undefined; } options.silenceErrors = true; if (!this.dataCatalog.canHaveSqlAnalyzerMeta()) { return CancellablePromise.reject(); } if (this.sqlAnalyzerPopularityForChildrenPromise && !shouldReload(options)) { return applyCancellable(this.sqlAnalyzerPopularityForChildrenPromise, options); } if (this.definition && this.definition.sqlAnalyzerLoaded && !shouldReload(options)) { this.sqlAnalyzerPopularityForChildrenPromise = new CancellablePromise( async (resolve, reject, onCancel) => { const childPromise = this.getChildren(options); onCancel(() => { childPromise.cancel(); }); try { const children = await childPromise; resolve(children.filter(child => child.sqlAnalyzerPopularity)); } catch (err) { reject(err); } } ); } else if (this.isDatabase() || this.isTableOrView()) { this.sqlAnalyzerPopularityForChildrenPromise = new CancellablePromise( async (resolve, reject, onCancel) => { const cancellablePromises: Cancellable[] = []; onCancel(() => { cancellablePromises.forEach(cancellable => cancellable.cancel()); }); const popularityPromise = options.sqlAnalyzer.fetchPopularity({ ...options, paths: [this.path] }); cancellablePromises.push(popularityPromise); try { const analyzerResponse = await popularityPromise; const applyPromise = this.applySqlAnalyzerResponseToChildren(analyzerResponse, options); cancellablePromises.push(applyPromise); const entries = await applyPromise; resolve(entries); } catch (err) { resolve([]); } } ); } else { this.sqlAnalyzerPopularityForChildrenPromise = CancellablePromise.resolve([]); } return applyCancellable(this.sqlAnalyzerPopularityForChildrenPromise); } /** * Returns true if the catalog entry can have navigator metadata */ canHaveNavigatorMetadata(): boolean { if (!(window).HAS_CATALOG) { return false; } // TODO: Move to connector attributes return ( (this.getDialect() === 'hive' || this.getDialect() === 'impala') && (this.isDatabase() || this.isTableOrView() || this.isColumn()) ); } /** * Returns the currently known comment without loading any additional metadata */ getResolvedComment(): string { // TODO: Move to connector attributes if (this.navigatorMeta && (this.getDialect() === 'hive' || this.getDialect() === 'impala')) { if (this.navigatorMeta.description) { return this.navigatorMeta.description; } if (this.navigatorMeta.originalDescription) { return this.navigatorMeta.originalDescription; } } if (this.definition && this.definition.comment) { return this.definition.comment; } return (this.sourceMeta && this.sourceMeta.comment) || ''; } /** * This can be used to get an observable for the comment which will be updated once a comment has been * resolved. */ getCommentObservable(): KnockoutObservable { if (!this.commentObservable) { this.commentObservable = ko.observable(this.getResolvedComment()); } return this.commentObservable; } /** * Checks whether the comment is known and has been loaded from the proper source */ hasResolvedComment(): boolean { if (this.canHaveNavigatorMetadata()) { return typeof this.navigatorMeta !== 'undefined'; } return typeof this.sourceMeta !== 'undefined'; } /** * Gets the comment for this entry, fetching it if necessary from the proper source. */ getComment(options?: CatalogGetOptions): CancellablePromise { const promise = new CancellablePromise(async (resolve, reject, onCancel) => { const cancellablePromises: Cancellable[] = []; onCancel(() => { cancellablePromises.forEach(cancellable => cancellable.cancel()); }); if (this.canHaveNavigatorMetadata()) { const navigatorMetaPromise = this.getNavigatorMeta(options); cancellablePromises.push(navigatorMetaPromise); try { const navigatorMeta = await navigatorMetaPromise; if (navigatorMeta) { resolve(navigatorMeta.description || navigatorMeta.originalDescription || ''); return; } } catch (err) {} } if (this.sourceMeta) { resolve(this.sourceMeta.comment || ''); } else if (this.definition && this.definition.comment) { resolve(this.definition.comment); } else { const sourceMetaPromise = this.getSourceMeta(options); try { const sourceMeta = await sourceMetaPromise; resolve((sourceMeta && sourceMeta.comment) || ''); } catch (err) { reject(err); } } }); return applyCancellable(promise); } /** * Updates custom navigator metadata for the catalog entry */ async updateNavigatorCustomMetadata( modifiedCustomMetadata?: { [key: string]: string }, deletedCustomMetadataKeys?: string[], apiOptions?: Omit ): Promise { if (!this.canHaveNavigatorMetadata()) { return Promise.reject(); } const navigatorMeta = await this.getNavigatorMeta(apiOptions); if (!navigatorMeta) { throw new Error('Could not load navigator metadata.'); } return new Promise((resolve, reject) => { updateNavigatorProperties({ identity: navigatorMeta.identity, modifiedCustomMetadata, deletedCustomMetadataKeys }) .then(entity => { if (entity) { this.navigatorMeta = entity; this.navigatorMetaPromise = CancellablePromise.resolve(entity); this.saveLater(); resolve(entity); } else { reject(); } }) .catch(reject); }); } /** * Sets the comment in the proper source */ async setComment( comment: string, options?: Omit ): Promise { if (this.canHaveNavigatorMetadata()) { const navigatorMeta = await this.getNavigatorMeta(options); if (!navigatorMeta) { throw new Error('Could not load navigator metadata.'); } return new Promise((resolve, reject) => { updateNavigatorProperties({ identity: navigatorMeta.identity, properties: { description: comment } }) .then(async entity => { if (entity) { this.navigatorMeta = entity; this.navigatorMetaPromise = CancellablePromise.resolve(entity); this.saveLater(); } this.getComment(options) .then(comment => { if (this.commentObservable) { this.commentObservable(comment); } resolve(comment); }) .catch(reject); }) .catch(reject); }); } return new Promise((resolve, reject) => { updateSourceMetadata({ entry: this, properties: { comment: comment }, silenceErrors: options?.silenceErrors }) .then(async () => { try { await this.reloadSourceMeta(options); const comment = await this.getComment(options); if (this.commentObservable) { this.commentObservable(comment); } resolve(comment); } catch (err) { reject(err); } }) .catch(reject); }); } /** * Adds a list of tags and updates the navigator metadata of the entry */ async addNavigatorTags( tags: string[], apiOptions?: Omit ): Promise { if (!this.canHaveNavigatorMetadata()) { return Promise.reject(); } const navigatorMeta = await this.getNavigatorMeta(apiOptions); return new Promise((resolve, reject) => { addNavTags(navigatorMeta.identity, tags) .then(entity => { if (entity) { this.navigatorMeta = entity; this.navigatorMetaPromise = CancellablePromise.resolve(entity); this.saveLater(); resolve(entity); } else { reject(); } }) .catch(reject); }); } /** * Removes a list of tags and updates the navigator metadata of the entry */ async deleteNavigatorTags( tags: string[], apiOptions?: Omit ): Promise { if (!this.canHaveNavigatorMetadata()) { return Promise.reject(); } const navigatorMeta = await this.getNavigatorMeta(apiOptions); return new Promise((resolve, reject) => { deleteNavTags(navigatorMeta.identity, tags) .then(entity => { if (entity) { this.navigatorMeta = entity; this.navigatorMetaPromise = CancellablePromise.resolve(entity); this.saveLater(); resolve(entity); } else { reject(); } }) .catch(reject); }); } /** * Checks if the entry can have children or not without fetching additional metadata. */ hasPossibleChildren(): boolean { return ( this.path.length < 3 || (!this.definition && !this.sourceMeta) || (!!this.sourceMeta && /^(?:struct|array|map)/i.test((this.sourceMeta).type)) || (!!this.definition && !!this.definition.type && /^(?:struct|array|map)/i.test(this.definition.type)) ); } /** * Returns the index representing the order in which the backend returned this entry. */ getIndex(): number { return (this.definition && this.definition.index) || 0; } /** * Returns the dialect of this entry. */ getDialect(): string { return this.getConnector().dialect || this.getConnector().id; // .id for editor v1 } /** * Returns the connector for this entry */ getConnector(): Connector { return this.dataCatalog.connector; } /** * Returns true if the entry represents a data source. * * @return {boolean} */ isSource(): boolean { return this.path.length === 0; } /** * Returns true if the entry is a database. */ isDatabase(): boolean { return this.path.length === 1; } /** * Returns true if the entry is a table or a view. * * @return {boolean} */ isTableOrView(): boolean { return this.path.length === 2; } /** * Returns the default title used for the entry, the qualified path with type for fields. Optionally include * the comment after, if already resolved. */ getTitle(includeComment?: boolean): string { let title = this.getQualifiedPath(); if (this.isField()) { const type = this.getType(); if (type) { title += ' (' + type + ')'; } } else if ( this.definition && this.definition.type && this.definition.type.toLowerCase() === 'materialized_view' ) { title += ' (' + I18n('Materialized') + ')'; } if (includeComment && this.hasResolvedComment() && this.getResolvedComment()) { title += ' - ' + this.getResolvedComment(); } return title; } /** * Returns the fully qualified path for this entry. */ getQualifiedPath(): string { return this.path.join('.'); } /** * Returns the display name for the entry, name or qualified path plus type for fields */ getDisplayName(qualified?: boolean): string { const displayName = qualified ? this.getQualifiedPath() : this.name; if (this.isField()) { const type = this.getType(); if (type) { return `${displayName} (${type})`; } } return displayName; } /** * Returns true for columns that are a primary key. Note that the definition has to come from a parent entry, i.e. * getChildren(). */ isPrimaryKey(): boolean { return !!(this.isColumn() && this.definition && (this.definition).primaryKey); } /** * Returns true if the entry is a partition key. Note that the definition has to come from a parent entry, i.e. * getChildren(). */ isPartitionKey(): boolean { return !!(this.definition && (this.definition).partitionKey); } /** * Returns true if the entry is a foreign key. Note that the definition has to come from a parent entry, i.e. * getChildren(). */ isForeignKey(): boolean { return !!this.definition && !!(this.definition).foreignKey; } /** * Returns true if the entry is either a partition or primary key. Note that the definition has to come from a parent entry, i.e. * getChildren(). */ isKey(): boolean { return this.isPartitionKey() || this.isPrimaryKey() || this.isForeignKey(); } /** * Returns true if the entry is a table. It will be accurate once the source meta has been loaded. */ isTable(): boolean { if (this.path.length === 2) { if ( this.analysis && this.analysis.details && this.analysis.details.properties && this.analysis.details.properties.table_type === 'VIRTUAL_VIEW' ) { return false; } if (this.sourceMeta) { return !(this.sourceMeta).is_view; } if (this.definition && this.definition.type) { return this.definition.type.toLowerCase() === 'table'; } return true; } return false; } getHdfsFilePath(): string { const hdfs_link = this.analysis?.hdfs_link || ''; return hdfs_link.replace('/filebrowser/view=', ''); } /** * Returns true if the entry is an Iceberg table */ isIcebergTable(): boolean { return this.analysis?.details?.stats?.table_type === 'ICEBERG'; } isTransactionalTable(): boolean { return this.analysis?.details?.stats?.transactional === 'true'; } /** * Returns true if the entry is a view. It will be accurate once the source meta has been loaded. */ isView(): boolean { if (this.path.length === 2) { if (this.sourceMeta && (this.sourceMeta).is_view) { return true; } if ( this.definition && this.definition.type && (this.definition.type.toLowerCase() === 'view' || this.definition.type.toLowerCase() === 'materialized_view') ) { return true; } if ( this.analysis && this.analysis.details && this.analysis.details.properties && this.analysis.details.properties.table_type === 'VIRTUAL_VIEW' ) { return true; } } return false; } /** * Returns true if the entry is a ML Model. It will be accurate once the source meta has been loaded. */ isModel(): boolean { return ( this.path.length === 2 && !!this.definition && !!this.definition.type && this.definition.type.toLowerCase() === 'model' ); } /** * Returns true if the entry is a column. */ isColumn(): boolean { return this.path.length === 3; } /** * Returns true if the entry is a column. It will be accurate once the source meta has been loaded or if loaded from * a parent entry via getChildren(). */ isComplex(): boolean { const sourceMeta = this.sourceMeta; return !!( this.path.length > 2 && ((sourceMeta && /^(?:struct|array|map)/i.test(sourceMeta.type)) || (this.definition && this.definition.type && /^(?:struct|array|map)/i.test(this.definition.type))) ); } /** * Returns true if the entry is a field, i.e. column or child of a complex type. */ isField(): boolean { return this.path.length > 2; } /** * Returns true if the entry is an array. It will be accurate once the source meta has been loaded or if loaded from * a parent entry via getChildren(). */ isArray(): boolean { return ( (!!this.sourceMeta && /^array/i.test((this.sourceMeta).type)) || (!!this.definition && !!this.definition.type && /^array/i.test(this.definition.type)) ); } /** * Returns true if the entry is a map. It will be accurate once the source meta has been loaded or if loaded from * a parent entry via getChildren(). */ isMap(): boolean { return ( (!!this.sourceMeta && /^map/i.test((this.sourceMeta).type)) || (!!this.definition && !!this.definition.type && /^map/i.test(this.definition.type)) ); } /** * Returns true if the entry is a map value. It will be accurate once the source meta has been loaded or if loaded * from a parent entry via getChildren(). */ isMapValue(): boolean { return !!this.definition && !!this.definition.isMapValue; } /** * Returns the type of the entry. It will be accurate once the source meta has been loaded or if loaded from * a parent entry via getChildren(). * * The returned string is always lower case and for complex entries the type definition is stripped to * either 'array', 'map' or 'struct'. */ getType(): string { let type = this.getRawType(); if (type.indexOf('<') !== -1) { type = type.substring(0, type.indexOf('<')); } return type.toLowerCase(); } /** * Returns the raw type of the entry. It will be accurate once the source meta has been loaded or if loaded from * a parent entry via getChildren(). * * For complex entries the type definition is the full version. */ getRawType(): string { return ( (this.sourceMeta && (this.sourceMeta).type) || (this.definition && this.definition.type) || '' ); } /** * Gets the source metadata for the entry. It will fetch it if not cached or if the refresh option is set. */ getSourceMeta(options?: CatalogGetOptions): CancellablePromise { if (this.sourceMetaPromise && this.sourceMetaPromise.cancelled) { this.sourceMetaPromise = undefined; } if (!this.sourceMetaPromise && cachedOnly(options)) { return CancellablePromise.reject(); } if (!this.sourceMetaPromise || shouldReload(options)) { return this.reloadSourceMeta(options); } return applyCancellable(this.sourceMetaPromise, options); } /** * Gets the analysis for the entry. It will fetch it if not cached or if the refresh option is set. */ getAnalysis( options?: CatalogGetOptions & { refreshAnalysis?: boolean; } ): CancellablePromise { if (this.analysisPromise && this.analysisPromise.cancelled) { this.analysisPromise = undefined; } if (!this.analysisPromise && cachedOnly(options)) { return CancellablePromise.reject(); } if (!this.analysisPromise || shouldReload(options)) { return this.reloadAnalysis(options); } return applyCancellable(this.analysisPromise, options); } /** * Gets the partitions for the entry. It will fetch it if not cached or if the refresh option is set. */ getPartitions(options?: CatalogGetOptions): CancellablePromise { if (this.partitionsPromise && this.partitionsPromise.cancelled) { this.partitionsPromise = undefined; } if (!this.isTableOrView() || (!this.partitionsPromise && cachedOnly(options))) { return CancellablePromise.reject(); } if (!this.partitionsPromise || shouldReload(options)) { return this.reloadPartitions(options); } return applyCancellable(this.partitionsPromise, options); } /** * Gets the Navigator metadata for the entry. It will fetch it if not cached or if the refresh option is set. */ getNavigatorMeta(options?: CatalogGetOptions): CancellablePromise { if (this.navigatorMetaPromise && this.navigatorMetaPromise.cancelled) { this.navigatorMetaPromise = undefined; } options = forceSilencedErrors(options); if (!this.canHaveNavigatorMetadata() || (!this.navigatorMetaPromise && cachedOnly(options))) { return CancellablePromise.reject(); } if (!this.navigatorMetaPromise || shouldReload(options)) { return this.reloadNavigatorMeta(options); } return applyCancellable(this.navigatorMetaPromise, options); } /** * Gets the SQL Analyzer metadata for the entry. It will fetch it if not cached or if the refresh option is set. */ getSqlAnalyzerMeta( options: CatalogGetOptions & { sqlAnalyzer: SqlAnalyzer } ): CancellablePromise { if (this.sqlAnalyzerMetaPromise && this.sqlAnalyzerMetaPromise.cancelled) { this.sqlAnalyzerMetaPromise = undefined; } options.silenceErrors = true; if (!this.dataCatalog.canHaveSqlAnalyzerMeta() || !this.isTableOrView()) { return CancellablePromise.reject(); } if (!this.sqlAnalyzerMetaPromise && cachedOnly(options)) { return CancellablePromise.reject(); } if (!this.sqlAnalyzerMetaPromise || shouldReload(options)) { return this.reloadSqlAnalyzerMeta(options); } return applyCancellable(this.sqlAnalyzerMetaPromise, options); } /** * Gets the sample for the entry, if unknown it will first check if any parent table already has the sample. It * will fetch it if not cached or if the refresh option is set. */ getSample( options?: CatalogGetOptions & { operation?: string; } ): CancellablePromise { if (this.samplePromise && this.samplePromise.cancelled) { this.samplePromise = undefined; } // This prevents caching of any non-standard sample queries, i.e. DISTINCT etc. if (options && options.operation && options.operation !== 'default') { const operation = options.operation; const samplePromise = fetchSample({ entry: this, operation, silenceErrors: options.silenceErrors }); return applyCancellable(samplePromise, options); } // Check if parent has a sample that we can reuse if (!this.samplePromise && this.isColumn() && !shouldReload(options)) { this.samplePromise = new CancellablePromise(async (resolve, reject, onCancel) => { const cancellablePromises: Cancellable[] = []; onCancel(() => { cancellablePromises.forEach(promise => promise.cancel()); }); try { const tableEntry = await this.dataCatalog.getEntry({ namespace: this.namespace, compute: this.compute, path: this.path.slice(0, 2), definition: { type: 'table' } }); if (tableEntry && tableEntry.samplePromise) { cancellablePromises.push(applyCancellable(tableEntry.samplePromise, options)); const parentSample = await tableEntry.samplePromise; const colSample = { hueTimestamp: parentSample.hueTimestamp, has_more: parentSample.has_more, type: parentSample.type, data: [], meta: [] }; if (parentSample.meta) { for (let i = 0; i < parentSample.meta.length; i++) { if (parentSample.meta[i].name.toLowerCase() === this.name.toLowerCase()) { colSample.meta[0] = parentSample.meta[i]; parentSample.data.forEach(parentRow => { colSample.data.push([parentRow[i]]); }); break; } } } if (colSample.meta.length) { this.sample = colSample; resolve(this.sample); return; } } } catch (err) {} if (cachedOnly(options)) { reject(); } else { const reloadPromise = this.reloadSample(options); try { resolve(await reloadPromise); } catch (err) { reject(); } } }); return applyCancellable(this.samplePromise, options); } if (!this.samplePromise && cachedOnly(options)) { return CancellablePromise.reject(); } if (!this.samplePromise || shouldReload(options)) { return this.reloadSample(options); } return applyCancellable(this.samplePromise, options); } /** * Gets the top aggregate UDFs for the entry if it's a table or view. It will fetch it if not cached or if the refresh option is set. */ getTopAggs( options: CatalogGetOptions & { sqlAnalyzer: SqlAnalyzer } ): CancellablePromise { const promise = new CancellablePromise(async (resolve, reject, onCancel) => { const multiTableEntry = await getMultiTableEntry(this); const topAggsPromise = multiTableEntry.getTopAggs(options); onCancel(() => { topAggsPromise.cancel(); }); topAggsPromise.then(resolve).catch(reject); }); return applyCancellable(promise); } /** * Gets the top filters for the entry if it's a table or view. It will fetch it if not cached or if the refresh option is set. * * @return {CancellableJqPromise} */ getTopFilters( options: CatalogGetOptions & { sqlAnalyzer: SqlAnalyzer } ): CancellablePromise { const promise = new CancellablePromise(async (resolve, reject, onCancel) => { const multiTableEntry = await getMultiTableEntry(this); const topFiltersPromise = multiTableEntry.getTopFilters(options); onCancel(() => { topFiltersPromise.cancel(); }); topFiltersPromise.then(resolve).catch(reject); }); return applyCancellable(promise); } /** * Gets the top joins for the entry if it's a table or view. It will fetch it if not cached or if the refresh option is set. */ getTopJoins( options: CatalogGetOptions & { sqlAnalyzer: SqlAnalyzer } ): CancellablePromise { const promise = new CancellablePromise(async (resolve, reject, onCancel) => { const multiTableEntry = await getMultiTableEntry(this); const topJoinsPromise = multiTableEntry.getTopJoins(options); onCancel(() => { topJoinsPromise.cancel(); }); topJoinsPromise.then(resolve).catch(reject); }); return applyCancellable(promise); } }