gethue
Version:
Hue is an Open source SQL Query Editor for Databases/Warehouses
775 lines (707 loc) • 29.5 kB
JavaScript
// 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 $ from 'jquery';
import * as ko from 'knockout';
import ASSIST_TABLE_TEMPLATES from '../../../ko/components/assist/ko.assistDbPanel';
import AssistDbEntry from '../../../ko/components/assist/assistDbEntry';
import AssistDbSource from '../../../ko/components/assist/assistDbSource';
import componentUtils from '../../../ko/components/componentUtils';
import dataCatalog from '../../../catalog/dataCatalog';
import huePubSub from '../../../utils/huePubSub';
import I18n from '../../../utils/i18n';
import DataCatalogEntry from '../../../catalog/DataCatalogEntry';
const TEMPLATE =
ASSIST_TABLE_TEMPLATES +
`
<div class="assist-inner-panel assist-assistant-panel">
<div class="assist-flex-panel">
<div class="assist-flex-header">
<div class="assist-inner-header">
<!-- ko if: isSolr -->
${I18n('Indexes')}
<!-- /ko -->
<!-- ko ifnot: isSolr -->
${I18n('Tables')}
<!-- ko if: statementCount() > 1 -->
<div class="statement-count">${I18n(
'Statement'
)} <span data-bind="text: activeStatementIndex() + '/' + statementCount()"></span></div>
<!-- /ko -->
<!-- /ko -->
</div>
</div>
<div class="assist-flex-search" data-bind="visible: activeTables().length > 0">
<div class="assist-filter">
<!-- ko component: {
name: 'inline-autocomplete',
params: {
querySpec: filter.querySpec,
facets: ['type'],
knownFacetValues: isSolr() ? SOLR_ASSIST_KNOWN_FACET_VALUES : SQL_ASSIST_KNOWN_FACET_VALUES,
autocompleteFromEntries: $component.autocompleteFromEntries
}
} --><!-- /ko -->
</div>
</div>
<div class="assist-flex-fill assist-db-scrollable" data-bind="delayedOverflow">
<!-- ko if: filteredTables().length === 0 && (!filter.querySpec() || filter.querySpec().query === '') -->
<div class="assist-no-entries">
<!-- ko if: isSolr -->
${I18n('No indexes selected.')}
<!-- /ko -->
<!-- ko ifnot: isSolr -->
${I18n('No tables identified.')}
<!-- /ko -->
</div>
<!-- /ko -->
<!-- ko if: filteredTables().length === 0 && filter.querySpec() && filter.querySpec().query !== '' && !someLoading() -->
<div class="assist-no-entries">${I18n('No entries found')}</div>
<!-- /ko -->
<!-- ko if: filteredTables().length > 0 -->
<ul class="database-tree assist-tables" data-bind="foreachVisible: { data: filteredTables, minHeight: 22, container: '.assist-db-scrollable', skipScrollEvent: true }">
<!-- ko if: hasErrors -->
<li class="assist-table hue-warning" data-bind="attr: { 'title': $parent.isSolr() ? '${I18n(
'Error loading index details.'
)}' : '${I18n('Error loading table details.')}'}">
<span class="assist-entry">
<i class="hue-warning fa fa-fw muted valign-middle fa-warning"></i>
<!-- ko with: catalogEntry -->
<!-- ko if: typeof reload !== 'undefined' -->
<span data-bind="text: getDisplayName(true)"></span> <a class="inactive-action" href="javascript: void(0);" data-bind="click: reload"><i class="fa fa-refresh" data-bind="css: { 'fa-spin': reloading }"></i></a>
<!-- /ko -->
<!-- /ko -->
</span>
</li>
<!-- /ko -->
<!-- ko ifnot: hasErrors -->
<!-- ko template: { if: catalogEntry.isTableOrView(), name: 'assist-table-entry' } --><!-- /ko -->
<!-- ko template: { if: catalogEntry.isField(), name: 'assist-column-entry-assistant' } --><!-- /ko -->
<!-- /ko -->
</ul>
<!-- /ko -->
<!-- ko hueSpinner: { spin: filter.querySpec() && filter.querySpec().query !== '' && someLoading(), inline: true, center: true} --><!-- /ko -->
</div>
<!-- ko if: showRisks -->
<div class="assist-flex-header assist-divider"><div class="assist-inner-header">${I18n(
'Query Analysis'
)}</div></div>
<div class="assist-flex-third">
<!-- ko if: ! activeRisks().hints -->
<div class="assist-no-entries">${I18n(
'Select a query or start typing to get optimization hints.'
)}</div>
<!-- /ko -->
<!-- ko if: activeRisks().hints && activeRisks().hints.length === 0 -->
<div class="assist-no-entries">${I18n('No optimizations identified.')}</div>
<!-- /ko -->
<!-- ko if: activeRisks().hints && activeRisks().hints.length > 0 -->
<ul class="risk-list" data-bind="foreach: activeRisks().hints">
<li>
<div class="risk-list-title" data-bind="css: { 'risk-list-high' : risk === 'high', 'risk-list-normal': risk !== 'high' }, tooltip: { title: risk + ' ' + riskTables }"><span data-bind="text: riskAnalysis"></span></div>
<div class="risk-list-description" data-bind="text: riskRecommendation"></div>
<div class="risk-quickfix" data-bind="visible: (riskId === 17 || riskId === 18 || riskId === 22) && $parent.activeEditor() && $parent.activeLocations()" style="display:none;">
<a href="javascript:void(0);" data-bind="click: function () { $parent.addFilter(riskId); hueAnalytics.convert('sqlAnalyzer', 'addFilter/' + riskId); }">${I18n(
'Fix me'
)}</a>
</div>
</li>
</ul>
<!-- /ko -->
<!-- ko if: hasMissingRisks() -->
<div class="margin-top-20">
<!-- ko hueSpinner: { spin: uploadingTableStats, inline: true} --><!-- /ko -->
<!-- ko ifnot: uploadingTableStats -->
<a href="javascript:void(0)" data-bind="visible: activeTables().length > 0, click: function() { uploadTableStats(true) }, attr: { 'title': ('${I18n(
'Add table'
)} ' + (isMissingDDL() ? 'DDL' : '') + (isMissingDDL() && isMissingStats() ? ' ${I18n(
'and'
)} ' : '') + (isMissingStats() ? 'stats' : '')) }">
<i class="fa fa-fw fa-plus-circle"></i> ${I18n('Improve Analysis')}
</a>
<!-- /ko -->
</div>
<!-- /ko -->
</div>
<!-- /ko -->
</div>
</div>
`;
class AssistantUtils {
static getFilteredTablesPureComputed(vm) {
const openedByFilter = [];
return ko.pureComputed(() => {
if (
!vm.filter ||
!vm.filter.querySpec() ||
((!vm.filter.querySpec().facets ||
Object.keys(vm.filter.querySpec().facets).length === 0) &&
(!vm.filter.querySpec().text || vm.filter.querySpec().text.length === 0))
) {
while (openedByFilter.length) {
openedByFilter.pop().open(false);
}
return vm.activeTables();
}
const facets = vm.filter.querySpec().facets;
const result = [];
vm.activeTables().forEach(entry => {
let facetMatch =
!facets || !facets['type'] || (!facets['type']['table'] && !facets['type']['view']);
if (!facetMatch && facets['type']['table']) {
facetMatch = entry.catalogEntry.isTable();
}
if (!facetMatch && facets['type']['view']) {
facetMatch = entry.catalogEntry.isView();
}
let textMatch = !vm.filter.querySpec().text || vm.filter.querySpec().text.length === 0;
if (!textMatch) {
const nameLower = entry.catalogEntry.name.toLowerCase();
textMatch = vm.filter
.querySpec()
.text.every(text => nameLower.indexOf(text.toLowerCase()) !== -1);
}
entry.filterColumnNames(!textMatch);
if ((facetMatch && textMatch) || entry.filteredEntries().length > 0) {
if (!entry.open()) {
entry.open(true);
openedByFilter.push(entry);
}
result.push(entry);
}
});
return result;
});
}
}
class AssistEditorContextPanel {
constructor(params) {
this.isSolr = ko.observable(false);
this.activeTab = params.activeTab;
this.connector = params.connector;
this.showRisks = ko.pureComputed(() => {
if (!window.HAS_SQL_ANALYZER || this.isSolr()) {
return false;
}
if (this.connector().dialect_properties) {
// TODO: dialect_properties only for when ENABLE_CONNECTORS is enabled
return this.connector().dialect_properties.has_optimizer_risks;
} else {
return this.connector().dialect === 'impala' || this.connector().dialect === 'hive';
}
});
this.uploadingTableStats = ko.observable(false);
this.activeStatement = ko.observable();
this.activeTables = ko.observableArray();
this.activeRisks = ko.observable({});
this.activeEditor = ko.observable();
this.activeRisks.subscribe(() => {
if (this.isMissingDDL()) {
this.uploadTableStats(false);
}
});
this.activeLocations = ko.observable();
this.statementCount = ko.observable(0);
this.activeStatementIndex = ko.observable(0);
this.hasActiveRisks = ko.pureComputed(
() => this.activeRisks().hints && this.activeRisks().hints.length > 0
);
this.hasMissingRisks = ko.pureComputed(() => this.isMissingDDL() || this.isMissingStats());
this.isMissingDDL = ko.pureComputed(
() => this.activeRisks().noDDL && this.activeRisks().noDDL.length > 0
);
this.isMissingStats = ko.pureComputed(
() =>
window.AUTO_UPLOAD_SQL_ANALYZER_STATS &&
this.activeRisks().noStats &&
this.activeRisks().noStats.length > 0
);
this.someLoading = ko.pureComputed(() =>
this.activeTables().some(
table => table.loading() || (!table.hasEntries() && !table.hasErrors())
)
);
const createQualifiedIdentifier = (identifierChain, defaultDatabase) => {
if (identifierChain.length === 1) {
return defaultDatabase + '.' + identifierChain[0].name;
}
return identifierChain
.map(identifier => identifier.name)
.join('.')
.toLowerCase();
};
this.filter = {
querySpec: ko
.observable({
query: '',
facets: {},
text: []
})
.extend({ rateLimit: 300 })
};
this.filteredTables = AssistantUtils.getFilteredTablesPureComputed(this);
const navigationSettings = {
showStats: true,
rightAssist: true
};
const i18n = {};
const sources = {};
let loadEntriesTimeout = -1;
// This fetches the columns for each table synchronously with 2 second in between.
const loadEntries = currentCount => {
let count = currentCount || 0;
count++;
if (count > 10) {
return;
}
window.clearTimeout(loadEntriesTimeout);
if (this.activeTables().length === 1) {
const singleTable = this.activeTables()[0];
if (!singleTable.hasErrors()) {
singleTable.open(true);
}
} else {
loadEntriesTimeout = window.setTimeout(() => {
this.activeTables().every(table => {
if (!table.loaded && !table.hasErrors() && !table.loading()) {
table.loadEntries(() => {
loadEntries(count);
});
return false;
}
return !table.loading();
});
}, 2000);
}
};
this.autocompleteFromEntries = (nonPartial, partial) => {
const added = {};
const result = [];
const partialLower = partial.toLowerCase();
this.filteredTables().forEach(table => {
if (
!added[table.catalogEntry.name] &&
table.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0
) {
added[table.catalogEntry.name] = true;
result.push(nonPartial + partial + table.catalogEntry.name.substring(partial.length));
}
table.entries().forEach(col => {
if (
!added[col.catalogEntry.name] &&
col.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0
) {
added[col.catalogEntry.name] = true;
result.push(nonPartial + partial + col.catalogEntry.name.substring(partial.length));
}
});
});
return result;
};
this.activeTables.subscribe(loadEntries);
let updateOnVisible = false;
const runningPromises = [];
const handleLocationUpdate = activeLocations => {
while (runningPromises.length) {
const promise = runningPromises.pop();
if (promise.cancel) {
promise.cancel();
}
}
updateOnVisible = false;
if (!sources[activeLocations.connector.id]) {
sources[activeLocations.connector.id] = {
assistDbSource: new AssistDbSource({
i18n: i18n,
initialNamespace: activeLocations.namespace,
connector: activeLocations.connector,
type: activeLocations.connector.id,
name: activeLocations.connector.displayName,
navigationSettings: navigationSettings
}),
databaseIndex: {},
activeTableIndex: {}
};
}
const assistDbSource = sources[activeLocations.connector.id].assistDbSource;
const databaseIndex = sources[activeLocations.connector.id].databaseIndex;
const activeTableIndex = sources[activeLocations.connector.id].activeTableIndex;
if (!activeLocations) {
this.activeLocations(undefined);
return;
}
this.activeLocations(activeLocations);
this.statementCount(activeLocations.totalStatementCount);
this.activeStatementIndex(activeLocations.activeStatementIndex);
if (activeLocations.activeStatementLocations) {
let updateTables = false;
const tableQidIndex = {};
const ctes = {};
activeLocations.activeStatementLocations.forEach(location => {
if (location.type === 'alias' && location.source === 'cte') {
ctes[location.alias.toLowerCase()] = true;
}
});
activeLocations.activeStatementLocations.forEach(location => {
if (
location.type === 'table' &&
(location.identifierChain.length !== 1 ||
!ctes[location.identifierChain[0].name.toLowerCase()])
) {
const tableDeferred = $.Deferred();
const dbDeferred = $.Deferred();
runningPromises.push(tableDeferred);
runningPromises.push(dbDeferred);
const qid = createQualifiedIdentifier(
location.identifierChain,
activeLocations.defaultDatabase
);
if (activeTableIndex[qid]) {
tableQidIndex[qid] = true;
tableDeferred.resolve(activeTableIndex[qid]);
dbDeferred.resolve(activeTableIndex[qid].parent);
} else {
let database =
location.identifierChain.length === 2
? location.identifierChain[0].name
: activeLocations.defaultDatabase;
database = database.toLowerCase();
if (databaseIndex[database]) {
dbDeferred.resolve(databaseIndex[database]);
} else {
dataCatalog
.getEntry({
namespace: activeLocations.namespace,
compute: activeLocations.compute,
connector: activeLocations.connector,
path: [database],
definition: { type: 'database' }
})
.then(catalogEntry => {
databaseIndex[database] = new AssistDbEntry(
catalogEntry,
null,
assistDbSource,
this.filter,
i18n,
navigationSettings
);
updateTables = true;
dbDeferred.resolve(databaseIndex[database]);
})
.catch(() => {
dbDeferred.reject();
});
}
dbDeferred
.done(dbEntry => {
dbEntry.catalogEntry
.getChildren({ silenceErrors: true })
.then(tableEntries => {
const tableName =
location.identifierChain[location.identifierChain.length - 1].name;
const found = tableEntries.some(tableEntry => {
if (tableEntry.name.toLowerCase() === tableName.toLowerCase()) {
const assistTableEntry = new AssistDbEntry(
tableEntry,
dbEntry,
assistDbSource,
this.filter,
i18n,
navigationSettings
);
activeTableIndex[
createQualifiedIdentifier(
location.identifierChain,
activeLocations.defaultDatabase
)
] = assistTableEntry;
tableQidIndex[qid] = true;
updateTables = true;
tableDeferred.resolve(assistTableEntry);
return true;
}
});
if (!found) {
const catalogEntry = new DataCatalogEntry({
path: [dbEntry.catalogEntry.name, tableName],
dataCatalog: dbEntry.catalogEntry.dataCatalog,
namespace: dbEntry.catalogEntry.namespace,
compute: dbEntry.catalogEntry.namespace
});
catalogEntry.reloading = ko.observable(false);
catalogEntry.reload = function () {
const self = this;
if (self.reloading()) {
return;
}
self.reloading(true);
huePubSub.subscribeOnce('data.catalog.entry.refreshed', data => {
data.entry.getSourceMeta({ silenceErrors: true }).finally(() => {
self.reloading(false);
});
});
dataCatalog
.getEntry({
namespace: activeLocations.namespace,
compute: activeLocations.compute,
connector: activeLocations.connector,
path: []
})
.then(sourceEntry => {
sourceEntry
.getChildren()
.then(dbEntries => {
let clearPromise;
// Clear the database first if it exists without cascade
const hasDb = dbEntries.some(dbEntry => {
if (dbEntry.name.toLowerCase() === self.path[0].toLowerCase()) {
clearPromise = dbEntry.clearCache({
invalidate: 'invalidate',
cascade: false
});
return true;
}
});
if (!hasDb) {
// If the database is missing clear the source without cascade
clearPromise = sourceEntry.clearCache({
invalidate: 'invalidate',
cascade: false
});
}
clearPromise.catch(() => {
self.reloading(false);
});
})
.catch(() => {
self.reloading(false);
});
})
.catch(() => {
self.reloading(false);
});
};
const missingEntry = new AssistDbEntry(
catalogEntry,
dbEntry,
assistDbSource,
this.filter,
i18n,
navigationSettings
);
activeTableIndex[
createQualifiedIdentifier(
location.identifierChain,
activeLocations.defaultDatabase
)
] = missingEntry;
tableQidIndex[qid] = true;
updateTables = true;
missingEntry.hasErrors(true);
tableDeferred.resolve(missingEntry);
}
})
.catch(tableDeferred.reject);
})
.catch(tableDeferred.reject);
}
}
});
$.when.apply($, runningPromises).always(() => {
runningPromises.length = 0;
Object.keys(activeTableIndex).forEach(key => {
if (!tableQidIndex[key]) {
delete activeTableIndex[key];
updateTables = true;
}
});
if (updateTables) {
const tables = [];
Object.keys(activeTableIndex).forEach(key => {
tables.push(activeTableIndex[key]);
});
tables.sort((a, b) => {
return a.catalogEntry.name.localeCompare(b.catalogEntry.name);
});
this.activeTables(tables);
}
});
}
};
huePubSub.subscribe('data.catalog.entry.refreshed', details => {
const connectorId = details.entry.getConnector().id;
if (sources[connectorId]) {
let completeRefresh = false;
if (details.entry.isSource()) {
sources[connectorId].databaseIndex = {};
sources[connectorId].activeTableIndex = {};
completeRefresh = true;
} else if (
details.entry.isDatabase() &&
sources[connectorId].databaseIndex[details.entry.name]
) {
const dbEntry = sources[connectorId].databaseIndex[details.entry.name];
const activeTableIndex = sources[connectorId].activeTableIndex;
Object.keys(activeTableIndex).forEach(tableKey => {
const tableEntry = activeTableIndex[tableKey];
if (tableEntry.parent === dbEntry) {
delete activeTableIndex[tableKey];
completeRefresh = true;
}
});
} else if (details.entry.isTableOrView()) {
const activeTableIndex = sources[connectorId].activeTableIndex;
if (activeTableIndex[details.entry.getQualifiedPath()]) {
delete activeTableIndex[details.entry.getQualifiedPath()];
completeRefresh = true;
}
}
if (completeRefresh) {
handleLocationUpdate(this.activeLocations());
}
}
});
if (this.activeTab() === 'editorAssistant') {
huePubSub.publish('get.active.editor.locations', handleLocationUpdate);
} else {
updateOnVisible = true;
}
this.activeTab.subscribe(activeTab => {
if (activeTab === 'editorAssistant' && updateOnVisible) {
huePubSub.publish('get.active.editor.locations', handleLocationUpdate);
}
});
huePubSub.subscribe('editor.active.locations', activeLocations => {
if (this.activeTab() === 'editorAssistant') {
handleLocationUpdate(activeLocations);
} else {
updateOnVisible = true;
}
});
huePubSub.subscribe('editor.active.risks', details => {
if (details.risks !== this.activeRisks()) {
this.activeRisks(details.risks);
this.activeEditor(details.editor);
}
});
huePubSub.publish('editor.get.active.risks', details => {
this.activeRisks(details.risks);
this.activeEditor(details.editor);
});
}
addFilter(riskId) {
if (this.activeLocations() && this.activeEditor()) {
this.activeLocations().activeStatementLocations.every(location => {
let isLowerCase = false;
if (
this.activeLocations().activeStatementLocations &&
this.activeLocations().activeStatementLocations.length > 0
) {
const firstToken = this.activeLocations().activeStatementLocations[0].firstToken;
isLowerCase = firstToken === firstToken.toLowerCase();
}
if (
riskId === 22 &&
location.type === 'whereClause' &&
!location.subquery &&
location.missing
) {
this.activeEditor().moveCursorToPosition({
row: location.location.last_line - 1,
column: location.location.last_column - 1
});
this.activeEditor().clearSelection();
if (/\S$/.test(this.activeEditor().getTextBeforeCursor())) {
this.activeEditor().session.insert(this.activeEditor().getCursorPosition(), ' ');
}
const operation = location.missing ? 'WHERE ' : 'AND ';
this.activeEditor().session.insert(
this.activeEditor().getCursorPosition(),
isLowerCase ? operation.toLowerCase() : operation
);
this.activeEditor().focus();
if (riskId === 22) {
huePubSub.publish('editor.autocomplete.temporary.sort.override', {
partitionColumnsFirst: true
});
}
window.setTimeout(() => {
this.activeEditor().execCommand('startAutocomplete');
}, 1);
return false;
}
if (
riskId === 17 &&
location.type === 'limitClause' &&
!location.subquery &&
location.missing
) {
this.activeEditor().moveCursorToPosition({
row: location.location.last_line - 1,
column: location.location.last_column - 1
});
this.activeEditor().clearSelection();
if (/\S$/.test(this.activeEditor().getTextBeforeCursor())) {
this.activeEditor().session.insert(this.activeEditor().getCursorPosition(), ' ');
}
const operation = 'LIMIT';
this.activeEditor().session.insert(
this.activeEditor().getCursorPosition(),
isLowerCase ? operation.toLowerCase() : operation
);
this.activeEditor().focus();
window.setTimeout(() => {
this.activeEditor().execCommand('startAutocomplete');
}, 1);
return false;
}
if (riskId === 18 && location.type === 'asterisk' && !location.subquery) {
this.activeEditor().moveCursorToPosition({
row: location.location.last_line - 1,
column: location.location.last_column - 1
});
this.activeEditor().clearSelection();
this.activeEditor().removeTextBeforeCursor(1);
this.activeEditor().focus();
window.setTimeout(() => {
this.activeEditor().execCommand('startAutocomplete');
}, 1);
return false;
}
return true;
});
}
}
uploadTableStats(showProgress) {
if (this.uploadingTableStats()) {
return;
}
this.uploadingTableStats(true);
huePubSub.publish('editor.upload.table.stats', {
activeTables: this.activeTables(),
showProgress: showProgress,
callback: () => {
this.uploadingTableStats(false);
}
});
}
}
componentUtils.registerStaticComponent(
'assist-editor-context-panel',
AssistEditorContextPanel,
TEMPLATE
);
export { TEMPLATE, AssistantUtils };