This is an automated email from the ASF dual-hosted git repository.
villebro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 7eceebce1ce feat(dbspec): Support apache datafusion dbspec (#42374)
7eceebce1ce is described below
commit 7eceebce1ceaca78b548e705ab3630fa1e9dd687
Author: Bhargava Vadlamani <[email protected]>
AuthorDate: Fri Jul 24 11:56:30 2026 -0700
feat(dbspec): Support apache datafusion dbspec (#42374)
---
docs/src/data/databases.json | 108 +++++++++++++++++++--
docs/static/img/databases/datafusion.png | Bin 0 -> 14784 bytes
pyproject.toml | 1 +
superset/db_engine_specs/datafusion.py | 92 ++++++++++++++++++
superset_text.yml | 1 +
.../unit_tests/db_engine_specs/test_datafusion.py | 101 +++++++++++++++++++
6 files changed, 296 insertions(+), 7 deletions(-)
diff --git a/docs/src/data/databases.json b/docs/src/data/databases.json
index c2209b079ef..33662b38ea1 100644
--- a/docs/src/data/databases.json
+++ b/docs/src/data/databases.json
@@ -1,16 +1,16 @@
{
"generated": "2026-04-17T17:08:35.132Z",
"statistics": {
- "totalDatabases": 80,
- "withDocumentation": 80,
- "withConnectionString": 80,
- "withDrivers": 28,
+ "totalDatabases": 81,
+ "withDocumentation": 81,
+ "withConnectionString": 81,
+ "withDrivers": 29,
"withAuthMethods": 5,
- "supportsJoins": 74,
- "supportsSubqueries": 77,
+ "supportsJoins": 75,
+ "supportsSubqueries": 78,
"supportsDynamicSchema": 29,
"supportsCatalog": 20,
- "averageScore": 54,
+ "averageScore": 53,
"maxScore": 201,
"byCategory": {
"Cloud - AWS": [
@@ -20,6 +20,7 @@
],
"Query Engines": [
"Amazon Athena",
+ "Apache DataFusion",
"Apache Drill",
"Apache Hive",
"Apache Impala",
@@ -94,6 +95,7 @@
"Vertica"
],
"Apache Projects": [
+ "Apache DataFusion",
"Apache Doris",
"Apache Drill",
"Apache Druid",
@@ -108,6 +110,7 @@
"Databricks Interactive Cluster"
],
"Open Source": [
+ "Apache DataFusion",
"Apache Doris",
"Apache Drill",
"Apache Druid",
@@ -9698,6 +9701,97 @@
"supports_dynamic_schema": true,
"supports_catalog": true,
"supports_dynamic_catalog": true
+ },
+ "Apache DataFusion": {
+ "time_grains": {
+ "SECOND": true,
+ "FIVE_SECONDS": true,
+ "THIRTY_SECONDS": true,
+ "MINUTE": true,
+ "FIVE_MINUTES": true,
+ "TEN_MINUTES": true,
+ "FIFTEEN_MINUTES": true,
+ "THIRTY_MINUTES": true,
+ "HALF_HOUR": false,
+ "HOUR": true,
+ "SIX_HOURS": false,
+ "DAY": true,
+ "WEEK": true,
+ "WEEK_STARTING_SUNDAY": false,
+ "WEEK_STARTING_MONDAY": false,
+ "WEEK_ENDING_SATURDAY": false,
+ "WEEK_ENDING_SUNDAY": false,
+ "MONTH": true,
+ "QUARTER": true,
+ "QUARTER_YEAR": false,
+ "YEAR": true
+ },
+ "module": "superset.db_engine_specs.datafusion",
+ "limit_method": 1,
+ "limit_clause": true,
+ "joins": true,
+ "subqueries": true,
+ "alias_in_select": true,
+ "alias_in_orderby": true,
+ "time_groupby_inline": false,
+ "alias_to_source_column": false,
+ "order_by_not_in_select": true,
+ "expressions_in_orderby": false,
+ "cte_in_subquery": true,
+ "max_column_name": null,
+ "sql_comments": true,
+ "escaped_colons": true,
+ "masked_encrypted_extra": false,
+ "column_type_mapping": false,
+ "function_names": false,
+ "user_impersonation": false,
+ "file_upload": false,
+ "get_extra_table_metadata": false,
+ "dbapi_exception_mapping": false,
+ "custom_errors": false,
+ "dynamic_schema": false,
+ "catalog": false,
+ "dynamic_catalog": false,
+ "ssh_tunneling": true,
+ "query_cancelation": false,
+ "get_metrics": false,
+ "where_latest_partition": false,
+ "expand_data": false,
+ "query_cost_estimation": false,
+ "sql_validation": false,
+ "score": 24,
+ "max_score": 201,
+ "documentation": {
+ "description": "DataFusion is a highly performant query engine",
+ "logo": "datafusion.png",
+ "homepage_url": "https://datafusion.apache.org/",
+ "categories": [
+ "Query Engines",
+ "Open Source",
+ "Apache Projects"
+ ],
+ "pypi_packages": [
+ "flightsql-dbapi"
+ ],
+ "connection_string": "datafusion://host:port",
+ "drivers": [
+ {
+ "name": "Arrow Flight SQL (Recommended)",
+ "pypi_package": "flightsql-dbapi",
+ "connection_string": "datafusion://host:port",
+ "is_recommended": true
+ }
+ ],
+ "category": "Apache Projects"
+ },
+ "engine": "datafusion",
+ "engine_name": "Apache DataFusion",
+ "engine_aliases": [],
+ "default_driver": "flightsql",
+ "supports_file_upload": false,
+ "supports_dynamic_schema": false,
+ "supports_catalog": false,
+ "supports_dynamic_catalog": false
}
}
}
diff --git a/docs/static/img/databases/datafusion.png
b/docs/static/img/databases/datafusion.png
new file mode 100644
index 00000000000..381265e62d7
Binary files /dev/null and b/docs/static/img/databases/datafusion.png differ
diff --git a/pyproject.toml b/pyproject.toml
index d379670d325..ecaf600014f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -142,6 +142,7 @@ databricks = [
"databricks-sql-connector>=4.2.6, <4.4.0",
"databricks-sqlalchemy==1.0.5",
]
+datafusion = ["flightsql-dbapi>=0.2.0, <0.3"]
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
diff --git a/superset/db_engine_specs/datafusion.py
b/superset/db_engine_specs/datafusion.py
new file mode 100644
index 00000000000..ec7a9ac4f64
--- /dev/null
+++ b/superset/db_engine_specs/datafusion.py
@@ -0,0 +1,92 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF 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.
+
+from datetime import datetime
+from typing import Any
+
+from sqlalchemy import types
+
+from superset.constants import TimeGrain
+from superset.db_engine_specs import BaseEngineSpec
+from superset.db_engine_specs.base import DatabaseCategory
+
+
+class DataFusionEngineSpec(BaseEngineSpec):
+ engine_name = "Apache DataFusion"
+ engine = "datafusion"
+ drivers = {
+ "flightsql": "Arrow Flight SQL protocol for DataFusion",
+ }
+ default_driver = "flightsql"
+ sqlalchemy_uri_placeholder = "datafusion://host:port"
+ supports_file_upload = False
+
+ metadata = {
+ "description": "DataFusion is a highly performant query engine",
+ "logo": "datafusion.png",
+ "homepage_url": "https://datafusion.apache.org/",
+ "categories": [
+ DatabaseCategory.QUERY_ENGINES,
+ DatabaseCategory.OPEN_SOURCE,
+ DatabaseCategory.APACHE_PROJECTS,
+ ],
+ "pypi_packages": ["flightsql-dbapi"],
+ "connection_string": "datafusion://host:port",
+ "drivers": [
+ {
+ "name": "Arrow Flight SQL (Recommended)",
+ "pypi_package": "flightsql-dbapi",
+ "connection_string": "datafusion://host:port",
+ "is_recommended": True,
+ }
+ ],
+ }
+
+ _time_grain_expressions = {
+ None: "{col}",
+ TimeGrain.SECOND: "DATE_TRUNC('second', {col})",
+ TimeGrain.FIVE_SECONDS: "DATE_TRUNC('minute', {col}) + INTERVAL '5
seconds' * FLOOR(EXTRACT(SECOND FROM {col}) / 5)", # noqa: E501
+ TimeGrain.THIRTY_SECONDS: "DATE_TRUNC('minute', {col}) + INTERVAL '30
seconds' * FLOOR(EXTRACT(SECOND FROM {col}) / 30)", # noqa: E501
+ TimeGrain.MINUTE: "DATE_TRUNC('minute', {col})",
+ TimeGrain.FIVE_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '5
minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 5)", # noqa: E501
+ TimeGrain.TEN_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '10
minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 10)", # noqa: E501
+ TimeGrain.FIFTEEN_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '15
minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 15)", # noqa: E501
+ TimeGrain.THIRTY_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '30
minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 30)", # noqa: E501
+ TimeGrain.HOUR: "DATE_TRUNC('hour', {col})",
+ TimeGrain.DAY: "DATE_TRUNC('day', {col})",
+ TimeGrain.WEEK: "DATE_TRUNC('week', {col})",
+ TimeGrain.MONTH: "DATE_TRUNC('month', {col})",
+ TimeGrain.QUARTER: "DATE_TRUNC('quarter', {col})",
+ TimeGrain.YEAR: "DATE_TRUNC('year', {col})",
+ }
+
+ @classmethod
+ def epoch_to_dttm(cls) -> str:
+ return "from_unixtime({col})"
+
+ @classmethod
+ def convert_dttm(
+ cls, target_type: str, dttm: datetime, db_extra: dict[str, Any] | None
= None
+ ) -> str | None:
+ sqla_type = cls.get_sqla_column_type(target_type)
+
+ if isinstance(sqla_type, types.Date):
+ return f"TO_DATE('{dttm.date().isoformat()}', 'YYYY-MM-DD')"
+ if isinstance(sqla_type, types.DateTime):
+ dttm_formatted = dttm.isoformat(sep=" ", timespec="milliseconds")
+ return f"""TO_TIMESTAMP('{dttm_formatted}', 'YYYY-MM-DD
HH24:MI:SS.FFF')"""
+ return None
diff --git a/superset_text.yml b/superset_text.yml
index 3b3a346b54f..aeb84d631b7 100644
--- a/superset_text.yml
+++ b/superset_text.yml
@@ -19,6 +19,7 @@
# To set the images of your preferred database, you may create a mapping here
with engine and locations of the relevant images. The image can be hosted
locally inside your static/file directory or online (e.g. S3).
# DB_IMAGES:
+# datafusion: "path/to/image/datafusion.jpg"
# postgresql: "path/to/image/postgres.jpg"
# bigquery: "path/to/s3bucket/bigquery.jpg"
# snowflake: "path/to/image/snowflake.jpg"
diff --git a/tests/unit_tests/db_engine_specs/test_datafusion.py
b/tests/unit_tests/db_engine_specs/test_datafusion.py
new file mode 100644
index 00000000000..0595f885cfd
--- /dev/null
+++ b/tests/unit_tests/db_engine_specs/test_datafusion.py
@@ -0,0 +1,101 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF 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.
+from datetime import datetime
+
+import pytest
+
+from superset.db_engine_specs.datafusion import DataFusionEngineSpec
+from tests.unit_tests.db_engine_specs.utils import assert_convert_dttm
+from tests.unit_tests.fixtures.common import dttm # noqa: F401
+
+
[email protected](
+ "target_type,expected_result",
+ [
+ ("Date", "TO_DATE('2019-01-02', 'YYYY-MM-DD')"),
+ (
+ "DateTime",
+ "TO_TIMESTAMP('2019-01-02 03:04:05.678', 'YYYY-MM-DD
HH24:MI:SS.FFF')",
+ ),
+ (
+ "TimeStamp",
+ "TO_TIMESTAMP('2019-01-02 03:04:05.678', 'YYYY-MM-DD
HH24:MI:SS.FFF')",
+ ),
+ ("UnknownType", None),
+ ],
+)
+def test_convert_dttm(
+ target_type: str,
+ expected_result: str | None,
+ dttm: datetime, # noqa: F811
+) -> None:
+ assert_convert_dttm(DataFusionEngineSpec, target_type, expected_result,
dttm)
+
+
+def test_epoch_to_dttm() -> None:
+ assert DataFusionEngineSpec.epoch_to_dttm().format(col="ts") ==
"from_unixtime(ts)"
+
+
[email protected](
+ "time_grain,expected",
+ [
+ (None, "ts"),
+ ("PT1S", "DATE_TRUNC('second', ts)"),
+ (
+ "PT5S",
+ "DATE_TRUNC('minute', ts) + INTERVAL '5 seconds' * "
+ "FLOOR(EXTRACT(SECOND FROM ts) / 5)",
+ ),
+ (
+ "PT30S",
+ "DATE_TRUNC('minute', ts) + INTERVAL '30 seconds' * "
+ "FLOOR(EXTRACT(SECOND FROM ts) / 30)",
+ ),
+ ("PT1M", "DATE_TRUNC('minute', ts)"),
+ (
+ "PT5M",
+ "DATE_TRUNC('hour', ts) + INTERVAL '5 minutes' * "
+ "FLOOR(EXTRACT(MINUTE FROM ts) / 5)",
+ ),
+ (
+ "PT10M",
+ "DATE_TRUNC('hour', ts) + INTERVAL '10 minutes' * "
+ "FLOOR(EXTRACT(MINUTE FROM ts) / 10)",
+ ),
+ (
+ "PT15M",
+ "DATE_TRUNC('hour', ts) + INTERVAL '15 minutes' * "
+ "FLOOR(EXTRACT(MINUTE FROM ts) / 15)",
+ ),
+ (
+ "PT30M",
+ "DATE_TRUNC('hour', ts) + INTERVAL '30 minutes' * "
+ "FLOOR(EXTRACT(MINUTE FROM ts) / 30)",
+ ),
+ ("PT1H", "DATE_TRUNC('hour', ts)"),
+ ("P1D", "DATE_TRUNC('day', ts)"),
+ ("P1W", "DATE_TRUNC('week', ts)"),
+ ("P1M", "DATE_TRUNC('month', ts)"),
+ ("P3M", "DATE_TRUNC('quarter', ts)"),
+ ("P1Y", "DATE_TRUNC('year', ts)"),
+ ],
+)
+def test_time_grain_expressions(time_grain: str | None, expected: str) -> None:
+ assert (
+
DataFusionEngineSpec._time_grain_expressions[time_grain].format(col="ts")
+ == expected
+ )