Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions sqlglot/dialects/redshift.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,25 @@ def _parse_approximate_count(self) -> t.Optional[exp.ApproxDistinct]:
self._retreat(index)
return None

def _parse_projections(self) -> t.Tuple[t.List[exp.Expression], t.List[exp.Expression]]:
projections, _ = super()._parse_projections()
if self._prev and self._prev.text.upper() == "EXCLUDE" and self._curr:
self._retreat(self._index - 1)

# EXCLUDE clause always comes at the end of the projection list and applies to it as a whole
exclude = self._match_text_seq("EXCLUDE") and self._parse_wrapped_csv(
self._parse_expression, optional=True
)

if (
exclude
and isinstance(expr := projections[-1], exp.Alias)
and expr.alias == "EXCLUDE"
):
projections[-1] = expr.this.pop()

return projections, exclude

class Tokenizer(Postgres.Tokenizer):
BIT_STRINGS = []
HEX_STRINGS = []
Expand Down Expand Up @@ -175,6 +194,8 @@ class Generator(Postgres.Generator):
SUPPORTS_DECODE_CASE = True
SUPPORTS_BETWEEN_FLAGS = False
LIMIT_FETCH = "LIMIT"
STAR_EXCEPT = "EXCLUDE"
STAR_EXCLUDE_REQUIRES_DERIVED_TABLE = False

# Redshift doesn't have `WITH` as part of their with_properties so we remove it
WITH_PROPERTIES_PREFIX = " "
Expand Down
9 changes: 6 additions & 3 deletions sqlglot/dialects/tsql.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,21 +768,24 @@ def _parse_for_xml() -> t.Optional[exp.Expression]:

return self._parse_csv(_parse_for_xml)

def _parse_projections(self) -> t.List[exp.Expression]:
def _parse_projections(
self,
) -> t.Tuple[t.List[exp.Expression], t.Optional[t.List[exp.Expression]]]:
"""
T-SQL supports the syntax alias = expression in the SELECT's projection list,
so we transform all parsed Selects to convert their EQ projections into Aliases.

See: https://learn.microsoft.com/en-us/sql/t-sql/queries/select-clause-transact-sql?view=sql-server-ver16#syntax
"""
projections, _ = super()._parse_projections()
return [
(
exp.alias_(projection.expression, projection.this.this, copy=False)
if isinstance(projection, exp.EQ) and isinstance(projection.this, exp.Column)
else projection
)
for projection in super()._parse_projections()
]
for projection in projections
], None

def _parse_commit_or_rollback(self) -> exp.Commit | exp.Rollback:
"""Applies to SQL Server and Azure SQL Database
Expand Down
3 changes: 3 additions & 0 deletions sqlglot/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3947,6 +3947,8 @@ class Lock(Expression):
arg_types = {"update": True, "expressions": False, "wait": False, "key": False}


# In Redshift, * and EXCLUDE can be separated with column projections (e.g., SELECT *, col1 EXCLUDE (col2))
# The "exclude" arg enables correct parsing and transpilation of this clause
class Select(Query):
arg_types = {
"with_": False,
Expand All @@ -3957,6 +3959,7 @@ class Select(Query):
"into": False,
"from_": False,
"operation_modifiers": False,
"exclude": False,
**QUERY_MODIFIERS,
}

Expand Down
15 changes: 15 additions & 0 deletions sqlglot/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,9 @@ class Generator(metaclass=_Generator):
# Unsupported (MySQL, SingleStore): UPDATE t1 JOIN t2 ON TRUE SET t1.a = t2.b
UPDATE_STATEMENT_SUPPORTS_FROM = True

# Whether SELECT *, ... EXCLUDE requires wrapping in a subquery for transpilation.
STAR_EXCLUDE_REQUIRES_DERIVED_TABLE = True

TYPE_MAPPING = {
exp.DataType.Type.DATETIME2: "TIMESTAMP",
exp.DataType.Type.NCHAR: "CHAR",
Expand Down Expand Up @@ -2908,6 +2911,12 @@ def select_sql(self, expression: exp.Select) -> str:
operation_modifiers = self.expressions(expression, key="operation_modifiers", sep=" ")
operation_modifiers = f"{self.sep()}{operation_modifiers}" if operation_modifiers else ""

exclude = expression.args.get("exclude")

if not self.STAR_EXCLUDE_REQUIRES_DERIVED_TABLE and exclude:
exclude_sql = self.expressions(sqls=exclude, flat=True)
expressions = f"{expressions}{self.seg('EXCLUDE')} ({exclude_sql})"

# We use LIMIT_IS_TOP as a proxy for whether DISTINCT should go first because tsql and Teradata
# are the only dialects that use LIMIT_IS_TOP and both place DISTINCT first.
top_distinct = f"{distinct}{hint}{top}" if self.LIMIT_IS_TOP else f"{top}{hint}{distinct}"
Expand All @@ -2926,6 +2935,12 @@ def select_sql(self, expression: exp.Select) -> str:

sql = self.prepend_ctes(expression, sql)

if self.STAR_EXCLUDE_REQUIRES_DERIVED_TABLE and exclude:
expression.set("exclude", None)
subquery = expression.subquery(copy=False)
star = exp.Star(except_=exclude)
sql = self.sql(exp.select(star).from_(subquery, copy=False))

if not self.SUPPORTS_SELECT_INTO and into:
if into.args.get("temporary"):
table_kind = " TEMPORARY"
Expand Down
9 changes: 6 additions & 3 deletions sqlglot/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3356,8 +3356,10 @@ def _parse_value_expression() -> t.Optional[exp.Expression]:
return self.expression(exp.Tuple, expressions=[expression])
return None

def _parse_projections(self) -> t.List[exp.Expression]:
return self._parse_expressions()
def _parse_projections(
self,
) -> t.Tuple[t.List[exp.Expression], t.Optional[t.List[exp.Expression]]]:
return self._parse_expressions(), None

def _parse_wrapped_select(self, table: bool = False) -> t.Optional[exp.Expression]:
if self._match_set((TokenType.PIVOT, TokenType.UNPIVOT)):
Expand Down Expand Up @@ -3482,7 +3484,7 @@ def _parse_select_query(
operation_modifiers.append(exp.var(self._prev.text.upper()))

limit = self._parse_limit(top=True)
projections = self._parse_projections()
projections, exclude = self._parse_projections()

this = self.expression(
exp.Select,
Expand All @@ -3491,6 +3493,7 @@ def _parse_select_query(
distinct=distinct,
expressions=projections,
limit=limit,
exclude=exclude,
operation_modifiers=operation_modifiers or None,
)
this.comments = comments
Expand Down
33 changes: 33 additions & 0 deletions tests/dialects/test_redshift.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,39 @@ def test_identity(self):
"""SELECT CONVERT_TIMEZONE('UTC', 'America/New_York', '2024-08-06 09:10:00.000')""",
)

self.validate_all(
"SELECT *, 4 AS col4 EXCLUDE (col2, col3) FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3)",
write={
"redshift": "SELECT *, 4 AS col4 EXCLUDE (col2, col3) FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3)",
"duckdb": "SELECT * EXCLUDE (col2, col3) FROM (SELECT *, 4 AS col4 FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3))",
"snowflake": "SELECT * EXCLUDE (col2, col3) FROM (SELECT *, 4 AS col4 FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3))",
},
)

self.validate_all(
"SELECT *, 4 AS col4 EXCLUDE col2, col3 FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3)",
write={
"redshift": "SELECT *, 4 AS col4 EXCLUDE (col2, col3) FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3)",
"duckdb": "SELECT * EXCLUDE (col2, col3) FROM (SELECT *, 4 AS col4 FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3))",
"snowflake": "SELECT * EXCLUDE (col2, col3) FROM (SELECT *, 4 AS col4 FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3))",
},
)

self.validate_all(
"SELECT col1, *, col2 EXCLUDE(col3) FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3)",
write={
"redshift": "SELECT col1, *, col2 EXCLUDE (col3) FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3)",
"duckdb": "SELECT * EXCLUDE (col3) FROM (SELECT col1, *, col2 FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3))",
"snowflake": "SELECT * EXCLUDE (col3) FROM (SELECT col1, *, col2 FROM (SELECT 1 AS col1, 2 AS col2, 3 AS col3))",
},
)

self.validate_identity("SELECT 1 EXCLUDE", "SELECT 1 AS EXCLUDE")
self.validate_identity("SELECT 1 EXCLUDE FROM t", "SELECT 1 AS EXCLUDE FROM t")
self.validate_identity("SELECT 1 AS EXCLUDE")
self.validate_identity("SELECT * FROM (SELECT 1 AS EXCLUDE) AS t")
self.validate_identity("SELECT 1 AS EXCLUDE, 2 AS foo")

def test_values(self):
# Test crazy-sized VALUES clause to UNION ALL conversion to ensure we don't get RecursionError
values = [str(v) for v in range(0, 10000)]
Expand Down
Loading