gethue
Version:
Hue is an Open source SQL Query Editor for Databases/Warehouses
301 lines (276 loc) • 9.68 kB
text/typescript
// 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
};