From b462eea6c42328bbeba0cb11dedea30ded7bdf99 Mon Sep 17 00:00:00 2001 From: Nnamdi Nwabuokei Date: Fri, 28 Nov 2025 14:50:08 +0100 Subject: [PATCH 1/7] chore(exasol): implemented a select preprocessor to qualify bare '*' and add alias in exasol --- sqlglot/dialects/exasol.py | 53 ++++++++++++++++++++++++++++++++++- tests/dialects/test_exasol.py | 4 +++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/sqlglot/dialects/exasol.py b/sqlglot/dialects/exasol.py index 7848fa5662..1162f92afb 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,55 @@ def _substring_index_sql(self: Exasol.Generator, expression: exp.SubstringIndex) return self.func("SUBSTR", haystack_sql, direction, length) +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 + items = list(node.expressions or []) + + has_bare_star = any(isinstance(x, exp.Star) and x.this is None for x in items) + + if not has_bare_star or len(items) <= 1: + return node + + from_ = node.args.get("from_") + + if not from_ or not isinstance(from_.this, exp.Table): + return node + + table = from_.this + alias_node = table.args.get("alias") + scope = build_scope(node) + if not scope: + return node + + if alias_node and alias_node.name: + alias_name = alias_node.name + + else: + taken_source_name = set(scope.sources) + alias_name = find_new_name(taken_source_name, "T") + table.set("alias", exp.TableAlias(this=exp.to_identifier(alias_name, quoted=False))) + + qualified_items: list[exp.Expression] = [] + + for it in items: + if isinstance(it, exp.Star) and it.this is None: + qualified_items.append( + exp.Column(this=exp.Star(), table=exp.to_identifier(alias_name, quoted=False)) + ) + else: + qualified_items.append(it) + node.set("expressions", qualified_items) + + return node + + DATE_UNITS = {"DAY", "WEEK", "MONTH", "YEAR", "HOUR", "MINUTE", "SECOND"} @@ -425,6 +475,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..5be672fb83 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -10,6 +10,10 @@ def test_exasol(self): "SELECT 1 AS [x]", 'SELECT 1 AS "x"', ) + self.validate_identity( + "SELECT *, 1 FROM TEST", + "SELECT T.*, 1 FROM TEST AS T", + ) def test_type_mappings(self): self.validate_identity("CAST(x AS BLOB)", "CAST(x AS VARCHAR)") From a334cdeadf5c41d2169036b9b232c994245aca41 Mon Sep 17 00:00:00 2001 From: Nnamdi Nwabuokei Date: Tue, 2 Dec 2025 10:20:42 +0100 Subject: [PATCH 2/7] chore(exasol): refactored implementation to handle SELECT * queries with multiple sources --- sqlglot/dialects/exasol.py | 63 +++++++++++++++++++++-------------- tests/dialects/test_exasol.py | 14 ++++++++ 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/sqlglot/dialects/exasol.py b/sqlglot/dialects/exasol.py index 1162f92afb..bc8f727327 100644 --- a/sqlglot/dialects/exasol.py +++ b/sqlglot/dialects/exasol.py @@ -177,43 +177,56 @@ def _qualify_unscoped_star(node: exp.Expression) -> exp.Expression: if not isinstance(node, exp.Select): return node - items = list(node.expressions or []) + select_expressions = list(node.expressions or []) - has_bare_star = any(isinstance(x, exp.Star) and x.this is None for x in items) + 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(items) <= 1: + if not has_bare_star or len(select_expressions) <= 1: return node - from_ = node.args.get("from_") + from_clause = node.args.get("from_") - if not from_ or not isinstance(from_.this, exp.Table): - return node + base_source = from_clause.this if from_clause else None - table = from_.this - alias_node = table.args.get("alias") - scope = build_scope(node) - if not scope: + if not base_source: return node - if alias_node and alias_node.name: - alias_name = alias_node.name + 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 - else: - taken_source_name = set(scope.sources) - alias_name = find_new_name(taken_source_name, "T") - table.set("alias", exp.TableAlias(this=exp.to_identifier(alias_name, quoted=False))) + scope = build_scope(node) + used_alias_names = set(scope.sources.keys()) if scope else set() - qualified_items: list[exp.Expression] = [] + qualifiers: list[exp.Identifier] = [] - for it in items: - if isinstance(it, exp.Star) and it.this is None: - qualified_items.append( - exp.Column(this=exp.Star(), table=exp.to_identifier(alias_name, quoted=False)) - ) + for src in table_sources: + alias = src.args.get("alias") + if isinstance(alias, (exp.TableAlias, exp.Alias)) and alias.name: + name = alias.name else: - qualified_items.append(it) - node.set("expressions", qualified_items) - + 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 diff --git a/tests/dialects/test_exasol.py b/tests/dialects/test_exasol.py index 5be672fb83..713156254a 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -10,10 +10,24 @@ def test_exasol(self): "SELECT 1 AS [x]", '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)") From 4cac7e071095d2d370dba1edafb96162adf96922 Mon Sep 17 00:00:00 2001 From: Nnamdi Nwabuokei Date: Tue, 2 Dec 2025 10:31:51 +0100 Subject: [PATCH 3/7] chore(exasol): fixed linting --- sqlglot/dialects/exasol.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sqlglot/dialects/exasol.py b/sqlglot/dialects/exasol.py index bc8f727327..3b425ef0d6 100644 --- a/sqlglot/dialects/exasol.py +++ b/sqlglot/dialects/exasol.py @@ -168,6 +168,7 @@ 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 @@ -179,7 +180,9 @@ def _qualify_unscoped_star(node: exp.Expression) -> exp.Expression: 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) + 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 db2e6b10efbaa96ce0be1e3a97a10dd833d0863a Mon Sep 17 00:00:00 2001 From: Nnamdi Nwabuokei Date: Fri, 5 Dec 2025 11:29:01 +0100 Subject: [PATCH 4/7] chore(exasol): refactored implementation to qualify bare star --- sqlglot/dialects/exasol.py | 69 +++++++++++++---------------------- tests/dialects/test_exasol.py | 15 ++++++-- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/sqlglot/dialects/exasol.py b/sqlglot/dialects/exasol.py index fb8f4255db..e7c58436ac 100644 --- a/sqlglot/dialects/exasol.py +++ b/sqlglot/dialects/exasol.py @@ -18,7 +18,7 @@ DATE_ADD_OR_SUB, ) from sqlglot.generator import unsupported_args -from sqlglot.helper import seq_get, find_new_name +from sqlglot.helper import seq_get from sqlglot.tokens import TokenType from sqlglot.optimizer.scope import build_scope @@ -171,68 +171,49 @@ def _substring_index_sql(self: Exasol.Generator, expression: exp.SubstringIndex) # 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: +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 *, FROM
Into: SELECT T.*, FROM
AS T """ - if not isinstance(node, exp.Select): - return node - select_expressions = list(node.expressions or []) + if not isinstance(expression, exp.Select): + return expression - has_bare_star = any( - isinstance(expr, exp.Star) and expr.this is None for expr in select_expressions - ) + select_expressions = expression.expressions or [] - if not has_bare_star or len(select_expressions) <= 1: - return node + def is_qualified_star(expr: exp.Expression) -> bool: + return isinstance(expr, exp.Star) and expr.this is None - from_clause = node.args.get("from_") + has_unqualified_star = any(is_qualified_star(select) for select in select_expressions) - base_source = from_clause.this if from_clause else None + has_other_expression = any(not (is_qualified_star(select)) for select in select_expressions) - if not base_source: - return node + if not (has_unqualified_star and has_other_expression): + return expression - table_sources: list[exp.Expression] = [base_source] + scope = build_scope(expression) - table_sources.extend( - join.this - for join in (node.args.get("joins") or []) - if isinstance(join, exp.Join) and join.this - ) + if not scope or not scope.selected_sources: + return expression - if not table_sources: - return node + qualified_star_columns = [ + exp.Column(this=exp.Star(), table=exp.to_identifier(source_name)) + for source_name in scope.selected_sources.keys() + ] - scope = build_scope(node) - used_alias_names = set(scope.sources.keys()) if scope else set() + new_select_expressions: list[exp.Expression] = [] - qualifiers: list[exp.Identifier] = [] + for select_expr in select_expressions: + new_select_expressions.extend(qualified_star_columns) if is_qualified_star( + select_expr + ) else new_select_expressions.append(select_expr) - 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)) + expression.set("expressions", new_select_expressions) + return expression - 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 def _add_date_sql(self: Exasol.Generator, expression: DATE_ADD_OR_SUB) -> str: interval = expression.expression if isinstance(expression.expression, exp.Interval) else None diff --git a/tests/dialects/test_exasol.py b/tests/dialects/test_exasol.py index a17137edd9..fcb6f91854 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -14,15 +14,24 @@ def test_exasol(self): def test_qualify_unscoped_star(self): self.validate_identity( "SELECT *, 1 FROM TEST", - "SELECT T.*, 1 FROM TEST AS T", + "SELECT TEST.*, 1 FROM TEST", + ) + self.validate_identity( + "SELECT t.*, 1 FROM t", + ) + self.validate_identity( + "SELECT t.* 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 T.*, T_2.*, 3 FROM t1 AS T, t2 AS T_2", + "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 T.*, T_2.*, 3 FROM "A" AS T JOIN "B" AS T_2 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", From 3749666aea07df61fb00d9680bdde59c106efccf Mon Sep 17 00:00:00 2001 From: Nnamdi Nwabuokei Date: Fri, 5 Dec 2025 12:04:23 +0100 Subject: [PATCH 5/7] chore(exasol): changed method name and added more test --- sqlglot/dialects/exasol.py | 10 +++++----- tests/dialects/test_exasol.py | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sqlglot/dialects/exasol.py b/sqlglot/dialects/exasol.py index e7c58436ac..00dbbf4f47 100644 --- a/sqlglot/dialects/exasol.py +++ b/sqlglot/dialects/exasol.py @@ -183,14 +183,14 @@ def _qualify_unscoped_star(expression: exp.Expression) -> exp.Expression: select_expressions = expression.expressions or [] - def is_qualified_star(expr: exp.Expression) -> bool: + def is_bare_star(expr: exp.Expression) -> bool: return isinstance(expr, exp.Star) and expr.this is None - has_unqualified_star = any(is_qualified_star(select) for select in select_expressions) + has_bare_star = any(is_bare_star(select) for select in select_expressions) - has_other_expression = any(not (is_qualified_star(select)) for select in select_expressions) + has_other_expression = any(not (is_bare_star(select)) for select in select_expressions) - if not (has_unqualified_star and has_other_expression): + if not (has_bare_star and has_other_expression): return expression scope = build_scope(expression) @@ -206,7 +206,7 @@ def is_qualified_star(expr: exp.Expression) -> bool: new_select_expressions: list[exp.Expression] = [] for select_expr in select_expressions: - new_select_expressions.extend(qualified_star_columns) if is_qualified_star( + new_select_expressions.extend(qualified_star_columns) if is_bare_star( select_expr ) else new_select_expressions.append(select_expr) diff --git a/tests/dialects/test_exasol.py b/tests/dialects/test_exasol.py index fcb6f91854..9f23ffac1d 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -22,6 +22,9 @@ def test_qualify_unscoped_star(self): 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", ) From 53dd8dcffc7a2092ad628470a5afc0cdca46c469 Mon Sep 17 00:00:00 2001 From: Nnamdi Nwabuokei Date: Mon, 8 Dec 2025 11:57:46 +0100 Subject: [PATCH 6/7] chore(exasol): fixed the test cases using validate_all instead of validate_identity --- tests/dialects/test_exasol.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/dialects/test_exasol.py b/tests/dialects/test_exasol.py index 9f23ffac1d..45890b0fef 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -12,9 +12,11 @@ def test_exasol(self): ) def test_qualify_unscoped_star(self): - self.validate_identity( - "SELECT *, 1 FROM TEST", + self.validate_all( "SELECT TEST.*, 1 FROM TEST", + read={ + "": "SELECT *, 1 FROM TEST", + }, ) self.validate_identity( "SELECT t.*, 1 FROM t", @@ -28,17 +30,23 @@ def test_qualify_unscoped_star(self): 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", + self.validate_all( "WITH t1 AS (SELECT 1 AS c1), t2 AS (SELECT 2 AS c2) SELECT t1.*, t2.*, 3 FROM t1, t2", + read={ + "": "WITH t1 AS (SELECT 1 AS c1), t2 AS (SELECT 2 AS c2) SELECT *, 3 FROM t1, t2", + }, ) - self.validate_identity( - 'SELECT *, 3 FROM "A" JOIN "B" ON 1=1', + self.validate_all( 'SELECT A.*, B.*, 3 FROM "A" JOIN "B" ON 1 = 1', + read={ + "": 'SELECT *, 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", + self.validate_all( "SELECT s.*, q.*, 7 FROM (SELECT 1 AS x) AS s CROSS JOIN (SELECT 2 AS y) AS q", + read={ + "": "SELECT *, 7 FROM (SELECT 1 AS x) s CROSS JOIN (SELECT 2 AS y) q", + }, ) def test_type_mappings(self): From 9b3433aa107a51b74671054b5d7a808cb3fdb018 Mon Sep 17 00:00:00 2001 From: Nnamdi Nwabuokei Date: Tue, 9 Dec 2025 00:04:32 +0100 Subject: [PATCH 7/7] chore(exasol): refactored implementation reducing the walk through select_expressions, mutating the original star and fixing normalization issues --- sqlglot/dialects/exasol.py | 30 +++++++++++++++++++++++------- tests/dialects/test_exasol.py | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/sqlglot/dialects/exasol.py b/sqlglot/dialects/exasol.py index 00dbbf4f47..de8b7741f7 100644 --- a/sqlglot/dialects/exasol.py +++ b/sqlglot/dialects/exasol.py @@ -186,11 +186,18 @@ def _qualify_unscoped_star(expression: exp.Expression) -> exp.Expression: 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): + has_other_expression = False + bare_star_expr: exp.Expression | None = None + for expr in select_expressions: + has_bare_star = is_bare_star(expr) + if has_bare_star and bare_star_expr is None: + bare_star_expr = expr + elif not has_bare_star: + has_other_expression = True + if bare_star_expr and has_other_expression: + break + + if not (bare_star_expr and has_other_expression): return expression scope = build_scope(expression) @@ -198,9 +205,18 @@ def is_bare_star(expr: exp.Expression) -> bool: if not scope or not scope.selected_sources: return expression + table_identifiers: list[exp.Identifier] = [] + + for source_name, (source_expr, _) in scope.selected_sources.items(): + ident = ( + source_expr.this.copy() + if isinstance(source_expr, exp.Table) and isinstance(source_expr.this, exp.Identifier) + else exp.to_identifier(source_name) + ) + table_identifiers.append(ident) + qualified_star_columns = [ - exp.Column(this=exp.Star(), table=exp.to_identifier(source_name)) - for source_name in scope.selected_sources.keys() + exp.Column(this=bare_star_expr.copy(), table=ident) for ident in table_identifiers ] new_select_expressions: list[exp.Expression] = [] diff --git a/tests/dialects/test_exasol.py b/tests/dialects/test_exasol.py index 45890b0fef..5469d41bf5 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -37,7 +37,7 @@ def test_qualify_unscoped_star(self): }, ) self.validate_all( - 'SELECT A.*, B.*, 3 FROM "A" JOIN "B" ON 1 = 1', + 'SELECT "A".*, "B".*, 3 FROM "A" JOIN "B" ON 1 = 1', read={ "": 'SELECT *, 3 FROM "A" JOIN "B" ON 1=1', },