Skip to content
46 changes: 46 additions & 0 deletions sqlglot/dialects/exasol.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from sqlglot.generator import unsupported_args
from sqlglot.helper import seq_get
from sqlglot.tokens import TokenType
from sqlglot.optimizer.scope import build_scope

if t.TYPE_CHECKING:
from sqlglot.dialects.dialect import DialectType
Expand Down Expand Up @@ -169,6 +170,50 @@ 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(expression: exp.Expression) -> exp.Expression:
"""
Exasol doesn't support a bare * alongside other select items, so we rewrite it
Rewrite: SELECT *, <other> FROM <Table>
Into: SELECT T.*, <other> FROM <Table> AS T
"""

if not isinstance(expression, exp.Select):
return expression

select_expressions = expression.expressions or []

def is_bare_star(expr: exp.Expression) -> bool:
return isinstance(expr, exp.Star) and expr.this is None

has_bare_star = any(is_bare_star(select) for select in select_expressions)

has_other_expression = any(not (is_bare_star(select)) for select in select_expressions)

if not (has_bare_star and has_other_expression):
return expression

scope = build_scope(expression)

if not scope or not scope.selected_sources:
return expression

qualified_star_columns = [
exp.Column(this=exp.Star(), table=exp.to_identifier(source_name))
for source_name in scope.selected_sources.keys()
]

new_select_expressions: list[exp.Expression] = []

for select_expr in select_expressions:
new_select_expressions.extend(qualified_star_columns) if is_bare_star(
select_expr
) else new_select_expressions.append(select_expr)

expression.set("expressions", new_select_expressions)
return expression


def _add_date_sql(self: Exasol.Generator, expression: DATE_ADD_OR_SUB) -> str:
interval = expression.expression if isinstance(expression.expression, exp.Interval) else None

Expand Down Expand Up @@ -453,6 +498,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,
]
),
Expand Down
30 changes: 30 additions & 0 deletions tests/dialects/test_exasol.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,36 @@ def test_exasol(self):
'SELECT 1 AS "x"',
)

def test_qualify_unscoped_star(self):
self.validate_identity(
"SELECT *, 1 FROM TEST",
"SELECT TEST.*, 1 FROM TEST",
)
self.validate_identity(
"SELECT t.*, 1 FROM t",
)
self.validate_identity(
"SELECT t.* FROM t",
)
self.validate_identity(
"SELECT * FROM t",
)
self.validate_identity(
"WITH t AS (SELECT 1 AS x) SELECT t.*, 3 FROM 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 t1.*, t2.*, 3 FROM t1, t2",
)
self.validate_identity(
'SELECT *, 3 FROM "A" JOIN "B" ON 1=1',
'SELECT A.*, B.*, 3 FROM "A" JOIN "B" 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)")
Expand Down