Repository: tobymao/sqlglot (2024-09-09 ~ 2024-09-09)

problem_statement: Support Postgres 17 MERGE Returning
**Is your feature request related to a problem? Please describe.**
Postgres 17, which is likely going to be generally available later this month adds the ability for MERGE statements to return rows. However, sqlglot is unable to parse these.

**Describe the solution you'd like**
Take this example, [from this blog](https://neon.tech/blog/top-3-features-in-postgres-17):

```sql
MERGE INTO hero h
USING (VALUES ('Wade', 'Wilson', 'Deadpool')) v(first_name, last_name, hero_name)
ON h.hero_name = v.hero_name
WHEN MATCHED THEN
  UPDATE SET first_name = v.first_name, last_name = v.last_name
WHEN NOT MATCHED THEN
  INSERT (first_name, last_name, hero_name)
  VALUES (v.first_name, v.last_name, v.hero_name)
RETURNING merge_action(), *;
```

Trying to parse this yields:

```
Invalid expression / Unexpected token. Line 10, Col: 9.
   THEN
  INSERT (first_name, last_name, hero_name)
  VALUES (v.first_name, v.last_name, v.hero_name)
RETURNING merge_action(), *;
```


date: 2024-09-09
patch: diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index 6bbd0fb4..21d3a8fd 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -6279,6 +6279,7 @@ class Merge(Expression):
         "on": True,
         "expressions": True,
         "with": False,
+        "returning": False,
     }
 
 
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 9a346b24..6e11d68c 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -3587,9 +3587,12 @@ class Generator(metaclass=_Generator):
         on = f"ON {self.sql(expression, 'on')}"
         expressions = self.expressions(expression, sep=" ", indent=False)
         sep = self.sep()
+        returning = self.expressions(expression, key="returning", indent=False)
+        returning = f"RETURNING {returning}" if returning else ""
 
         return self.prepend_ctes(
-            expression, f"MERGE INTO {this}{table_alias}{sep}{using}{sep}{on}{sep}{expressions}"
+            expression,
+            f"MERGE INTO {this}{table_alias}{sep}{using}{sep}{on}{sep}{expressions}{sep}{returning}",
         )
 
     def tochar_sql(self, expression: exp.ToChar) -> str:
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index a15dd815..840f2f13 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -6825,6 +6825,7 @@ class Parser(metaclass=_Parser):
             using=using,
             on=on,
             expressions=self._parse_when_matched(),
+            returning=self._match(TokenType.RETURNING) and self._parse_csv(self._parse_bitwise),
         )
 
     def _parse_when_matched(self) -> t.List[exp.When]:
@@ -6865,7 +6866,7 @@ class Parser(metaclass=_Parser):
             elif self._match(TokenType.DELETE):
                 then = self.expression(exp.Var, this=self._prev.text)
             else:
-                then = None
+                then = self._parse_var_from_options(self.CONFLICT_ACTIONS)
 
             whens.append(
                 self.expression(

test_patch: diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py
index f3f21a9b..1b89ecd1 100644
--- a/tests/dialects/test_postgres.py
+++ b/tests/dialects/test_postgres.py
@@ -794,6 +794,9 @@ class TestPostgres(Validator):
         self.validate_identity(
             'SELECT js, js IS JSON ARRAY WITH UNIQUE KEYS AS "array w. UK?", js IS JSON ARRAY WITHOUT UNIQUE KEYS AS "array w/o UK?", js IS JSON ARRAY UNIQUE KEYS AS "array w UK 2?" FROM t'
         )
+        self.validate_identity(
+            "MERGE INTO target_table USING source_table AS source ON target.id = source.id WHEN MATCHED THEN DO NOTHING WHEN NOT MATCHED THEN DO NOTHING RETURNING MERGE_ACTION(), *"
+        )
 
     def test_ddl(self):
         # Checks that user-defined types are parsed into DataType instead of Identifier


----

problem_statement: Transpile from DuckDB to BigQuery for UUID results in a BigQuery column type of UUID but no such type exists in BQ
As the name suggests DuckDB defines a number of column types one of which is UUID. But such a type doesn't exist in BigQuery. The below snippet shows that.

```
>>> sqlg.transpile("create table foo(a long, id uuid)", read="duckdb", write="
... bigquery")[0]
'CREATE TABLE foo (a INT64, id UUID)'
```

There exists a BQ function to generate UUIDs: `GENERATE_UUID` and it returns a string.

SQLGlot should probably parse this and return it as a `STRING` when going from duckdb to BQ.

More types reference: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types

date: 2024-09-09
patch: diff --git a/sqlglot/dialects/bigquery.py b/sqlglot/dialects/bigquery.py
index 883ee665..08dfbbc8 100644
--- a/sqlglot/dialects/bigquery.py
+++ b/sqlglot/dialects/bigquery.py
@@ -754,8 +754,9 @@ class BigQuery(Dialect):
             exp.DataType.Type.TIMESTAMPTZ: "TIMESTAMP",
             exp.DataType.Type.TIMESTAMPLTZ: "TIMESTAMP",
             exp.DataType.Type.TINYINT: "INT64",
-            exp.DataType.Type.VARBINARY: "BYTES",
             exp.DataType.Type.ROWVERSION: "BYTES",
+            exp.DataType.Type.UUID: "STRING",
+            exp.DataType.Type.VARBINARY: "BYTES",
             exp.DataType.Type.VARCHAR: "STRING",
             exp.DataType.Type.VARIANT: "ANY TYPE",
         }

test_patch: diff --git a/tests/dialects/test_duckdb.py b/tests/dialects/test_duckdb.py
index 18a030c6..98d1604b 100644
--- a/tests/dialects/test_duckdb.py
+++ b/tests/dialects/test_duckdb.py
@@ -18,6 +18,13 @@ class TestDuckDB(Validator):
             "WITH _data AS (SELECT [STRUCT(1 AS a, 2 AS b), STRUCT(2 AS a, 3 AS b)] AS col) SELECT col.b FROM _data, UNNEST(_data.col) AS col WHERE col.a = 1",
         )
 
+        self.validate_all(
+            "CAST(x AS UUID)",
+            write={
+                "bigquery": "CAST(x AS STRING)",
+                "duckdb": "CAST(x AS UUID)",
+            },
+        )
         self.validate_all(
             """SELECT CASE WHEN JSON_VALID('{"x: 1}') THEN '{"x: 1}' ELSE NULL END""",
             read={


----

problem_statement: BigQuery: `MERGE ... INSERT ROW` not supported
I stumbled upon this issue in sqlmesh, but assume that sqlglot causes it.
In BigQuery, the `MERGE` statement has an alternative `INSERT ROW` syntax which inserts all columns without having to specify each one explicitly. sqlglot seems to not support this yet, as it treats `ROW` as a column name to insert.

Input: `sqlglot.transpile('merge a using b on false when not matched then insert row', read='bigquery', write='bigquery')`
Output: `['MERGE INTO a USING b ON FALSE WHEN NOT MATCHED THEN INSERT (row)']`

Expected output: `['MERGE INTO a USING b ON FALSE WHEN NOT MATCHED THEN INSERT ROW']`

Documentation: https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement
Example: https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#example_8
date: 2024-09-09
patch: diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 2ae75e17..6ee0cb14 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -6851,13 +6851,13 @@ class Parser(metaclass=_Parser):
             self._match(TokenType.THEN)
 
             if self._match(TokenType.INSERT):
-                _this = self._parse_star()
-                if _this:
-                    then: t.Optional[exp.Expression] = self.expression(exp.Insert, this=_this)
+                this = self._parse_star()
+                if this:
+                    then: t.Optional[exp.Expression] = self.expression(exp.Insert, this=this)
                 else:
                     then = self.expression(
                         exp.Insert,
-                        this=self._parse_value(),
+                        this=exp.var("ROW") if self._match_text_seq("ROW") else self._parse_value(),
                         expression=self._match_text_seq("VALUES") and self._parse_value(),
                     )
             elif self._match(TokenType.UPDATE):

test_patch: diff --git a/tests/dialects/test_bigquery.py b/tests/dialects/test_bigquery.py
index c8c21765..b8838962 100644
--- a/tests/dialects/test_bigquery.py
+++ b/tests/dialects/test_bigquery.py
@@ -191,6 +191,9 @@ LANGUAGE js AS
         self.validate_identity("CAST(x AS NVARCHAR)", "CAST(x AS STRING)")
         self.validate_identity("CAST(x AS TIMESTAMPTZ)", "CAST(x AS TIMESTAMP)")
         self.validate_identity("CAST(x AS RECORD)", "CAST(x AS STRUCT)")
+        self.validate_identity(
+            "MERGE INTO dataset.NewArrivals USING (SELECT * FROM UNNEST([('microwave', 10, 'warehouse #1'), ('dryer', 30, 'warehouse #1'), ('oven', 20, 'warehouse #2')])) ON FALSE WHEN NOT MATCHED THEN INSERT ROW WHEN NOT MATCHED BY SOURCE THEN DELETE"
+        )
         self.validate_identity(
             "SELECT * FROM `SOME_PROJECT_ID.SOME_DATASET_ID.INFORMATION_SCHEMA.SOME_VIEW`"
         )