diff --git a/sqlglot/dialects/exasol.py b/sqlglot/dialects/exasol.py index 7848fa5662..3b425ef0d6 100644 --- a/sqlglot/dialects/exasol.py +++ b/sqlglot/dialects/exasol.py @@ -16,8 +16,9 @@ build_date_delta, ) from sqlglot.generator import unsupported_args -from sqlglot.helper import seq_get +from sqlglot.helper import seq_get, find_new_name from sqlglot.tokens import TokenType +from sqlglot.optimizer.scope import build_scope if t.TYPE_CHECKING: from sqlglot.dialects.dialect import DialectType @@ -167,6 +168,71 @@ def _substring_index_sql(self: Exasol.Generator, expression: exp.SubstringIndex) return self.func("SUBSTR", haystack_sql, direction, length) +# https://docs.exasol.com/db/latest/sql/select.htm#:~:text=The%20select_list%20defines%20the%20columns%20of%20the%20result%20table.%20If%20*%20is%20used%2C%20all%20columns%20are%20listed.%20You%20can%20use%20an%20expression%20like%20t.*%20to%20list%20all%20columns%20of%20the%20table%20t%2C%20the%20view%20t%2C%20or%20the%20object%20with%20the%20table%20alias%20t. +def _qualify_unscoped_star(node: exp.Expression) -> exp.Expression: + """ + Exasol doesn't support a bare * alongside other select items, so we rewrite it + Rewrite: SELECT *, FROM + Into: SELECT T.*, FROM
AS T + """ + + if not isinstance(node, exp.Select): + return node + select_expressions = list(node.expressions or []) + + has_bare_star = any( + isinstance(expr, exp.Star) and expr.this is None for expr in select_expressions + ) + + if not has_bare_star or len(select_expressions) <= 1: + return node + + from_clause = node.args.get("from_") + + base_source = from_clause.this if from_clause else None + + if not base_source: + return node + + table_sources: list[exp.Expression] = [base_source] + + table_sources.extend( + join.this + for join in (node.args.get("joins") or []) + if isinstance(join, exp.Join) and join.this + ) + + if not table_sources: + return node + + scope = build_scope(node) + used_alias_names = set(scope.sources.keys()) if scope else set() + + qualifiers: list[exp.Identifier] = [] + + for src in table_sources: + alias = src.args.get("alias") + if isinstance(alias, (exp.TableAlias, exp.Alias)) and alias.name: + name = alias.name + else: + name = find_new_name(used_alias_names, base="T") + src.set("alias", exp.TableAlias(this=exp.to_identifier(name, quoted=False))) + used_alias_names.add(name) + qualifiers.append(exp.to_identifier(name, quoted=False)) + + star_columns = [ + exp.Column(this=exp.Star(), table=alias_identifier) for alias_identifier in qualifiers + ] + + new_items: list[exp.Expression] = [] + for select_expression in select_expressions: + new_items.extend(star_columns) if isinstance( + select_expression, exp.Star + ) and select_expression.this is None else new_items.append(select_expression) + node.set("expressions", new_items) + return node + + DATE_UNITS = {"DAY", "WEEK", "MONTH", "YEAR", "HOUR", "MINUTE", "SECOND"} @@ -425,6 +491,7 @@ def datatype_sql(self, expression: exp.DataType) -> str: exp.CommentColumnConstraint: lambda self, e: f"COMMENT IS {self.sql(e, 'this')}", exp.Select: transforms.preprocess( [ + _qualify_unscoped_star, _add_local_prefix_for_aliases, ] ), diff --git a/tests/dialects/test_exasol.py b/tests/dialects/test_exasol.py index 1c7c69e2f3..713156254a 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -11,6 +11,24 @@ def test_exasol(self): 'SELECT 1 AS "x"', ) + def test_qualify_unscoped_star(self): + self.validate_identity( + "SELECT *, 1 FROM TEST", + "SELECT T.*, 1 FROM TEST AS T", + ) + self.validate_identity( + "WITH t1 AS (SELECT 1 AS c1), t2 AS (SELECT 2 AS c2) SELECT *, 3 FROM t1, t2", + "WITH t1 AS (SELECT 1 AS c1), t2 AS (SELECT 2 AS c2) SELECT T.*, T_2.*, 3 FROM t1 AS T, t2 AS T_2", + ) + self.validate_identity( + 'SELECT *, 3 FROM "A" JOIN "B" ON 1=1', + 'SELECT T.*, T_2.*, 3 FROM "A" AS T JOIN "B" AS T_2 ON 1 = 1', + ) + self.validate_identity( + "SELECT *, 7 FROM (SELECT 1 AS x) s CROSS JOIN (SELECT 2 AS y) q", + "SELECT s.*, q.*, 7 FROM (SELECT 1 AS x) AS s CROSS JOIN (SELECT 2 AS y) AS q", + ) + def test_type_mappings(self): self.validate_identity("CAST(x AS BLOB)", "CAST(x AS VARCHAR)") self.validate_identity("CAST(x AS LONGBLOB)", "CAST(x AS VARCHAR)")