Repository: tobymao/sqlglot

Issue:
`xor` function fails to parse with the clickhouse dialect
**Fully reproducible code snippet**

```
In [12]: import sqlglot as sg

In [13]: sg.__version__
Out[13]: '17.6.0'

In [14]: sg.parse_one("select xor(true, false)", read="clickhouse")
...
ParseError: Invalid expression / Unexpected token. Line 1, Col: 10.
  select xor(true, false)
```

**Official Documentation**

https://clickhouse.com/docs/en/sql-reference/functions/logical-functions#xor


Discussion:
GeorgeSittas: Seems like duckdb also supports `xor(a, b)`, will add support for that shortly

Patch:
diff --git a/sqlglot/dialects/clickhouse.py b/sqlglot/dialects/clickhouse.py
index 1f124a30..9c788d7c 100644
--- a/sqlglot/dialects/clickhouse.py
+++ b/sqlglot/dialects/clickhouse.py
@@ -58,12 +58,18 @@ class ClickHouse(Dialect):
         }
 
     class Parser(parser.Parser):
+        FUNC_TOKENS = {
+            *parser.Parser.FUNC_TOKENS,
+            TokenType.XOR,
+        }
+
         FUNCTIONS = {
             **parser.Parser.FUNCTIONS,
             "ANY": exp.AnyValue.from_arg_list,
             "MAP": parse_var_map,
             "MATCH": exp.RegexpLike.from_arg_list,
             "UNIQ": exp.ApproxDistinct.from_arg_list,
+            "XOR": lambda args: exp.Xor(expressions=args),
         }
 
         FUNCTIONS_WITH_ALIASED_ARGS = {*parser.Parser.FUNCTIONS_WITH_ALIASED_ARGS, "TUPLE"}
@@ -325,6 +331,7 @@ class ClickHouse(Dialect):
             exp.RegexpLike: lambda self, e: f"match({self.format_args(e.this, e.expression)})",
             exp.StrPosition: lambda self, e: f"position({self.format_args(e.this, e.args.get('substr'), e.args.get('position'))})",
             exp.VarMap: lambda self, e: _lower_func(var_map_sql(self, e)),
+            exp.Xor: lambda self, e: self.func("xor", e.this, e.expression, *e.expressions),
         }
 
         PROPERTIES_LOCATION = {
diff --git a/sqlglot/dialects/mysql.py b/sqlglot/dialects/mysql.py
index e4de9348..809fd159 100644
--- a/sqlglot/dialects/mysql.py
+++ b/sqlglot/dialects/mysql.py
@@ -473,6 +473,11 @@ class MySQL(Dialect):
 
         LIMIT_FETCH = "LIMIT"
 
+        def xor_sql(self, expression: exp.Xor) -> str:
+            if expression.expressions:
+                return self.expressions(expression, sep=" XOR ")
+            return super().xor_sql(expression)
+
         def jsonarraycontains_sql(self, expression: exp.JSONArrayContains) -> str:
             return f"{self.sql(expression, 'this')} MEMBER OF({self.sql(expression, 'expression')})"
 
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index d452a3ad..64c0c0d0 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -3554,10 +3554,6 @@ class Or(Connector):
     pass
 
 
-class Xor(Connector):
-    pass
-
-
 class BitwiseAnd(Binary):
     pass
 
@@ -4237,6 +4233,10 @@ class Hex(Func):
     pass
 
 
+class Xor(Connector, Func):
+    arg_types = {"this": False, "expression": False, "expressions": False}
+
+
 class If(Func):
     arg_types = {"this": True, "true": True, "false": False}
 
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 7fbef6e3..a2fe2a13 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -2044,7 +2044,7 @@ class Generator:
     def and_sql(self, expression: exp.And) -> str:
         return self.connector_sql(expression, "AND")
 
-    def xor_sql(self, expression: exp.And) -> str:
+    def xor_sql(self, expression: exp.Xor) -> str:
         return self.connector_sql(expression, "XOR")
 
     def connector_sql(self, expression: exp.Connector, op: str) -> str:


Test patch:
diff --git a/tests/dialects/test_clickhouse.py b/tests/dialects/test_clickhouse.py
index 97e4bb08..bc826453 100644
--- a/tests/dialects/test_clickhouse.py
+++ b/tests/dialects/test_clickhouse.py
@@ -6,6 +6,7 @@ class TestClickhouse(Validator):
     dialect = "clickhouse"
 
     def test_clickhouse(self):
+        self.validate_identity("SELECT xor(TRUE, FALSE)")
         self.validate_identity("ATTACH DATABASE DEFAULT ENGINE = ORDINARY")
         self.validate_identity("CAST(['hello'], 'Array(Enum8(''hello'' = 1))')")
         self.validate_identity("SELECT x, COUNT() FROM y GROUP BY x WITH TOTALS")
@@ -52,6 +53,34 @@ class TestClickhouse(Validator):
             "CREATE MATERIALIZED VIEW test_view (id UInt8) TO db.table1 AS SELECT * FROM test_data"
         )
 
+        self.validate_all(
+            "SELECT xor(1, 0)",
+            read={
+                "clickhouse": "SELECT xor(1, 0)",
+                "mysql": "SELECT 1 XOR 0",
+            },
+            write={
+                "mysql": "SELECT 1 XOR 0",
+            },
+        )
+        self.validate_all(
+            "SELECT xor(0, 1, xor(1, 0, 0))",
+            write={
+                "clickhouse": "SELECT xor(0, 1, xor(1, 0, 0))",
+                "mysql": "SELECT 0 XOR 1 XOR 1 XOR 0 XOR 0",
+            },
+        )
+        self.validate_all(
+            "SELECT xor(xor(1, 0), 1)",
+            read={
+                "clickhouse": "SELECT xor(xor(1, 0), 1)",
+                "mysql": "SELECT 1 XOR 0 XOR 1",
+            },
+            write={
+                "clickhouse": "SELECT xor(xor(1, 0), 1)",
+                "mysql": "SELECT 1 XOR 0 XOR 1",
+            },
+        )
         self.validate_all(
             "CONCAT(CASE WHEN COALESCE(CAST(a AS TEXT), '') IS NULL THEN COALESCE(CAST(a AS TEXT), '') ELSE CAST(COALESCE(CAST(a AS TEXT), '') AS TEXT) END, CASE WHEN COALESCE(CAST(b AS TEXT), '') IS NULL THEN COALESCE(CAST(b AS TEXT), '') ELSE CAST(COALESCE(CAST(b AS TEXT), '') AS TEXT) END)",
             read={"postgres": "CONCAT(a, b)"},