Skip to content

Commit 2c013a5

Browse files
authored
Feat(exasol): qualify bare stars to facilitate transpilation (#6431)
* chore(exasol): implemented a select preprocessor to qualify bare '*' and add alias in exasol * chore(exasol): refactored implementation to handle SELECT * queries with multiple sources * chore(exasol): fixed linting * chore(exasol): refactored implementation to qualify bare star * chore(exasol): changed method name and added more test * chore(exasol): fixed the test cases using validate_all instead of validate_identity * chore(exasol): refactored implementation reducing the walk through select_expressions, mutating the original star and fixing normalization issues
1 parent 8b5298a commit 2c013a5

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

sqlglot/dialects/exasol.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from sqlglot.generator import unsupported_args
2121
from sqlglot.helper import seq_get
2222
from sqlglot.tokens import TokenType
23+
from sqlglot.optimizer.scope import build_scope
2324

2425
if t.TYPE_CHECKING:
2526
from sqlglot.dialects.dialect import DialectType
@@ -183,6 +184,66 @@ def _substring_index_sql(self: Exasol.Generator, expression: exp.SubstringIndex)
183184
return self.func("SUBSTR", haystack_sql, direction, length)
184185

185186

187+
# 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.
188+
def _qualify_unscoped_star(expression: exp.Expression) -> exp.Expression:
189+
"""
190+
Exasol doesn't support a bare * alongside other select items, so we rewrite it
191+
Rewrite: SELECT *, <other> FROM <Table>
192+
Into: SELECT T.*, <other> FROM <Table> AS T
193+
"""
194+
195+
if not isinstance(expression, exp.Select):
196+
return expression
197+
198+
select_expressions = expression.expressions or []
199+
200+
def is_bare_star(expr: exp.Expression) -> bool:
201+
return isinstance(expr, exp.Star) and expr.this is None
202+
203+
has_other_expression = False
204+
bare_star_expr: exp.Expression | None = None
205+
for expr in select_expressions:
206+
has_bare_star = is_bare_star(expr)
207+
if has_bare_star and bare_star_expr is None:
208+
bare_star_expr = expr
209+
elif not has_bare_star:
210+
has_other_expression = True
211+
if bare_star_expr and has_other_expression:
212+
break
213+
214+
if not (bare_star_expr and has_other_expression):
215+
return expression
216+
217+
scope = build_scope(expression)
218+
219+
if not scope or not scope.selected_sources:
220+
return expression
221+
222+
table_identifiers: list[exp.Identifier] = []
223+
224+
for source_name, (source_expr, _) in scope.selected_sources.items():
225+
ident = (
226+
source_expr.this.copy()
227+
if isinstance(source_expr, exp.Table) and isinstance(source_expr.this, exp.Identifier)
228+
else exp.to_identifier(source_name)
229+
)
230+
table_identifiers.append(ident)
231+
232+
qualified_star_columns = [
233+
exp.Column(this=bare_star_expr.copy(), table=ident) for ident in table_identifiers
234+
]
235+
236+
new_select_expressions: list[exp.Expression] = []
237+
238+
for select_expr in select_expressions:
239+
new_select_expressions.extend(qualified_star_columns) if is_bare_star(
240+
select_expr
241+
) else new_select_expressions.append(select_expr)
242+
243+
expression.set("expressions", new_select_expressions)
244+
return expression
245+
246+
186247
def _add_date_sql(self: Exasol.Generator, expression: DATE_ADD_OR_SUB) -> str:
187248
interval = expression.expression if isinstance(expression.expression, exp.Interval) else None
188249

@@ -467,6 +528,7 @@ def datatype_sql(self, expression: exp.DataType) -> str:
467528
exp.CommentColumnConstraint: lambda self, e: f"COMMENT IS {self.sql(e, 'this')}",
468529
exp.Select: transforms.preprocess(
469530
[
531+
_qualify_unscoped_star,
470532
_add_local_prefix_for_aliases,
471533
]
472534
),

tests/dialects/test_exasol.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,44 @@ def test_exasol(self):
1111
'SELECT 1 AS "x"',
1212
)
1313

14+
def test_qualify_unscoped_star(self):
15+
self.validate_all(
16+
"SELECT TEST.*, 1 FROM TEST",
17+
read={
18+
"": "SELECT *, 1 FROM TEST",
19+
},
20+
)
21+
self.validate_identity(
22+
"SELECT t.*, 1 FROM t",
23+
)
24+
self.validate_identity(
25+
"SELECT t.* FROM t",
26+
)
27+
self.validate_identity(
28+
"SELECT * FROM t",
29+
)
30+
self.validate_identity(
31+
"WITH t AS (SELECT 1 AS x) SELECT t.*, 3 FROM t",
32+
)
33+
self.validate_all(
34+
"WITH t1 AS (SELECT 1 AS c1), t2 AS (SELECT 2 AS c2) SELECT t1.*, t2.*, 3 FROM t1, t2",
35+
read={
36+
"": "WITH t1 AS (SELECT 1 AS c1), t2 AS (SELECT 2 AS c2) SELECT *, 3 FROM t1, t2",
37+
},
38+
)
39+
self.validate_all(
40+
'SELECT "A".*, "B".*, 3 FROM "A" JOIN "B" ON 1 = 1',
41+
read={
42+
"": 'SELECT *, 3 FROM "A" JOIN "B" ON 1=1',
43+
},
44+
)
45+
self.validate_all(
46+
"SELECT s.*, q.*, 7 FROM (SELECT 1 AS x) AS s CROSS JOIN (SELECT 2 AS y) AS q",
47+
read={
48+
"": "SELECT *, 7 FROM (SELECT 1 AS x) s CROSS JOIN (SELECT 2 AS y) q",
49+
},
50+
)
51+
1452
def test_type_mappings(self):
1553
self.validate_identity("CAST(x AS BLOB)", "CAST(x AS VARCHAR)")
1654
self.validate_identity("CAST(x AS LONGBLOB)", "CAST(x AS VARCHAR)")

0 commit comments

Comments
 (0)