Repository: tobymao/sqlglot
Pull request: Fix!(presto): ensure ||, CONCAT args are strings
Fixes #1735

Problem statement:
Can not convert '||' from redshift to presto/trino when the arguments are not string typed
```
sql = """
select 'a' || 1 || 1.1
"""
converted_sql = sqlglot.transpile(sql, read="redshift", write="trino")[0]
print(converted_sql)
```
```
SELECT 'a' || 1 || 1.1

```

Trino would throw error `Unexpected parameters (varchar(1), integer) for function concat. Expected: concat(char(x), char(y)), concat(array(E), E) E, concat(E, array(E)) E, concat(array(E)) E, concat(varchar), concat(varbinary)'` when executing the output SQL, as Trino doesn't covert the type implicitly like Redshift does.

Hints from issue discussion:
~I don't think this can be done without type annotations, because you could also have `'a' || x`. So for the transpilation to be correct you need to know the type of `x`.~

E.g. the following is valid Redshift
```
WITH t AS (SELECT 1 AS x) SELECT 'a' || x FROM t;
```

I guess we'll just take a look and see what's possible; maybe we can always cast to a string type?

EDIT: discussed in slack about a workaround for this, will try to make a fix soon.
There are some other issues also caused by Redshift implicit type converting.. such as 
```

sql = """
select a from (select '1' as a) 
where a = 1

"""
```
and 

```
sql = """
select a from (select 1 as a) 
where a = '1'

"""
```
the converted SQL does not work in Trino due to `Cannot apply operator: varchar(1) = integer`
but I guess those are harder to fix 
Yep, I think this is out of scope for now, similar to https://github.com/tobymao/sqlglot/issues/1736#issuecomment-1579567236.
Btw, when I say "this" I mean your last comment -- currently working on a fix for the `||` issue.

Commit message: Simplify

Patch to review:
diff --git a/sqlglot/dialects/clickhouse.py b/sqlglot/dialects/clickhouse.py
index 1509a093b4..5daa68e61a 100644
--- a/sqlglot/dialects/clickhouse.py
+++ b/sqlglot/dialects/clickhouse.py
@@ -23,6 +23,7 @@ def _lower_func(sql: str) -> str:
 class ClickHouse(Dialect):
     NORMALIZE_FUNCTIONS: bool | str = False
     NULL_ORDERING = "nulls_are_last"
+    STRICT_STRING_CONCAT = True
 
     class Tokenizer(tokens.Tokenizer):
         COMMENTS = ["--", "#", "#!", ("/*", "*/")]
@@ -333,6 +334,16 @@ class Generator(generator.Generator):
             "NAMED COLLECTION",
         }
 
+        def safeconcat_sql(self, expression: exp.SafeConcat) -> str:
+            # Clickhouse errors out if we try to cast a NULL value to TEXT
+            return self.func(
+                "CONCAT",
+                *[
+                    exp.func("if", e.is_(exp.null()), e, exp.cast(e, "text"))
+                    for e in expression.expressions
+                ],
+            )
+
         def cte_sql(self, expression: exp.CTE) -> str:
             if isinstance(expression.this, exp.Alias):
                 return self.sql(expression, "this")
diff --git a/sqlglot/dialects/dialect.py b/sqlglot/dialects/dialect.py
index 036d20e8d1..405b71c391 100644
--- a/sqlglot/dialects/dialect.py
+++ b/sqlglot/dialects/dialect.py
@@ -122,6 +122,9 @@ def get_start_end(token_type: TokenType) -> t.Tuple[t.Optional[str], t.Optional[
                 if hasattr(subclass, name):
                     setattr(subclass, name, value)
 
+        if klass.STRICT_STRING_CONCAT:
+            klass.parser_class.BITWISE[TokenType.DPIPE] = exp.SafeDPipe
+
         return klass
 
 
@@ -138,6 +141,9 @@ class Dialect(metaclass=_Dialect):
     # Determines whether or not an unquoted identifier can start with a digit
     IDENTIFIERS_CAN_START_WITH_DIGIT = False
 
+    # Determines whether or not CONCAT's arguments must be strings
+    STRICT_STRING_CONCAT = False
+
     # Determines how function names are going to be normalized
     NORMALIZE_FUNCTIONS: bool | str = "upper"
 
@@ -535,6 +541,14 @@ def _ts_or_ds_to_date_sql(self: Generator, expression: exp.TsOrDsToDate) -> str:
     return _ts_or_ds_to_date_sql
 
 
+def concat_to_dpipe_sql(self: Generator, expression: exp.Concat | exp.SafeConcat) -> str:
+    this, *rest_args = expression.expressions
+    for arg in rest_args:
+        this = exp.DPipe(this=this, expression=arg)
+
+    return self.sql(this)
+
+
 # Spark, DuckDB use (almost) the same naming scheme for the output columns of the PIVOT operator
 def pivot_column_names(aggregations: t.List[exp.Expression], dialect: DialectType) -> t.List[str]:
     names = []
diff --git a/sqlglot/dialects/drill.py b/sqlglot/dialects/drill.py
index 01c3b18c65..3cca986b1c 100644
--- a/sqlglot/dialects/drill.py
+++ b/sqlglot/dialects/drill.py
@@ -82,6 +82,7 @@ class Tokenizer(tokens.Tokenizer):
 
     class Parser(parser.Parser):
         STRICT_CAST = False
+        CONCAT_NULL_OUTPUTS_STRING = True
 
         FUNCTIONS = {
             **parser.Parser.FUNCTIONS,
diff --git a/sqlglot/dialects/duckdb.py b/sqlglot/dialects/duckdb.py
index fe5ff808a9..9c8c51c269 100644
--- a/sqlglot/dialects/duckdb.py
+++ b/sqlglot/dialects/duckdb.py
@@ -114,6 +114,8 @@ class Tokenizer(tokens.Tokenizer):
         }
 
     class Parser(parser.Parser):
+        CONCAT_NULL_OUTPUTS_STRING = True
+
         FUNCTIONS = {
             **parser.Parser.FUNCTIONS,
             "ARRAY_LENGTH": exp.ArraySize.from_arg_list,
diff --git a/sqlglot/dialects/postgres.py b/sqlglot/dialects/postgres.py
index f69010a05e..67a84e10bf 100644
--- a/sqlglot/dialects/postgres.py
+++ b/sqlglot/dialects/postgres.py
@@ -258,6 +258,7 @@ class Tokenizer(tokens.Tokenizer):
 
     class Parser(parser.Parser):
         STRICT_CAST = False
+        CONCAT_NULL_OUTPUTS_STRING = True
 
         FUNCTIONS = {
             **parser.Parser.FUNCTIONS,
diff --git a/sqlglot/dialects/presto.py b/sqlglot/dialects/presto.py
index 345dbef7af..a8a9884989 100644
--- a/sqlglot/dialects/presto.py
+++ b/sqlglot/dialects/presto.py
@@ -179,6 +179,7 @@ class Presto(Dialect):
     NULL_ORDERING = "nulls_are_last"
     TIME_FORMAT = MySQL.TIME_FORMAT
     TIME_MAPPING = MySQL.TIME_MAPPING
+    STRICT_STRING_CONCAT = True
 
     class Tokenizer(tokens.Tokenizer):
         KEYWORDS = {
diff --git a/sqlglot/dialects/redshift.py b/sqlglot/dialects/redshift.py
index 68a64a1647..a7e25fae0d 100644
--- a/sqlglot/dialects/redshift.py
+++ b/sqlglot/dialects/redshift.py
@@ -3,7 +3,7 @@
 import typing as t
 
 from sqlglot import exp, transforms
-from sqlglot.dialects.dialect import rename_func
+from sqlglot.dialects.dialect import concat_to_dpipe_sql, rename_func
 from sqlglot.dialects.postgres import Postgres
 from sqlglot.helper import seq_get
 from sqlglot.tokens import TokenType
@@ -94,6 +94,7 @@ class Generator(Postgres.Generator):
 
         TRANSFORMS = {
             **Postgres.Generator.TRANSFORMS,
+            exp.Concat: concat_to_dpipe_sql,
             exp.CurrentTimestamp: lambda self, e: "SYSDATE",
             exp.DateAdd: lambda self, e: self.func(
                 "DATEADD", exp.var(e.text("unit") or "day"), e.expression, e.this
@@ -106,6 +107,7 @@ class Generator(Postgres.Generator):
             exp.FromBase: rename_func("STRTOL"),
             exp.JSONExtract: _json_sql,
             exp.JSONExtractScalar: _json_sql,
+            exp.SafeConcat: concat_to_dpipe_sql,
             exp.Select: transforms.preprocess([transforms.eliminate_distinct_on]),
             exp.SortKeyProperty: lambda self, e: f"{'COMPOUND ' if e.args['compound'] else ''}SORTKEY({self.format_args(*e.this)})",
             exp.TsOrDsToDate: lambda self, e: self.sql(e.this),
diff --git a/sqlglot/dialects/sqlite.py b/sqlglot/dialects/sqlite.py
index 8931b9746a..3b837ea3f8 100644
--- a/sqlglot/dialects/sqlite.py
+++ b/sqlglot/dialects/sqlite.py
@@ -5,6 +5,7 @@
     Dialect,
     arrow_json_extract_scalar_sql,
     arrow_json_extract_sql,
+    concat_to_dpipe_sql,
     count_if_to_sum,
     no_ilike_sql,
     no_pivot_sql,
@@ -96,6 +97,7 @@ class Generator(generator.Generator):
 
         TRANSFORMS = {
             **generator.Generator.TRANSFORMS,
+            exp.Concat: concat_to_dpipe_sql,
             exp.CountIf: count_if_to_sum,
             exp.Create: transforms.preprocess([_transform_create]),
             exp.CurrentDate: lambda *_: "CURRENT_DATE",
@@ -112,6 +114,7 @@ class Generator(generator.Generator):
             exp.LogicalOr: rename_func("MAX"),
             exp.LogicalAnd: rename_func("MIN"),
             exp.Pivot: no_pivot_sql,
+            exp.SafeConcat: concat_to_dpipe_sql,
             exp.Select: transforms.preprocess(
                 [transforms.eliminate_distinct_on, transforms.eliminate_qualify]
             ),
diff --git a/sqlglot/dialects/tsql.py b/sqlglot/dialects/tsql.py
index ebaa826c5a..6d674f5980 100644
--- a/sqlglot/dialects/tsql.py
+++ b/sqlglot/dialects/tsql.py
@@ -361,6 +361,8 @@ class Parser(parser.Parser):
         LOG_BASE_FIRST = False
         LOG_DEFAULTS_TO_LN = True
 
+        CONCAT_NULL_OUTPUTS_STRING = True
+
         def _parse_system_time(self) -> t.Optional[exp.Expression]:
             if not self._match_text_seq("FOR", "SYSTEM_TIME"):
                 return None
diff --git a/sqlglot/executor/env.py b/sqlglot/executor/env.py
index e76402bc3e..d2c4e72673 100644
--- a/sqlglot/executor/env.py
+++ b/sqlglot/executor/env.py
@@ -151,6 +151,7 @@ def interval(this, unit):
     "CAST": cast,
     "COALESCE": lambda *args: next((a for a in args if a is not None), None),
     "CONCAT": null_if_any(lambda *args: "".join(args)),
+    "SAFECONCAT": null_if_any(lambda *args: "".join(str(arg) for arg in args)),
     "CONCATWS": null_if_any(lambda this, *args: this.join(args)),
     "DATESTRTODATE": null_if_any(lambda arg: datetime.date.fromisoformat(arg)),
     "DIV": null_if_any(lambda e, this: e / this),
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index 141d43f07a..749c3dacfc 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -3507,6 +3507,10 @@ class DPipe(Binary):
     pass
 
 
+class SafeDPipe(DPipe):
+    pass
+
+
 class EQ(Binary, Predicate):
     pass
 
@@ -3921,6 +3925,10 @@ class Concat(Func):
     is_var_len_args = True
 
 
+class SafeConcat(Concat):
+    pass
+
+
 class ConcatWs(Concat):
     _sql_names = ["CONCAT_WS"]
 
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 1ba1eec13f..7a13fc2c0d 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -247,6 +247,7 @@ class Generator:
     UNNEST_COLUMN_ONLY = False
     ALIAS_POST_TABLESAMPLE = False
     IDENTIFIERS_CAN_START_WITH_DIGIT = False
+    STRICT_STRING_CONCAT = False
     NORMALIZE_FUNCTIONS: bool | str = "upper"
     NULL_ORDERING = "nulls_are_small"
 
@@ -1740,10 +1741,11 @@ def trim_sql(self, expression: exp.Trim) -> str:
         else:
             return self.func("TRIM", expression.this, expression.expression)
 
-    def concat_sql(self, expression: exp.Concat) -> str:
-        if len(expression.expressions) == 1:
-            return self.sql(expression.expressions[0])
-        return self.function_fallback_sql(expression)
+    def safeconcat_sql(self, expression: exp.SafeConcat) -> str:
+        expressions = expression.expressions
+        if self.STRICT_STRING_CONCAT:
+            expressions = [exp.cast(e, "text") for e in expressions]
+        return self.func("CONCAT", *expressions)
 
     def check_sql(self, expression: exp.Check) -> str:
         this = self.sql(expression, key="this")
@@ -1766,9 +1768,7 @@ def primarykey_sql(self, expression: exp.ForeignKey) -> str:
         return f"PRIMARY KEY ({expressions}){options}"
 
     def if_sql(self, expression: exp.If) -> str:
-        return self.case_sql(
-            exp.Case(ifs=[expression.copy()], default=expression.args.get("false"))
-        )
+        return self.case_sql(exp.Case(ifs=[expression], default=expression.args.get("false")))
 
     def matchagainst_sql(self, expression: exp.MatchAgainst) -> str:
         modifier = expression.args.get("modifier")
@@ -2074,6 +2074,11 @@ def intdiv_sql(self, expression: exp.IntDiv) -> str:
     def dpipe_sql(self, expression: exp.DPipe) -> str:
         return self.binary(expression, "||")
 
+    def safedpipe_sql(self, expression: exp.SafeDPipe) -> str:
+        if self.STRICT_STRING_CONCAT:
+            return self.func("CONCAT", *[exp.cast(e, "text") for e in expression.flatten()])
+        return self.dpipe_sql(expression)
+
     def div_sql(self, expression: exp.Div) -> str:
         return self.binary(expression, "/")
 
diff --git a/sqlglot/optimizer/annotate_types.py b/sqlglot/optimizer/annotate_types.py
index dc80790c39..9c8dfdb7aa 100644
--- a/sqlglot/optimizer/annotate_types.py
+++ b/sqlglot/optimizer/annotate_types.py
@@ -108,6 +108,9 @@ class TypeAnnotator:
         exp.If: lambda self, expr: self._annotate_by_args(expr, "true", "false"),
         exp.Coalesce: lambda self, expr: self._annotate_by_args(expr, "this", "expressions"),
         exp.Concat: lambda self, expr: self._annotate_with_type(expr, exp.DataType.Type.VARCHAR),
+        exp.SafeConcat: lambda self, expr: self._annotate_with_type(
+            expr, exp.DataType.Type.VARCHAR
+        ),
         exp.ConcatWs: lambda self, expr: self._annotate_with_type(expr, exp.DataType.Type.VARCHAR),
         exp.GroupConcat: lambda self, expr: self._annotate_with_type(
             expr, exp.DataType.Type.VARCHAR
diff --git a/sqlglot/optimizer/canonicalize.py b/sqlglot/optimizer/canonicalize.py
index da2fce8f3c..015b06a3d3 100644
--- a/sqlglot/optimizer/canonicalize.py
+++ b/sqlglot/optimizer/canonicalize.py
@@ -26,7 +26,7 @@ def canonicalize(expression: exp.Expression) -> exp.Expression:
 
 def add_text_to_concat(node: exp.Expression) -> exp.Expression:
     if isinstance(node, exp.Add) and node.type and node.type.this in exp.DataType.TEXT_TYPES:
-        node = exp.Concat(this=node.this, expression=node.expression)
+        node = exp.Concat(expressions=[node.left, node.right])
     return node
 
 
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 8b475fa120..83e38dfc2a 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -708,6 +708,7 @@ class Parser(metaclass=_Parser):
 
     FUNCTION_PARSERS: t.Dict[str, t.Callable] = {
         "CAST": lambda self: self._parse_cast(self.STRICT_CAST),
+        "CONCAT": lambda self: self._parse_concat(),
         "CONVERT": lambda self: self._parse_convert(self.STRICT_CAST),
         "DECODE": lambda self: self._parse_decode(),
         "EXTRACT": lambda self: self._parse_extract(),
@@ -779,6 +780,8 @@ class Parser(metaclass=_Parser):
 
     STRICT_CAST = True
 
+    CONCAT_NULL_OUTPUTS_STRING = False  # A NULL arg in CONCAT yields NULL by default
+
     CONVERT_TYPE_FIRST = False
 
     PREFIXED_PIVOT_COLUMNS = False
@@ -805,6 +808,7 @@ class Parser(metaclass=_Parser):
     INDEX_OFFSET: int = 0
     UNNEST_COLUMN_ONLY: bool = False
     ALIAS_POST_TABLESAMPLE: bool = False
+    STRICT_STRING_CONCAT = False
     NULL_ORDERING: str = "nulls_are_small"
     SHOW_TRIE: t.Dict = {}
     SET_TRIE: t.Dict = {}
@@ -3609,7 +3613,21 @@ def _parse_cast(self, strict: bool) -> exp.Expression:
 
         return self.expression(exp.Cast if strict else exp.TryCast, this=this, to=to)
 
-    def _parse_string_agg(self) -> exp.GroupConcat:
+    def _parse_concat(self) -> t.Optional[exp.Expression]:
+        args = self._parse_csv(self._parse_conjunction)
+        if self.CONCAT_NULL_OUTPUTS_STRING:
+            args = [exp.func("COALESCE", arg, exp.Literal.string("")) for arg in args]
+
+        # Some dialects (e.g. Trino) don't allow a single-argument CONCAT call, so when
+        # we find such a call we replace it with its argument.
+        if len(args) == 1:
+            return args[0]
+
+        return self.expression(
+            exp.Concat if self.STRICT_STRING_CONCAT else exp.SafeConcat, expressions=args  # type: ignore
+        )
+
+    def _parse_string_agg(self) -> exp.Expression:
         expression: t.Optional[exp.Expression]
 
         if self._match(TokenType.DISTINCT):
diff --git a/sqlglot/tokens.py b/sqlglot/tokens.py
index 67252fed2c..c89592ad26 100644
--- a/sqlglot/tokens.py
+++ b/sqlglot/tokens.py
@@ -346,6 +346,7 @@ def __init__(
             col: The column that the token ends on.
             start: The start index of the token.
             end: The ending index of the token.
+            comments: The comments to attach to the token.
         """
         self.token_type = token_type
         self.text = text
diff --git a/tests/dataframe/unit/test_functions.py b/tests/dataframe/unit/test_functions.py
index befa68b603..556001c612 100644
--- a/tests/dataframe/unit/test_functions.py
+++ b/tests/dataframe/unit/test_functions.py
@@ -1278,7 +1278,7 @@ def test_concat(self):
         col = SF.concat(SF.col("cola"), SF.col("colb"))
         self.assertEqual("CONCAT(cola, colb)", col.sql())
         col_single = SF.concat("cola")
-        self.assertEqual("cola", col_single.sql())
+        self.assertEqual("CONCAT(cola)", col_single.sql())
 
     def test_array_position(self):
         col_str = SF.array_position("cola", SF.col("colb"))
diff --git a/tests/dialects/test_clickhouse.py b/tests/dialects/test_clickhouse.py
index db46b89761..7584c67483 100644
--- a/tests/dialects/test_clickhouse.py
+++ b/tests/dialects/test_clickhouse.py
@@ -52,6 +52,14 @@ def test_clickhouse(self):
             "CREATE MATERIALIZED VIEW test_view (id UInt8) TO db.table1 AS SELECT * FROM test_data"
         )
 
+        self.validate_all(
+            "CONCAT(CASE WHEN COALESCE(a, '') IS NULL THEN COALESCE(a, '') ELSE CAST(COALESCE(a, '') AS TEXT) END, CASE WHEN COALESCE(b, '') IS NULL THEN COALESCE(b, '') ELSE CAST(COALESCE(b, '') AS TEXT) END)",
+            read={"postgres": "CONCAT(a, b)"},
+        )
+        self.validate_all(
+            "CONCAT(CASE WHEN a IS NULL THEN a ELSE CAST(a AS TEXT) END, CASE WHEN b IS NULL THEN b ELSE CAST(b AS TEXT) END)",
+            read={"mysql": "CONCAT(a, b)"},
+        )
         self.validate_all(
             r"'Enum8(\'Sunday\' = 0)'", write={"clickhouse": "'Enum8(''Sunday'' = 0)'"}
         )
diff --git a/tests/dialects/test_dialect.py b/tests/dialects/test_dialect.py
index 7e20812a26..09d39b4909 100644
--- a/tests/dialects/test_dialect.py
+++ b/tests/dialects/test_dialect.py
@@ -1084,6 +1084,14 @@ def test_operators(self):
         self.validate_identity("some.column LIKE 'foo' + another.column + 'bar'")
 
         self.validate_all("LIKE(x, 'z')", write={"": "'z' LIKE x"})
+        self.validate_all(
+            "CONCAT(a, b, c)",
+            write={
+                "": "CONCAT(a, b, c)",
+                "redshift": "a || b || c",
+                "sqlite": "a || b || c",
+            },
+        )
         self.validate_all(
             "x ILIKE '%y'",
             read={
@@ -1177,10 +1185,21 @@ def test_operators(self):
         self.validate_all(
             "CONCAT(a)",
             write={
-                "mysql": "a",
+                "clickhouse": "a",
+                "presto": "a",
+                "trino": "a",
                 "tsql": "a",
             },
         )
+        self.validate_all(
+            "COALESCE(a, '')",
+            read={
+                "drill": "CONCAT(a)",
+                "duckdb": "CONCAT(a)",
+                "postgres": "CONCAT(a)",
+                "tsql": "CONCAT(a)",
+            },
+        )
         self.validate_all(
             "IF(x > 1, 1, 0)",
             write={
diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py
index 972a8c857f..7a392daceb 100644
--- a/tests/dialects/test_postgres.py
+++ b/tests/dialects/test_postgres.py
@@ -540,3 +540,24 @@ def test_bool_or(self):
             "SELECT a, LOGICAL_OR(b) FROM table GROUP BY a",
             write={"postgres": "SELECT a, BOOL_OR(b) FROM table GROUP BY a"},
         )
+
+    def test_string_concat(self):
+        self.validate_all(
+            "CONCAT(a, b)",
+            write={
+                "": "CONCAT(COALESCE(a, ''), COALESCE(b, ''))",
+                "duckdb": "CONCAT(COALESCE(a, ''), COALESCE(b, ''))",
+                "postgres": "CONCAT(COALESCE(a, ''), COALESCE(b, ''))",
+                "presto": "CONCAT(CAST(COALESCE(a, '') AS VARCHAR), CAST(COALESCE(b, '') AS VARCHAR))",
+            },
+        )
+        self.validate_all(
+            "a || b",
+            write={
+                "": "a || b",
+                "clickhouse": "CONCAT(CAST(a AS TEXT), CAST(b AS TEXT))",
+                "duckdb": "a || b",
+                "postgres": "a || b",
+                "presto": "CONCAT(CAST(a AS VARCHAR), CAST(b AS VARCHAR))",
+            },
+        )
diff --git a/tests/test_executor.py b/tests/test_executor.py
index a121dea0c7..b91a28b0b6 100644
--- a/tests/test_executor.py
+++ b/tests/test_executor.py
@@ -496,6 +496,7 @@ def test_static_queries(self):
             ("SELECT 1", ["1"], [(1,)]),
             ("SELECT 1 + 2 AS x", ["x"], [(3,)]),
             ("SELECT CONCAT('a', 'b') AS x", ["x"], [("ab",)]),
+            ("SELECT CONCAT('a', 1) AS x", ["x"], [("a1",)]),
             ("SELECT 1 AS x, 2 AS y", ["x", "y"], [(1, 2)]),
             ("SELECT 'foo' LIMIT 1", ["foo"], [("foo",)]),
             (


Code review comments:
diff_hunk: @@ -2074,6 +2074,11 @@ def intdiv_sql(self, expression: exp.IntDiv) -> str:
     def dpipe_sql(self, expression: exp.DPipe) -> str:
         return self.binary(expression, "||")
 
+    def safedpipe_sql(self, expression: exp.SafeDPipe) -> str:
+        if self.STRICT_STRING_CONCAT:
+            return self.func("CONCAT", *[exp.cast(e, "text") for e in expression.flatten()])
path: sqlglot/generator.py
text: @user1:
change to gen (

@author:
is there a difference in the semantics? I thought it'd make no difference since we expand it to match the `*args`

@user1:
one creates a list, the other is an iterator. so you don’t incur the overhead of creating a list 

@author:
I see, makes sense, thanks for clarifying

@author:
Done. FYI I also fixed https://github.com/tobymao/sqlglot/pull/1738/commits/14f2386ca447e5409c31d1d3c8e9ed001aff007b#diff-362498362ae539e216cd83ebfe510977107648742f1a1bee7b2db7b5b7d27974R1747 similarly.