diff --git a/setup.cfg b/setup.cfg index 28a3ab3189351..9469118b2d61f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ combine_as_imports = true include_trailing_comma = true line_length = 88 known_first_party = superset -known_third_party =alembic,apispec,backoff,bleach,celery,click,colorama,contextlib2,croniter,cryptography,dataclasses,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,geohash,geopy,humanize,isodate,jinja2,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parsedatetime,pathlib2,polyline,prison,pyarrow,pyhive,pytz,retry,selenium,setuptools,simplejson,sphinx_rtd_theme,sqlalchemy,sqlalchemy_utils,sqlparse,werkzeug,wtforms,wtforms_json,yaml +known_third_party =alembic,apispec,backoff,bleach,celery,click,colorama,contextlib2,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,geohash,geopy,humanize,isodate,jinja2,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parsedatetime,pathlib2,polyline,prison,pyarrow,pyhive,pytz,retry,selenium,setuptools,simplejson,sphinx_rtd_theme,sqlalchemy,sqlalchemy_utils,sqlparse,werkzeug,wtforms,wtforms_json,yaml multi_line_output = 3 order_by_type = false diff --git a/superset-frontend/spec/javascripts/explore/AdhocFilter_spec.js b/superset-frontend/spec/javascripts/explore/AdhocFilter_spec.js index dd4818128e363..b699ad93a1d5e 100644 --- a/superset-frontend/spec/javascripts/explore/AdhocFilter_spec.js +++ b/superset-frontend/spec/javascripts/explore/AdhocFilter_spec.js @@ -153,6 +153,36 @@ describe('AdhocFilter', () => { }); // eslint-disable-next-line no-unused-expressions expect(adhocFilter5.isValid()).toBe(true); + + const adhocFilter6 = new AdhocFilter({ + expressionType: EXPRESSION_TYPES.SIMPLE, + subject: 'value', + operator: '==', + comparator: 1, + clause: CLAUSES.WHERE, + }); + // eslint-disable-next-line no-unused-expressions + expect(adhocFilter6.isValid()).toBe(true); + + const adhocFilter7 = new AdhocFilter({ + expressionType: EXPRESSION_TYPES.SIMPLE, + subject: 'value', + operator: '==', + comparator: 0, + clause: CLAUSES.WHERE, + }); + // eslint-disable-next-line no-unused-expressions + expect(adhocFilter7.isValid()).toBe(true); + + const adhocFilter8 = new AdhocFilter({ + expressionType: EXPRESSION_TYPES.SIMPLE, + subject: 'value', + operator: '==', + comparator: null, + clause: CLAUSES.WHERE, + }); + // eslint-disable-next-line no-unused-expressions + expect(adhocFilter8.isValid()).toBe(false); }); it('can translate from simple expressions to sql expressions', () => { diff --git a/superset-frontend/src/explore/AdhocFilter.js b/superset-frontend/src/explore/AdhocFilter.js index 0c84abc1c9fd0..74542bbb18fd5 100644 --- a/superset-frontend/src/explore/AdhocFilter.js +++ b/superset-frontend/src/explore/AdhocFilter.js @@ -135,13 +135,17 @@ export default class AdhocFilter { return !!(this.operator && this.subject); } - return !!( - this.operator && - this.subject && - this.comparator && - this.comparator.length > 0 && - this.clause - ); + if (this.operator && this.subject && this.clause) { + if (Array.isArray(this.comparator)) { + if (this.comparator.length > 0) { + // A non-empty array of values ('IN' or 'NOT IN' clauses) + return true; + } + } else if (this.comparator !== null) { + // A value has been selected or typed + return true; + } + } } else if (this.expressionType === EXPRESSION_TYPES.SQL) { return !!(this.sqlExpression && this.clause); } diff --git a/superset/sql_parse.py b/superset/sql_parse.py index bb6f341238e36..67ea2108ca21b 100644 --- a/superset/sql_parse.py +++ b/superset/sql_parse.py @@ -15,11 +15,11 @@ # specific language governing permissions and limitations # under the License. import logging +from dataclasses import dataclass from typing import List, Optional, Set from urllib import parse import sqlparse -from dataclasses import dataclass from sqlparse.sql import ( Function, Identifier,