Skip to content
8 changes: 8 additions & 0 deletions sqlglot/dialects/redshift.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ def _parse_approximate_count(self) -> t.Optional[exp.ApproxDistinct]:
self._retreat(index)
return None

def _parse_projections(self) -> 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)

return projections

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

# Redshift doesn't have `WITH` as part of their with_properties so we remove it
WITH_PROPERTIES_PREFIX = " "
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
7 changes: 7 additions & 0 deletions sqlglot/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2935,6 +2935,13 @@ def select_sql(self, expression: exp.Select) -> str:
table_kind = ""
sql = f"CREATE{table_kind} TABLE {self.sql(into.this)} AS {sql}"

exclude = expression.args.get("exclude")
if 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))

return sql

def schema_sql(self, expression: exp.Schema) -> str:
Expand Down
9 changes: 9 additions & 0 deletions sqlglot/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3484,13 +3484,22 @@ def _parse_select_query(
limit = self._parse_limit(top=True)
projections = self._parse_projections()

# Redshift's EXCLUDE clause, which 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:
projections.append(projections.pop())

this = self.expression(
exp.Select,
kind=kind,
hint=hint,
distinct=distinct,
expressions=projections,
limit=limit,
exclude=exclude,
operation_modifiers=operation_modifiers or None,
)
this.comments = comments
Expand Down
22 changes: 22 additions & 0 deletions tests/dialects/test_redshift.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,28 @@ 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 * EXCLUDE (col2, col3) FROM (SELECT *, 4 AS col4 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 * EXCLUDE (col2, col3) FROM (SELECT *, 4 AS col4 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_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