From 1b4ab14096010c9987f333cf0d7aec6b332d4a21 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Tue, 23 Jun 2026 10:17:39 +0200 Subject: [PATCH 1/3] fix(exasol): treat LISTAGG second argument as the separator Exasol routed both GROUP_CONCAT and LISTAGG through the MySQL-style _parse_group_concat, so LISTAGG(expr, sep) folded its second argument into the value via CONCAT instead of treating it as the separator. This produced a semantically wrong AST (separator duplicated into the value, non-idempotent on re-parse) that disagreed with every sibling dialect. Route LISTAGG through _parse_string_agg, matching Oracle, Snowflake, Trino and DuckDB. GROUP_CONCAT keeps _parse_group_concat since Exasol's GROUP_CONCAT genuinely uses the MySQL SEPARATOR keyword. --- sqlglot/parsers/exasol.py | 5 +++-- tests/dialects/test_exasol.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/sqlglot/parsers/exasol.py b/sqlglot/parsers/exasol.py index 0d21560352..9dd504e561 100644 --- a/sqlglot/parsers/exasol.py +++ b/sqlglot/parsers/exasol.py @@ -118,9 +118,10 @@ class ExasolParser(parser.Parser): FUNCTION_PARSERS = { **parser.Parser.FUNCTION_PARSERS, - # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/listagg.htm # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/group_concat.htm - **dict.fromkeys(("GROUP_CONCAT", "LISTAGG"), lambda self: self._parse_group_concat()), + "GROUP_CONCAT": lambda self: self._parse_group_concat(), + # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/listagg.htm + "LISTAGG": lambda self: self._parse_string_agg(), # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/json_value.htm "JSON_VALUE": lambda self: self._parse_json_value(), # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/json_extract.htm diff --git a/tests/dialects/test_exasol.py b/tests/dialects/test_exasol.py index 1404e1a3ca..f1b1353371 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -334,6 +334,17 @@ def test_stringFunctions(self): "databricks": "LISTAGG(DISTINCT x, ',') WITHIN GROUP (ORDER BY y DESC)", }, ) + # The second LISTAGG argument is the separator, not part of the value + # (matching Oracle/Snowflake/Trino/DuckDB), not a GROUP_CONCAT-style concat. + self.validate_identity("SELECT LISTAGG(x, ',') WITHIN GROUP (ORDER BY y) FROM t") + self.validate_all( + "SELECT LISTAGG(x, ',') WITHIN GROUP (ORDER BY y) FROM t", + read={ + "exasol": "SELECT LISTAGG(x, ',') WITHIN GROUP (ORDER BY y) FROM t", + "oracle": "SELECT LISTAGG(x, ',') WITHIN GROUP (ORDER BY y) FROM t", + "snowflake": "SELECT LISTAGG(x, ',') WITHIN GROUP (ORDER BY y) FROM t", + }, + ) self.validate_all( "EDIT_DISTANCE(col1, col2)", read={ From ac083c35b75af879c1fa5befe6a4b27073c5d0cc Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Tue, 23 Jun 2026 21:46:15 +0200 Subject: [PATCH 2/3] fix(exasol): preserve LISTAGG ON OVERFLOW clause on generation Routing LISTAGG through STRING_AGG parses the ON OVERFLOW clause, but the Exasol GroupConcat generator dropped it. Pass on_overflow=True so the clause round-trips, and add validate_identity coverage for ERROR / TRUNCATE ... WITH COUNT / TRUNCATE ... WITHOUT COUNT. --- sqlglot/generators/exasol.py | 2 +- tests/dialects/test_exasol.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/sqlglot/generators/exasol.py b/sqlglot/generators/exasol.py index 5806f62934..640f8a570c 100644 --- a/sqlglot/generators/exasol.py +++ b/sqlglot/generators/exasol.py @@ -349,7 +349,7 @@ def datatype_sql(self, expression: exp.DataType) -> str: exp.DayOfWeek: lambda self, e: f"CAST(TO_CHAR({self.sql(e, 'this')}, 'D') AS INTEGER)", exp.DatetimeTrunc: timestamptrunc_sql(), exp.GroupConcat: lambda self, e: groupconcat_sql( - self, e, func_name="LISTAGG", within_group=True + self, e, func_name="LISTAGG", within_group=True, on_overflow=True ), # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/edit_distance.htm#EDIT_DISTANCE exp.Levenshtein: unsupported_args("ins_cost", "del_cost", "sub_cost", "max_dist")( diff --git a/tests/dialects/test_exasol.py b/tests/dialects/test_exasol.py index f1b1353371..42188f59ab 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -345,6 +345,16 @@ def test_stringFunctions(self): "snowflake": "SELECT LISTAGG(x, ',') WITHIN GROUP (ORDER BY y) FROM t", }, ) + # Routing LISTAGG through STRING_AGG also preserves the ON OVERFLOW clause. + self.validate_identity( + "LISTAGG(x, ',' ON OVERFLOW ERROR) WITHIN GROUP (ORDER BY y)" + ) + self.validate_identity( + "LISTAGG(x, ',' ON OVERFLOW TRUNCATE '...' WITH COUNT) WITHIN GROUP (ORDER BY y)" + ) + self.validate_identity( + "LISTAGG(x, ',' ON OVERFLOW TRUNCATE '...' WITHOUT COUNT) WITHIN GROUP (ORDER BY y)" + ) self.validate_all( "EDIT_DISTANCE(col1, col2)", read={ From 8ed5fd4a1265a84925e4cb01442c2c12f910889c Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Tue, 23 Jun 2026 22:14:17 +0200 Subject: [PATCH 3/3] style: ruff-format LISTAGG ON OVERFLOW test --- tests/dialects/test_exasol.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/dialects/test_exasol.py b/tests/dialects/test_exasol.py index 42188f59ab..1afc474639 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -346,9 +346,7 @@ def test_stringFunctions(self): }, ) # Routing LISTAGG through STRING_AGG also preserves the ON OVERFLOW clause. - self.validate_identity( - "LISTAGG(x, ',' ON OVERFLOW ERROR) WITHIN GROUP (ORDER BY y)" - ) + self.validate_identity("LISTAGG(x, ',' ON OVERFLOW ERROR) WITHIN GROUP (ORDER BY y)") self.validate_identity( "LISTAGG(x, ',' ON OVERFLOW TRUNCATE '...' WITH COUNT) WITHIN GROUP (ORDER BY y)" )