This is an automated email from the ASF dual-hosted git repository.

beto 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 4ca5846c7f feat: old Firebolt dialect (#31849)
4ca5846c7f is described below

commit 4ca5846c7f2156348e431686496407ec4cfe2b46
Author: Beto Dealmeida <[email protected]>
AuthorDate: Wed Jan 15 09:02:37 2025 -0500

    feat: old Firebolt dialect (#31849)
---
 superset/config.py                  |   4 +-
 superset/initialization/__init__.py |   7 ++-
 superset/sql/dialects/__init__.py   |   4 ++
 superset/sql/dialects/firebolt.py   | 117 ++++++++++++++++++++++++++++++++++++
 superset/sql/parse.py               |   4 +-
 tests/unit_tests/sql/parse_tests.py |  18 ++++++
 6 files changed, 149 insertions(+), 5 deletions(-)

diff --git a/superset/config.py b/superset/config.py
index fa55318f69..f274cf31e5 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -74,6 +74,8 @@ if TYPE_CHECKING:
     from superset.models.dashboard import Dashboard
     from superset.models.slice import Slice
 
+    DialectExtensions = dict[str, Dialects | type[Dialect]]
+
 # Realtime stats logger, a StatsD implementation exists
 STATS_LOGGER = DummyStatsLogger()
 
@@ -251,7 +253,7 @@ SQLALCHEMY_ENCRYPTED_FIELD_TYPE_ADAPTER = (  # pylint: 
disable=invalid-name
 )
 
 # Extends the default SQLGlot dialects with additional dialects
-SQLGLOT_DIALECTS_EXTENSIONS: dict[str, Dialects | type[Dialect]] = {}
+SQLGLOT_DIALECTS_EXTENSIONS: DialectExtensions | Callable[[], 
DialectExtensions] = {}
 
 # The limit of queries fetched for query search
 QUERY_SEARCH_LIMIT = 1000
diff --git a/superset/initialization/__init__.py 
b/superset/initialization/__init__.py
index ee7fcf9ef1..10a5e50898 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -547,7 +547,12 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         feature_flag_manager.init_app(self.superset_app)
 
     def configure_sqlglot_dialects(self) -> None:
-        SQLGLOT_DIALECTS.update(self.config["SQLGLOT_DIALECTS_EXTENSIONS"])
+        extensions = self.config["SQLGLOT_DIALECTS_EXTENSIONS"]
+
+        if callable(extensions):
+            extensions = extensions()
+
+        SQLGLOT_DIALECTS.update(extensions)
 
     @transaction()
     def configure_fab(self) -> None:
diff --git a/superset/sql/dialects/__init__.py 
b/superset/sql/dialects/__init__.py
index 13a83393a9..ab09de3c2a 100644
--- a/superset/sql/dialects/__init__.py
+++ b/superset/sql/dialects/__init__.py
@@ -14,3 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
+from .firebolt import Firebolt, FireboltOld
+
+__all__ = ["Firebolt", "FireboltOld"]
diff --git a/superset/sql/dialects/firebolt.py 
b/superset/sql/dialects/firebolt.py
index 119ee3ba19..c939dee45b 100644
--- a/superset/sql/dialects/firebolt.py
+++ b/superset/sql/dialects/firebolt.py
@@ -19,6 +19,7 @@ from __future__ import annotations
 
 from sqlglot import exp, generator, parser
 from sqlglot.dialects.dialect import Dialect
+from sqlglot.helper import csv
 from sqlglot.tokens import TokenType
 
 
@@ -73,3 +74,119 @@ class Firebolt(Dialect):
                 return f"NOT ({self.sql(expression, 'this')})"
 
             return super().not_sql(expression)
+
+
+class FireboltOld(Firebolt):
+    """
+    Dialect for the old version of Firebolt (https://old.docs.firebolt.io/).
+
+    The main difference is that `UNNEST` is an operator like `JOIN`, instead 
of a
+    function.
+    """
+
+    class Parser(Firebolt.Parser):
+        TABLE_ALIAS_TOKENS = Firebolt.Parser.TABLE_ALIAS_TOKENS - 
{TokenType.UNNEST}
+
+        def _parse_join(
+            self,
+            skip_join_token: bool = False,
+            parse_bracket: bool = False,
+        ) -> exp.Join | None:
+            if unnest := self._parse_unnest():
+                return self.expression(exp.Join, this=unnest)
+
+            return super()._parse_join(skip_join_token, parse_bracket)
+
+        def _parse_unnest(self, with_alias: bool = True) -> exp.Unnest | None:
+            if not self._match(TokenType.UNNEST):
+                return None
+
+            # parse expressions (col1 AS foo), instead of equalities as in the 
original
+            # dialect
+            expressions = self._parse_wrapped_csv(self._parse_expression)
+            offset = self._match_pair(TokenType.WITH, TokenType.ORDINALITY)
+
+            alias = self._parse_table_alias() if with_alias else None
+
+            if alias:
+                if self.dialect.UNNEST_COLUMN_ONLY:
+                    if alias.args.get("columns"):
+                        self.raise_error("Unexpected extra column alias in 
unnest.")
+
+                    alias.set("columns", [alias.this])
+                    alias.set("this", None)
+
+                columns = alias.args.get("columns") or []
+                if offset and len(expressions) < len(columns):
+                    offset = columns.pop()
+
+            if not offset and self._match_pair(TokenType.WITH, 
TokenType.OFFSET):
+                self._match(TokenType.ALIAS)
+                offset = self._parse_id_var(
+                    any_token=False, tokens=self.UNNEST_OFFSET_ALIAS_TOKENS
+                ) or exp.to_identifier("offset")
+
+            return self.expression(
+                exp.Unnest,
+                expressions=expressions,
+                alias=alias,
+                offset=offset,
+            )
+
+    class Generator(Firebolt.Generator):
+        def join_sql(self, expression: exp.Join) -> str:
+            if not self.SEMI_ANTI_JOIN_WITH_SIDE and expression.kind in (
+                "SEMI",
+                "ANTI",
+            ):
+                side = None
+            else:
+                side = expression.side
+
+            op_sql = " ".join(
+                op
+                for op in (
+                    expression.method,
+                    "GLOBAL" if expression.args.get("global") else None,
+                    side,
+                    expression.kind,
+                    expression.hint if self.JOIN_HINTS else None,
+                )
+                if op
+            )
+            match_cond = self.sql(expression, "match_condition")
+            match_cond = f" MATCH_CONDITION ({match_cond})" if match_cond else 
""
+            on_sql = self.sql(expression, "on")
+            using = expression.args.get("using")
+
+            if not on_sql and using:
+                on_sql = csv(*(self.sql(column) for column in using))
+
+            this = expression.this
+            this_sql = self.sql(this)
+
+            if exprs := self.expressions(expression):
+                this_sql = f"{this_sql},{self.seg(exprs)}"
+
+            if on_sql:
+                on_sql = self.indent(on_sql, skip_first=True)
+                space = self.seg(" " * self.pad) if self.pretty else " "
+                if using:
+                    on_sql = f"{space}USING ({on_sql})"
+                else:
+                    on_sql = f"{space}ON {on_sql}"
+            elif not op_sql:
+                # the main difference with the base dialect is the lack of 
comma before
+                # an `UNNEST`
+                if (
+                    isinstance(this, exp.Lateral)
+                    and this.args.get("cross_apply") is not None
+                ) or isinstance(this, exp.Unnest):
+                    return f" {this_sql}"
+
+                return f", {this_sql}"
+
+            if op_sql != "STRAIGHT_JOIN":
+                op_sql = f"{op_sql} JOIN" if op_sql else "JOIN"
+
+            return f"{self.seg(op_sql)} {this_sql}{match_cond}{on_sql}"
diff --git a/superset/sql/parse.py b/superset/sql/parse.py
index 3fd13a8006..f5923fecc7 100644
--- a/superset/sql/parse.py
+++ b/superset/sql/parse.py
@@ -40,8 +40,6 @@ from superset.sql.dialects.firebolt import Firebolt
 
 logger = logging.getLogger(__name__)
 
-# register 3rd party dialects
-Dialect.classes["firebolt"] = Firebolt
 
 # mapping between DB engine specs and sqlglot dialects
 SQLGLOT_DIALECTS = {
@@ -65,7 +63,7 @@ SQLGLOT_DIALECTS = {
     # "elasticsearch": ???
     # "exa": ???
     # "firebird": ???
-    "firebolt": "firebolt",
+    "firebolt": Firebolt,
     "gsheets": Dialects.SQLITE,
     "hana": Dialects.POSTGRES,
     "hive": Dialects.HIVE,
diff --git a/tests/unit_tests/sql/parse_tests.py 
b/tests/unit_tests/sql/parse_tests.py
index a2aff686a7..2df24a1b3a 100644
--- a/tests/unit_tests/sql/parse_tests.py
+++ b/tests/unit_tests/sql/parse_tests.py
@@ -1146,3 +1146,21 @@ SELECT
 FROM tbl
     """.strip()
     )
+
+
+def test_firebolt_old() -> None:
+    """
+    Test the dialect for the old Firebolt syntax.
+    """
+    from superset.sql.dialects import FireboltOld
+    from superset.sql.parse import SQLGLOT_DIALECTS
+
+    SQLGLOT_DIALECTS["firebolt"] = FireboltOld
+
+    sql = "SELECT * FROM t1 UNNEST(col1 AS foo)"
+    assert (
+        SQLStatement(sql, "firebolt").format()
+        == """SELECT
+  *
+FROM t1 UNNEST(col1 AS foo)"""
+    )

Reply via email to