Skip to content

Commit d7b3a26

Browse files
authored
fix(postgres): transpile duckdb LIST_HAS_ANY and LIST_CONTAINS (tobymao#5440)
* fix(postgres): transpile duckdb LIST_HAS_ANY and LIST_CONTAINS * refactor arraycontains_sql * more refactor
1 parent 9f887f1 commit d7b3a26

File tree

3 files changed

+50
-0
lines changed

3 files changed

+50
-0
lines changed

sqlglot/dialects/duckdb.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,9 @@ class Parser(parser.Parser):
416416
"JSON": exp.ParseJSON.from_arg_list,
417417
"JSON_EXTRACT_PATH": parser.build_extract_json_with_path(exp.JSONExtract),
418418
"JSON_EXTRACT_STRING": parser.build_extract_json_with_path(exp.JSONExtractScalar),
419+
"LIST_CONTAINS": exp.ArrayContains.from_arg_list,
419420
"LIST_HAS": exp.ArrayContains.from_arg_list,
421+
"LIST_HAS_ANY": exp.ArrayOverlaps.from_arg_list,
420422
"LIST_REVERSE_SORT": _build_sort_array_desc,
421423
"LIST_SORT": exp.SortArray.from_arg_list,
422424
"LIST_VALUE": lambda args: exp.Array(expressions=args),

sqlglot/dialects/postgres.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,3 +818,29 @@ def interval_sql(self, expression: exp.Interval) -> str:
818818
def placeholder_sql(self, expression: exp.Placeholder) -> str:
819819
this = f"({expression.name})" if expression.this else ""
820820
return f"{self.NAMED_PLACEHOLDER_TOKEN}{this}s"
821+
822+
def arraycontains_sql(self, expression: exp.ArrayContains) -> str:
823+
# Convert DuckDB's LIST_CONTAINS(array, value) to PostgreSQL
824+
# DuckDB behavior:
825+
# - LIST_CONTAINS([1,2,3], 2) -> true
826+
# - LIST_CONTAINS([1,2,3], 4) -> false
827+
# - LIST_CONTAINS([1,2,NULL], 4) -> false (not NULL)
828+
# - LIST_CONTAINS([1,2,3], NULL) -> NULL
829+
#
830+
# PostgreSQL equivalent: CASE WHEN value IS NULL THEN NULL
831+
# ELSE COALESCE(value = ANY(array), FALSE) END
832+
value = expression.expression
833+
array = expression.this
834+
835+
coalesce_expr = exp.Coalesce(
836+
this=value.eq(exp.Any(this=exp.paren(expression=array, copy=False))),
837+
expressions=[exp.false()],
838+
)
839+
840+
case_expr = (
841+
exp.Case()
842+
.when(exp.Is(this=value, expression=exp.null()), exp.null(), copy=False)
843+
.else_(coalesce_expr, copy=False)
844+
)
845+
846+
return self.sql(case_expr)

tests/dialects/test_duckdb.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,28 @@ def test_duckdb(self):
978978
self.validate_identity("SELECT #2, #1 FROM (VALUES (1, 'foo'))")
979979
self.validate_identity("SELECT #2 AS a, #1 AS b FROM (VALUES (1, 'foo'))")
980980

981+
self.validate_all(
982+
"LIST_CONTAINS([1, 2, NULL], 1)",
983+
write={
984+
"duckdb": "ARRAY_CONTAINS([1, 2, NULL], 1)",
985+
"postgres": "CASE WHEN 1 IS NULL THEN NULL ELSE COALESCE(1 = ANY(ARRAY[1, 2, NULL]), FALSE) END",
986+
},
987+
)
988+
self.validate_all(
989+
"LIST_CONTAINS([1, 2, NULL], NULL)",
990+
write={
991+
"duckdb": "ARRAY_CONTAINS([1, 2, NULL], NULL)",
992+
"postgres": "CASE WHEN NULL IS NULL THEN NULL ELSE COALESCE(NULL = ANY(ARRAY[1, 2, NULL]), FALSE) END",
993+
},
994+
)
995+
self.validate_all(
996+
"LIST_HAS_ANY([1, 2, 3], [1,2])",
997+
write={
998+
"duckdb": "[1, 2, 3] && [1, 2]",
999+
"postgres": "ARRAY[1, 2, 3] && ARRAY[1, 2]",
1000+
},
1001+
)
1002+
9811003
def test_array_index(self):
9821004
with self.assertLogs(helper_logger) as cm:
9831005
self.validate_all(

0 commit comments

Comments
 (0)