@@ -701,8 +701,8 @@ class Dialect(metaclass=_Dialect):
701701 ARRAY_AGG_INCLUDES_NULLS : t .Optional [bool ] = True
702702 """Whether ArrayAgg needs to filter NULL values."""
703703
704- ARRAY_APPEND_PROPAGATES_NULLS = False
705- """Whether ArrayAppend returns NULL when the input array is NULL."""
704+ ARRAY_FUNCS_PROPAGATES_NULLS = False
705+ """Whether Array update functions return NULL when the input array is NULL."""
706706
707707 PROMOTE_TO_INFERRED_DATETIME_TYPE = False
708708 """
@@ -1366,7 +1366,7 @@ def array_append_sql(
13661366
13671367 Returns:
13681368 A callable that generates SQL with appropriate NULL handling for the target dialect.
1369- Dialects that propagate NULLs need to set `ARRAY_APPEND_PROPAGATES_NULLS ` to True.
1369+ Dialects that propagate NULLs need to set `ARRAY_FUNCS_PROPAGATES_NULLS ` to True.
13701370 """
13711371
13721372 def _array_append_sql (self : Generator , expression : exp .ArrayAppend | exp .ArrayPrepend ) -> str :
@@ -1376,7 +1376,7 @@ def _array_append_sql(self: Generator, expression: exp.ArrayAppend | exp.ArrayPr
13761376 func_sql = self .func (name , * args )
13771377
13781378 source_null_propagation = bool (expression .args .get ("null_propagation" ))
1379- target_null_propagation = self .dialect .ARRAY_APPEND_PROPAGATES_NULLS
1379+ target_null_propagation = self .dialect .ARRAY_FUNCS_PROPAGATES_NULLS
13801380
13811381 # No transpilation needed when source and target have matching NULL semantics
13821382 if source_null_propagation == target_null_propagation :
@@ -1400,6 +1400,88 @@ def _array_append_sql(self: Generator, expression: exp.ArrayAppend | exp.ArrayPr
14001400 return _array_append_sql
14011401
14021402
1403+ def array_concat_sql (
1404+ name : str ,
1405+ ) -> t .Callable [[Generator , exp .ArrayConcat ], str ]:
1406+ """
1407+ Transpile ARRAY_CONCAT/ARRAY_CAT between dialects with different NULL propagation semantics.
1408+
1409+ Some dialects (Redshift, Snowflake, Spark) return NULL when ANY input array is NULL.
1410+ Others (DuckDB, PostgreSQL) skip NULL arrays and continue concatenation.
1411+
1412+ Args:
1413+ name: Target dialect's function name (e.g., "ARRAY_CAT", "ARRAY_CONCAT", "LIST_CONCAT")
1414+
1415+ Returns:
1416+ A callable that generates SQL with appropriate NULL handling for the target dialect.
1417+ Dialects that propagate NULLs need to set `ARRAY_FUNCS_PROPAGATES_NULLS` to True.
1418+ """
1419+
1420+ def _build_func_call (self : Generator , func_name : str , args : t .Sequence [exp .Expression ]) -> str :
1421+ """Build ARRAY_CONCAT call from a list of arguments, handling variadic vs binary nesting."""
1422+ if self .ARRAY_CONCAT_IS_VAR_LEN :
1423+ return self .func (func_name , * args )
1424+ elif len (args ) == 1 :
1425+ # Single arg gets empty array to preserve semantics
1426+ return self .func (func_name , args [0 ], exp .Array (expressions = []))
1427+ else :
1428+ # Snowflake/PostgreSQL/Redshift require binary nesting: ARRAY_CAT(a, ARRAY_CAT(b, c))
1429+ # Build right-deep tree recursively to avoid creating new ArrayConcat expressions
1430+ result = self .func (func_name , args [- 2 ], args [- 1 ])
1431+ for arg in reversed (args [:- 2 ]):
1432+ result = f"{ func_name } ({ self .sql (arg )} , { result } )"
1433+ return result
1434+
1435+ def _array_concat_sql (self : Generator , expression : exp .ArrayConcat ) -> str :
1436+ this = expression .this
1437+ exprs = expression .expressions
1438+ all_args = [this ] + exprs
1439+
1440+ source_null_propagation = bool (expression .args .get ("null_propagation" ))
1441+ target_null_propagation = self .dialect .ARRAY_FUNCS_PROPAGATES_NULLS
1442+
1443+ # Skip wrapper when source and target have matching NULL semantics,
1444+ # or when the first argument is an array literal (which can never be NULL),
1445+ # or when it's a single-argument call (empty array is added, preserving NULL semantics)
1446+ if (
1447+ source_null_propagation == target_null_propagation
1448+ or isinstance (this , exp .Array )
1449+ or len (exprs ) == 0
1450+ ):
1451+ return _build_func_call (self , name , all_args )
1452+
1453+ # Case 1: Source propagates NULLs, target doesn't (Snowflake → DuckDB)
1454+ # Check if ANY argument is NULL and return NULL explicitly
1455+ if source_null_propagation :
1456+ # Build OR-chain: a IS NULL OR b IS NULL OR c IS NULL
1457+ null_checks : t .List [exp .Expression ] = [
1458+ exp .Is (this = arg .copy (), expression = exp .Null ()) for arg in all_args
1459+ ]
1460+ combined_check : exp .Expression = reduce (
1461+ lambda a , b : exp .Or (this = a , expression = b ), null_checks
1462+ )
1463+
1464+ func_sql = _build_func_call (self , name , all_args )
1465+
1466+ return self .sql (
1467+ exp .If (
1468+ this = combined_check ,
1469+ true = exp .Null (),
1470+ false = func_sql ,
1471+ )
1472+ )
1473+
1474+ # Case 2: Source doesn't propagate NULLs, target does (DuckDB → Snowflake)
1475+ # Wrap ALL arguments in COALESCE to convert NULL → empty array
1476+ wrapped_args = [
1477+ exp .Coalesce (expressions = [arg .copy (), exp .Array (expressions = [])]) for arg in all_args
1478+ ]
1479+
1480+ return _build_func_call (self , name , wrapped_args )
1481+
1482+ return _array_concat_sql
1483+
1484+
14031485def var_map_sql (
14041486 self : Generator , expression : exp .Map | exp .VarMap , map_func_name : str = "MAP"
14051487) -> str :
0 commit comments