Repository: TobikoData/sqlmesh (python)

Issue:
SQLMesh format converts SAFE_CAST to CAST
This is not semantically the same operation. The issue will need to be resolved in sqlglot I imagine but posting here because of its relevance in SQLMesh given the format CLI command.

![image](https://github.com/TobikoData/sqlmesh/assets/41213451/7e1e61a4-edeb-4416-bf69-c913a4a7f655)


Pull request:
Fix: format CAST as :: less aggressively
Fixes #2554

I went with the `any` approach instead of `.. and not args.get("safe")` to ensure we there are no future regressions if we were to add new args to `Cast`.

Patch:
diff --git a/examples/wursthall/models/src/customer_details.py b/examples/wursthall/models/src/customer_details.py
index 83dcabfe..95914b1e 100644
--- a/examples/wursthall/models/src/customer_details.py
+++ b/examples/wursthall/models/src/customer_details.py
@@ -50,7 +50,7 @@ def execute(
         for _ in range(random.choice(range(10, 20))):
             customer_details.append(
                 CustomerDetails(
-                    id=faker.uuid4(),
+                    id=str(faker.uuid4()),
                     name=faker.name(),
                     phone=faker.phone_number(),
                     email=faker.ascii_email(),
diff --git a/examples/wursthall/models/src/order_item_details.py b/examples/wursthall/models/src/order_item_details.py
index f7e664a1..32ce003c 100644
--- a/examples/wursthall/models/src/order_item_details.py
+++ b/examples/wursthall/models/src/order_item_details.py
@@ -79,7 +79,7 @@ def execute(
         faker = Faker()
         order_ds = order_date.strftime("%Y-%m-%d")
         for _ in range(random.choice(range(50, 100))):
-            order_item_id = faker.uuid4()
+            order_item_id = str(faker.uuid4())
             table_id = np.random.choice(range(0, 20))
             is_registered_customer = np.random.choice([True, False], p=[0.1, 0.9])
             if is_registered_customer:
diff --git a/sqlmesh/core/dialect.py b/sqlmesh/core/dialect.py
index 42f29b41..ac34f6b1 100644
--- a/sqlmesh/core/dialect.py
+++ b/sqlmesh/core/dialect.py
@@ -582,7 +582,13 @@ def format_model_expressions(
     *statements, query = expressions
 
     def cast_to_colon(node: exp.Expression) -> exp.Expression:
-        if isinstance(node, exp.Cast) and not node.args.get("format"):
+        if isinstance(node, exp.Cast) and not any(
+            # Only convert CAST into :: if it doesn't have additional args set, otherwise this
+            # conversion could alter the semantics (eg. changing SAFE_CAST in BigQuery to CAST)
+            arg
+            for name, arg in node.args.items()
+            if name not in ("this", "to")
+        ):
             this = node.this
 
             if not isinstance(this, (exp.Binary, exp.Unary)) or isinstance(this, exp.Paren):


Test patch:
diff --git a/tests/core/test_dialect.py b/tests/core/test_dialect.py
index 1dd5cb32..7d906db6 100644
--- a/tests/core/test_dialect.py
+++ b/tests/core/test_dialect.py
@@ -173,6 +173,27 @@ JINJA_QUERY_BEGIN;
 JINJA_END;"""
     )
 
+    x = format_model_expressions(
+        parse(
+            """
+            MODEL(name a.b, kind FULL, dialect bigquery);
+            SELECT SAFE_CAST('bla' AS INT64) AS FOO
+            """
+        ),
+        dialect="bigquery",
+    )
+    assert (
+        x
+        == """MODEL (
+  name a.b,
+  kind FULL,
+  dialect bigquery
+);
+
+SELECT
+  SAFE_CAST('bla' AS INT64) AS FOO"""
+    )
+
 
 def test_macro_format():
     assert parse_one("@EACH(ARRAY(1,2), x -> x)").sql() == "@EACH(ARRAY(1, 2), x -> x)"