diff --git a/superset/assets/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx b/superset/assets/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx
index b57ddd4950..1d0448e6c7 100644
--- a/superset/assets/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx
@@ -9,7 +9,7 @@ import sinon from 'sinon';
import $ from 'jquery';
import shortid from 'shortid';
-import { queries } from './fixtures';
+import { queries, queryWithBadColumns } from './fixtures';
import { sqlLabReducer } from '../../../src/SqlLab/reducers';
import * as actions from '../../../src/SqlLab/actions';
import ExploreResultsButton from '../../../src/SqlLab/components/ExploreResultsButton';
@@ -60,19 +60,35 @@ describe('ExploreResultsButton', () => {
requiresTime: true,
value: 'bar',
};
- const getExploreResultsButtonWrapper = () => (
- shallow( , {
+ const getExploreResultsButtonWrapper = (props = mockedProps) => (
+ shallow( , {
context: { store },
}).dive());
it('renders', () => {
expect(React.isValidElement( )).to.equal(true);
});
+
it('renders with props', () => {
expect(
React.isValidElement( ),
).to.equal(true);
});
+
+ it('detects bad columns', () => {
+ const wrapper = getExploreResultsButtonWrapper({
+ database,
+ show: true,
+ query: queryWithBadColumns,
+ });
+
+ const badCols = wrapper.instance().getInvalidColumns();
+ expect(badCols).to.deep.equal(['COUNT(*)', '1', '123', 'CASE WHEN 1=1 THEN 1 ELSE 0 END']);
+
+ const msgWrapper = shallow(wrapper.instance().renderInvalidColumnMessage());
+ expect(msgWrapper.find('div')).to.have.length(1);
+ });
+
it('renders a Button', () => {
const wrapper = getExploreResultsButtonWrapper();
expect(wrapper.find(Button)).to.have.length(1);
diff --git a/superset/assets/spec/javascripts/sqllab/fixtures.js b/superset/assets/spec/javascripts/sqllab/fixtures.js
index 510b5d5bda..f8d8911cb7 100644
--- a/superset/assets/spec/javascripts/sqllab/fixtures.js
+++ b/superset/assets/spec/javascripts/sqllab/fixtures.js
@@ -179,7 +179,7 @@ export const defaultQueryEditor = {
export const queries = [
{
dbId: 1,
- sql: 'SELECT *FROM superset.slices',
+ sql: 'SELECT * FROM superset.slices',
sqlEditorId: 'SJ8YO72R',
tab: 'Demo',
runAsync: false,
@@ -257,6 +257,43 @@ export const queries = [
results: null,
},
];
+export const queryWithBadColumns = {
+ ...queries[0],
+ results: {
+ data: queries[0].results.data,
+ columns: [{
+ is_date: true,
+ is_dim: false,
+ name: 'COUNT(*)',
+ type: 'STRING',
+ }, {
+ is_date: false,
+ is_dim: true,
+ name: 'this_col_is_ok',
+ type: 'STRING',
+ }, {
+ is_date: false,
+ is_dim: true,
+ name: 'a',
+ type: 'STRING',
+ }, {
+ is_date: false,
+ is_dim: true,
+ name: '1',
+ type: 'STRING',
+ }, {
+ is_date: false,
+ is_dim: true,
+ name: '123',
+ type: 'STRING',
+ }, {
+ is_date: false,
+ is_dim: true,
+ name: 'CASE WHEN 1=1 THEN 1 ELSE 0 END',
+ type: 'STRING',
+ }],
+ },
+};
export const databases = {
result: [{
allow_ctas: true,
diff --git a/superset/assets/src/SqlLab/components/ExploreResultsButton.jsx b/superset/assets/src/SqlLab/components/ExploreResultsButton.jsx
index 493b103c24..f5bbbe8975 100644
--- a/superset/assets/src/SqlLab/components/ExploreResultsButton.jsx
+++ b/superset/assets/src/SqlLab/components/ExploreResultsButton.jsx
@@ -33,12 +33,15 @@ class ExploreResultsButton extends React.PureComponent {
};
this.visualize = this.visualize.bind(this);
this.onClick = this.onClick.bind(this);
+ this.getInvalidColumns = this.getInvalidColumns.bind(this);
+ this.renderInvalidColumnMessage = this.renderInvalidColumnMessage.bind(this);
}
onClick() {
const timeout = this.props.timeout;
+ const msg = this.renderInvalidColumnMessage();
if (Math.round(this.getQueryDuration()) > timeout) {
this.dialog.show({
- title: 'Explore',
+ title: t('Explore'),
body: this.renderTimeoutWarning(),
actions: [
Dialog.CancelAction(),
@@ -51,6 +54,19 @@ class ExploreResultsButton extends React.PureComponent {
dialog.hide();
},
});
+ } else if (msg) {
+ this.dialog.show({
+ title: t('Explore'),
+ body: msg,
+ actions: [
+ Dialog.DefaultAction('Ok', () => {}, 'btn-danger'),
+ ],
+ bsSize: 'large',
+ bsStyle: 'warning',
+ onHide: (dialog) => {
+ dialog.hide();
+ },
+ });
} else {
this.visualize();
}
@@ -65,6 +81,10 @@ class ExploreResultsButton extends React.PureComponent {
getQueryDuration() {
return moment.duration(this.props.query.endDttm - this.props.query.startDttm).asSeconds();
}
+ getInvalidColumns() {
+ const re = /^[A-Za-z_]\w*$/;
+ return this.props.query.results.columns.map(col => col.name).filter(col => !re.test(col));
+ }
datasourceName() {
const { query } = this.props;
const uniqueId = shortid.generate();
@@ -126,6 +146,22 @@ class ExploreResultsButton extends React.PureComponent {
{t('feature to store a summarized data set that you can then explore.')}
);
}
+ renderInvalidColumnMessage() {
+ const invalidColumns = this.getInvalidColumns();
+ if (invalidColumns.length === 0) {
+ return null;
+ }
+ return (
+
+ {t('Column name(s) ')}
+ {invalidColumns.join(', ')}
+ {t('cannot be used as a column name. Please use aliases (as in ')}
+ SELECT count(*)
+ AS my_alias
+
){' '}
+ {t('limited to alphanumeric characters and underscores')}
+
);
+ }
render() {
return (