diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx index dfd323e1a0b32..89b8b7a03a9c8 100644 --- a/superset-frontend/src/datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/datasource/DatasourceEditor.jsx @@ -112,7 +112,7 @@ const ColumnButtonWrapper = styled.div` const checkboxGenerator = (d, onChange) => ( ); -const DATA_TYPES = ['STRING', 'NUMERIC', 'DATETIME']; +const DATA_TYPES = ['STRING', 'NUMERIC', 'DATETIME', 'BOOLEAN']; const DATASOURCE_TYPES_ARR = [ { key: 'physical', label: t('Physical (table or view)') }, diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py index f332ce87220cf..844dc48bb89af 100644 --- a/superset/connectors/base/models.py +++ b/superset/connectors/base/models.py @@ -374,6 +374,8 @@ def handle_single_value(value: Optional[FilterValue]) -> Optional[FilterValue]: return None if value == "": return "" + if target_column_type == utils.GenericDataType.BOOLEAN: + return utils.cast_to_boolean(value) return value if isinstance(values, (list, tuple)): diff --git a/superset/utils/core.py b/superset/utils/core.py index 97392878ac040..803bafc84e4e3 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -423,6 +423,35 @@ def cast_to_num(value: Optional[Union[float, int, str]]) -> Optional[Union[float return None +def cast_to_boolean(value: Any) -> bool: + """Casts a value to an int/float + + >>> cast_to_boolean(1) + True + >>> cast_to_boolean(0) + False + >>> cast_to_boolean(0.5) + True + >>> cast_to_boolean('true') + True + >>> cast_to_boolean('false') + False + >>> cast_to_boolean('False') + False + >>> cast_to_boolean(None) + False + + :param value: value to be converted to boolean representation + :returns: value cast to `bool`. when value is 'true' or value that are not 0 + converte into True + """ + if isinstance(value, (int, float)): + return value != 0 + if isinstance(value, str): + return value.strip().lower() == "true" + return False + + def list_minus(l: List[Any], minus: List[Any]) -> List[Any]: """Returns l without what is in minus diff --git a/tests/integration_tests/sqla_models_tests.py b/tests/integration_tests/sqla_models_tests.py index ed1358ac7f45e..f17cedb7c0ecd 100644 --- a/tests/integration_tests/sqla_models_tests.py +++ b/tests/integration_tests/sqla_models_tests.py @@ -20,6 +20,8 @@ from unittest.mock import patch import pytest +import sqlalchemy as sa + from superset import db from superset.connectors.sqla.models import SqlaTable, TableColumn from superset.db_engine_specs.bigquery import BigQueryEngineSpec @@ -264,6 +266,43 @@ class FilterTestCase(NamedTuple): else: self.assertIn(filter_.expected, sql) + @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") + def test_boolean_type_where_operators(self): + table = self.get_table(name="birth_names") + db.session.add( + TableColumn( + column_name="boolean_gender", + expression="case when gender = 'boy' then True else False end", + type="BOOLEAN", + table=table, + ) + ) + query_obj = { + "granularity": None, + "from_dttm": None, + "to_dttm": None, + "groupby": ["boolean_gender"], + "metrics": ["count"], + "is_timeseries": False, + "filter": [ + { + "col": "boolean_gender", + "op": FilterOperator.IN, + "val": ["true", "false"], + } + ], + "extras": {}, + } + sqla_query = table.get_sqla_query(**query_obj) + sql = table.database.compile_sqla_query(sqla_query.sqla_query) + dialect = table.database.get_dialect() + operand = "(true, false)" + # override native_boolean=False behavior in MySQLCompiler + # https://github.com/sqlalchemy/sqlalchemy/blob/master/lib/sqlalchemy/dialects/mysql/base.py + if not dialect.supports_native_boolean and dialect.name != "mysql": + operand = "(1, 0)" + self.assertIn(f"IN {operand}", sql) + def test_incorrect_jinja_syntax_raises_correct_exception(self): query_obj = { "granularity": None,