UNPKG

gethue

Version:

Hue is an Open source SQL Query Editor for Databases/Warehouses

301 lines (276 loc) 9.68 kB
// 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 { Cancellable, CancellablePromise } from '../api/cancellablePromise'; import { applyCancellable } from '../catalog/catalogUtils'; import DataCatalogEntry from '../catalog/DataCatalogEntry'; import dataCatalog from '../catalog/dataCatalog'; import { IdentifierChainEntry, ParsedLocation, ParsedTable } from '../parse/types'; import sqlReferenceRepository from '../sql/reference/sqlReferenceRepository'; import { CommentDetails, Suggestion } from '../apps/editor/components/aceEditor/autocomplete/AutocompleteResults'; import { Compute, Connector, Namespace } from '../config/types'; import { SqlReferenceProvider } from './reference/types'; const identifierEquals = (a?: string, b?: string): boolean => !!a && !!b && a.replace(/^\s*`/, '').replace(/`\s*$/, '').toLowerCase() === b.replace(/^\s*`/, '').replace(/`\s*$/, '').toLowerCase(); const autocompleteFilter = (filter: string, entries: Suggestion[]): Suggestion[] => { const lowerCaseFilter = filter.toLowerCase(); return entries.filter(suggestion => { // TODO: Extend with fuzzy matches let foundIndex = suggestion.value.toLowerCase().indexOf(lowerCaseFilter); if (foundIndex !== -1) { if ( foundIndex === 0 || (suggestion.filterValue && suggestion.filterValue.toLowerCase().indexOf(lowerCaseFilter) === 0) ) { suggestion.filterWeight = 3; } else { suggestion.filterWeight = 2; } } else if ( suggestion.details && (<CommentDetails>suggestion.details).comment && lowerCaseFilter.indexOf(' ') === -1 ) { foundIndex = (<CommentDetails>suggestion.details).comment .toLowerCase() .indexOf(lowerCaseFilter); if (foundIndex !== -1) { suggestion.filterWeight = 1; suggestion.matchComment = true; } } if (foundIndex !== -1) { suggestion.matchIndex = foundIndex; suggestion.matchLength = filter.length; return true; } return false; }); }; export interface SortOverride { partitionColumnsFirst?: boolean; } const sortSuggestions = ( suggestions: Suggestion[], filter: string, sortOverride?: SortOverride | null ): void => { suggestions.sort((a, b) => { if (filter) { if ( typeof a.filterWeight !== 'undefined' && typeof b.filterWeight !== 'undefined' && b.filterWeight !== a.filterWeight ) { return b.filterWeight - a.filterWeight; } if (typeof a.filterWeight !== 'undefined' && typeof b.filterWeight === 'undefined') { return -1; } if (typeof a.filterWeight === 'undefined' && typeof b.filterWeight !== 'undefined') { return 1; } } if (sortOverride && sortOverride.partitionColumnsFirst) { if (a.partitionKey && !b.partitionKey) { return -1; } if (b.partitionKey && !a.partitionKey) { return 1; } } const aWeight = (a.category.weight || 0) + (a.weightAdjust || 0); const bWeight = (b.category.weight || 0) + (b.weightAdjust || 0); if (typeof aWeight !== 'undefined' && typeof bWeight !== 'undefined' && bWeight !== aWeight) { return bWeight - aWeight; } if (typeof aWeight !== 'undefined' && typeof bWeight === 'undefined') { return -1; } if (typeof aWeight === 'undefined' && typeof bWeight !== 'undefined') { return 1; } return a.value.localeCompare(b.value); }); }; const identifierChainToPath = (identifierChain: IdentifierChainEntry[]): string[] => identifierChain.map(identifier => identifier.name); export const resolveCatalogEntry = (options: { connector: Connector; namespace: Namespace; compute: Compute; temporaryOnly?: boolean; cachedOnly?: boolean; cancellable?: boolean; identifierChain?: IdentifierChainEntry[]; tables?: ParsedTable[]; }): CancellablePromise<DataCatalogEntry> => { const promise = new CancellablePromise<DataCatalogEntry>((resolve, reject, onCancel) => { const cancellablePromises: Cancellable[] = []; onCancel(() => { cancellablePromises.forEach(cancellable => cancellable.cancel()); }); if (!options.identifierChain) { reject(); return; } const findInTree = (currentEntry: DataCatalogEntry, fieldsToGo: string[]): void => { if (fieldsToGo.length === 0) { reject(); return; } let nextField: string; if (currentEntry.getType() === 'map') { nextField = 'value'; } else if (currentEntry.getType() === 'array') { nextField = 'item'; } else { nextField = fieldsToGo.shift() || ''; } const childPromise = currentEntry.getChildren({ cancellable: !!options.cancellable, cachedOnly: !!options.cachedOnly, silenceErrors: true }); cancellablePromises.push(childPromise); childPromise .then(childEntries => { let foundEntry = undefined; childEntries.some((childEntry: { name: string }) => { if (identifierEquals(childEntry.name, nextField)) { foundEntry = childEntry; return true; } }); if (foundEntry && fieldsToGo.length) { findInTree(foundEntry, fieldsToGo); } else if (foundEntry) { resolve(foundEntry); } else { reject(); } }) .catch(reject); }; const findTable = (tablesToGo: ParsedTable[]): void => { if (tablesToGo.length === 0) { reject(); return; } const nextTable = tablesToGo.pop(); if (nextTable && typeof nextTable.subQuery !== 'undefined') { findTable(tablesToGo); return; } const childPromise = dataCatalog.getChildren({ connector: options.connector, namespace: options.namespace, compute: options.compute, path: identifierChainToPath((nextTable && nextTable.identifierChain) || []), cachedOnly: !!options.cachedOnly, cancellable: !!options.cancellable, temporaryOnly: !!options.temporaryOnly, silenceErrors: true }); cancellablePromises.push(childPromise); childPromise .then(childEntries => { let foundEntry = undefined; childEntries.some((childEntry: { name: string }) => { if ( options.identifierChain && options.identifierChain.length && identifierEquals(childEntry.name, options.identifierChain[0].name) ) { foundEntry = childEntry; return true; } }); if (foundEntry && options.identifierChain && options.identifierChain.length > 1) { findInTree(foundEntry, identifierChainToPath(options.identifierChain.slice(1))); } else if (foundEntry) { resolve(foundEntry); } else { findTable(tablesToGo); } }) .catch(reject); }; if (options.tables) { findTable(options.tables.concat()); } else { dataCatalog .getEntry({ namespace: options.namespace, compute: options.compute, connector: options.connector, path: [], cachedOnly: !!options.cachedOnly, temporaryOnly: !!options.temporaryOnly }) .then(entry => { if (options.identifierChain) { findInTree(entry, identifierChainToPath(options.identifierChain)); } }); } }); return applyCancellable(promise, options); }; export default { autocompleteFilter: autocompleteFilter, backTickIfNeeded: async ( connector: Connector, identifier: string, sqlReferenceProvider?: SqlReferenceProvider, forceAppendBacktick?: boolean ): Promise<string> => { if (forceAppendBacktick) { return identifier + '`'; } const quoteChar = (connector.dialect_properties && connector.dialect_properties.sql_identifier_quote) || '`'; if (identifier.indexOf(quoteChar) === 0) { return identifier; } const reservedKeywords = await ( sqlReferenceProvider || sqlReferenceRepository ).getReservedKeywords(connector.dialect || 'generic'); if (reservedKeywords.has(identifier.toUpperCase())) { return quoteChar + identifier + quoteChar; } if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(identifier)) { return quoteChar + identifier + quoteChar; } return identifier; }, locationEquals: (a?: ParsedLocation, b?: ParsedLocation): boolean => !!a && !!b && a.first_line === b.first_line && a.first_column === b.first_column && a.last_line === b.last_line && a.last_column === b.last_column, identifierEquals: identifierEquals, sortSuggestions: sortSuggestions, identifierChainToPath: identifierChainToPath };