Files
superset2/superset/sql/dialects/firebolt.py
2025-06-04 22:18:09 -04:00

165 lines
5.5 KiB
Python

# 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 __future__ import annotations
from sqlglot import exp, generator, parser, tokens
from sqlglot.dialects.dialect import Dialect
from sqlglot.helper import csv
from sqlglot.tokens import TokenType
class Firebolt(Dialect):
"""
A sqlglot dialect for Firebolt.
"""
class Parser(parser.Parser):
"""
Custom parser for Firebolt.
In Firebolt `NOT` has higher precedence than `IN`, so we need to wrap the
expression in parentheses when we find a negated range.
"""
UNARY_PARSERS = {
**parser.Parser.UNARY_PARSERS,
TokenType.NOT: lambda self: self.expression(
exp.Not,
this=self._parse_unary(), # pylint: disable=protected-access
),
}
def _negate_range(
self,
this: exp.Expression | None = None,
) -> exp.Expression | None:
if not this: # pragma: no cover
return this
return self.expression(exp.Not, this=self.expression(exp.Paren, this=this))
class Generator(generator.Generator):
"""
Custom generator for Firebolt.
"""
TYPE_MAPPING = {
**generator.Generator.TYPE_MAPPING,
exp.DataType.Type.VARBINARY: "BYTEA",
}
def not_sql(self, expression: exp.Not) -> str:
"""
Parenthesize negated expressions.
Firebolt requires negated to be wrapped in parentheses, since NOT has higher
precedence than IN.
"""
if isinstance(expression.this, exp.In):
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 Tokenizer(tokens.Tokenizer):
STRING_ESCAPES = ["'", "\\"]
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)
return self.expression(
exp.Unnest,
expressions=expressions,
offset=offset,
)
class Generator(Firebolt.Generator):
def join_sql(self, expression: exp.Join) -> str:
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 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}"
op_sql = f"{op_sql} JOIN" if op_sql else "JOIN"
return f"{self.seg(op_sql)} {this_sql}{match_cond}{on_sql}"