From 6ab6c046e2b28ff4bda00df693d3926e8e5b9131 Mon Sep 17 00:00:00 2001 From: fivetran-amrutabhimsenayachit Date: Thu, 13 Nov 2025 14:27:04 -0500 Subject: [PATCH 1/2] feat(duckDB): Add transpilation support for ANY_VALUE function with HAVING MAX and MIN clauses --- sqlglot/dialects/duckdb.py | 10 ++++++++++ tests/dialects/test_bigquery.py | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/sqlglot/dialects/duckdb.py b/sqlglot/dialects/duckdb.py index c687af2ded..9cd62832f6 100644 --- a/sqlglot/dialects/duckdb.py +++ b/sqlglot/dialects/duckdb.py @@ -281,6 +281,15 @@ def _cast_to_blob(self: DuckDB.Generator, expression: exp.Expression, result_sql return result_sql +def _anyvalue_sql(self: DuckDB.Generator, expression: exp.AnyValue) -> str: + # Transform ANY_VALUE(expr HAVING MAX/MIN having_expr) to ARG_MAX/ARG_MIN + having = expression.this + if isinstance(having, exp.HavingMax): + func_name = "ARG_MAX" if having.args.get("max") else "ARG_MIN" + return self.func(func_name, having.this, having.expression) + return self.function_fallback_sql(expression) + + class DuckDB(Dialect): NULL_ORDERING = "nulls_are_last" SUPPORTS_USER_DEFINED_TYPES = True @@ -699,6 +708,7 @@ class Generator(generator.Generator): TRANSFORMS = { **generator.Generator.TRANSFORMS, + exp.AnyValue: _anyvalue_sql, exp.ApproxDistinct: approx_count_distinct_sql, exp.Array: transforms.preprocess( [transforms.inherit_struct_field_names], diff --git a/tests/dialects/test_bigquery.py b/tests/dialects/test_bigquery.py index 8571fbd132..8e92f06605 100644 --- a/tests/dialects/test_bigquery.py +++ b/tests/dialects/test_bigquery.py @@ -111,6 +111,27 @@ def test_bigquery(self): self.validate_identity("SELECT PARSE_TIMESTAMP('%c', 'Thu Dec 25 07:30:00 2008', 'UTC')") self.validate_identity("SELECT ANY_VALUE(fruit HAVING MAX sold) FROM fruits") self.validate_identity("SELECT ANY_VALUE(fruit HAVING MIN sold) FROM fruits") + self.validate_all( + "SELECT ANY_VALUE(fruit HAVING MAX sold) FROM Store", + write={ + "bigquery": "SELECT ANY_VALUE(fruit HAVING MAX sold) FROM Store", + "duckdb": "SELECT ARG_MAX(fruit, sold) FROM Store", + }, + ) + self.validate_all( + "SELECT ANY_VALUE(fruit HAVING MIN sold) FROM Store", + write={ + "bigquery": "SELECT ANY_VALUE(fruit HAVING MIN sold) FROM Store", + "duckdb": "SELECT ARG_MIN(fruit, sold) FROM Store", + }, + ) + self.validate_all( + "SELECT category, ANY_VALUE(product HAVING MAX price), ANY_VALUE(product HAVING MIN cost), ANY_VALUE(supplier) FROM products GROUP BY category", + write={ + "bigquery": "SELECT category, ANY_VALUE(product HAVING MAX price), ANY_VALUE(product HAVING MIN cost), ANY_VALUE(supplier) FROM products GROUP BY category", + "duckdb": "SELECT category, ARG_MAX(product, price), ARG_MIN(product, cost), ANY_VALUE(supplier) FROM products GROUP BY category", + }, + ) self.validate_identity("SELECT `project-id`.udfs.func(call.dir)") self.validate_identity("SELECT CAST(CURRENT_DATE AS STRING FORMAT 'DAY') AS current_day") self.validate_identity("SAFE_CAST(encrypted_value AS STRING FORMAT 'BASE64')") From 4a1350be64a0d9c989259148cae94c44edc4628b Mon Sep 17 00:00:00 2001 From: fivetran-amrutabhimsenayachit Date: Fri, 14 Nov 2025 12:15:30 -0500 Subject: [PATCH 2/2] feat(duckdb): Handle null case with ANY_VALUE transpilation --- sqlglot/dialects/duckdb.py | 4 ++-- tests/dialects/test_bigquery.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/sqlglot/dialects/duckdb.py b/sqlglot/dialects/duckdb.py index 9cd62832f6..6a5d4fce37 100644 --- a/sqlglot/dialects/duckdb.py +++ b/sqlglot/dialects/duckdb.py @@ -282,10 +282,10 @@ def _cast_to_blob(self: DuckDB.Generator, expression: exp.Expression, result_sql def _anyvalue_sql(self: DuckDB.Generator, expression: exp.AnyValue) -> str: - # Transform ANY_VALUE(expr HAVING MAX/MIN having_expr) to ARG_MAX/ARG_MIN + # Transform ANY_VALUE(expr HAVING MAX/MIN having_expr) to ARG_MAX_NULL/ARG_MIN_NULL having = expression.this if isinstance(having, exp.HavingMax): - func_name = "ARG_MAX" if having.args.get("max") else "ARG_MIN" + func_name = "ARG_MAX_NULL" if having.args.get("max") else "ARG_MIN_NULL" return self.func(func_name, having.this, having.expression) return self.function_fallback_sql(expression) diff --git a/tests/dialects/test_bigquery.py b/tests/dialects/test_bigquery.py index 8e92f06605..fb6bdc673a 100644 --- a/tests/dialects/test_bigquery.py +++ b/tests/dialects/test_bigquery.py @@ -115,21 +115,27 @@ def test_bigquery(self): "SELECT ANY_VALUE(fruit HAVING MAX sold) FROM Store", write={ "bigquery": "SELECT ANY_VALUE(fruit HAVING MAX sold) FROM Store", - "duckdb": "SELECT ARG_MAX(fruit, sold) FROM Store", + "duckdb": "SELECT ARG_MAX_NULL(fruit, sold) FROM Store", }, ) self.validate_all( "SELECT ANY_VALUE(fruit HAVING MIN sold) FROM Store", write={ "bigquery": "SELECT ANY_VALUE(fruit HAVING MIN sold) FROM Store", - "duckdb": "SELECT ARG_MIN(fruit, sold) FROM Store", + "duckdb": "SELECT ARG_MIN_NULL(fruit, sold) FROM Store", }, ) self.validate_all( "SELECT category, ANY_VALUE(product HAVING MAX price), ANY_VALUE(product HAVING MIN cost), ANY_VALUE(supplier) FROM products GROUP BY category", write={ "bigquery": "SELECT category, ANY_VALUE(product HAVING MAX price), ANY_VALUE(product HAVING MIN cost), ANY_VALUE(supplier) FROM products GROUP BY category", - "duckdb": "SELECT category, ARG_MAX(product, price), ARG_MIN(product, cost), ANY_VALUE(supplier) FROM products GROUP BY category", + "duckdb": "SELECT category, ARG_MAX_NULL(product, price), ARG_MIN_NULL(product, cost), ANY_VALUE(supplier) FROM products GROUP BY category", + }, + ) + self.validate_all( + 'WITH data AS (SELECT "A" AS fruit, 20 AS sold UNION ALL SELECT NULL AS fruit, 25 AS sold) SELECT ANY_VALUE(fruit HAVING MAX sold) FROM data', + write={ + "duckdb": "WITH data AS (SELECT 'A' AS fruit, 20 AS sold UNION ALL SELECT NULL AS fruit, 25 AS sold) SELECT ARG_MAX_NULL(fruit, sold) FROM data", }, ) self.validate_identity("SELECT `project-id`.udfs.func(call.dir)")