Repository: TobikoData/sqlmesh
Pull request #4655 (2025-06-04 15:46:18)

feat: lazy import duckdb
Similar PRs: 
https://github.com/TobikoData/sqlmesh/pull/4653
https://github.com/TobikoData/sqlmesh/pull/4631

Removed support for <0.10.0 release of DuckDB since 0.10.0 was released Feb 2024 therefore I think we are safe to deprecate. 


Patch:
diff --git a/docs/guides/tablediff.md b/docs/guides/tablediff.md
index b9362c11..d4ed61e0 100644
--- a/docs/guides/tablediff.md
+++ b/docs/guides/tablediff.md
@@ -253,12 +253,12 @@ Then, specify each table's gateway in the `table_diff` command with this syntax:
 For example, we could diff the `landing.table` table across `bigquery` and `snowflake` gateways like this:
 
 ```sh
-$ sqlmesh table_diff 'bigquery|landing.table:snowflake|lake.table'
+$ tcloud sqlmesh table_diff 'bigquery|landing.table:snowflake|lake.table'
 ```
 
 This syntax tells SQLMesh to use the cross-database diffing algorithm instead of the normal within-database diffing algorithm.
 
-After adding gateways to the table names, use `table_diff` as described above - the same options apply for specifying the join keys, decimal precision, etc. See `sqlmesh table_diff --help` for a [full list of options](../reference/cli.md#table_diff).
+After adding gateways to the table names, use `table_diff` as described above - the same options apply for specifying the join keys, decimal precision, etc. See `tcloud sqlmesh table_diff --help` for a [full list of options](../reference/cli.md#table_diff).
 
 !!! warning
 
@@ -273,7 +273,7 @@ A cross-database diff is broken up into two stages.
 The first stage is a schema diff. This example shows that differences in column name case across the two tables are identified as schema differences:
 
 ```bash
-$ sqlmesh table_diff 'bigquery|sqlmesh_example.full_model:snowflake|sqlmesh_example.full_model' --on item_id --show-sample
+$ tcloud sqlmesh table_diff 'bigquery|sqlmesh_example.full_model:snowflake|sqlmesh_example.full_model' --on item_id --show-sample
 
 Schema Diff Between 'BIGQUERY|SQLMESH_EXAMPLE.FULL_MODEL' and 'SNOWFLAKE|SQLMESH_EXAMPLE.FULL_MODEL':
 ├── Added Columns:
diff --git a/pyproject.toml b/pyproject.toml
index c2464ccf..6054adc0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ dependencies = [
     "astor",
     "click",
     "croniter",
-    "duckdb!=0.10.3",
+    "duckdb>=0.10.0,!=0.10.3",
     "dateparser",
     "hyperscript>=0.1.0",
     "importlib-metadata; python_version<'3.12'",
@@ -275,6 +275,7 @@ extend-select = ["TID"]
 
 [tool.ruff.lint.flake8-tidy-imports]
 banned-module-level-imports = [
-    "pandas",
+    "duckdb",
     "numpy",
+    "pandas",
 ]
diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py
index 1bc12767..3dc77ea2 100644
--- a/sqlmesh/core/context.py
+++ b/sqlmesh/core/context.py
@@ -1677,6 +1677,12 @@ class GenericContext(BaseContext, t.Generic[C]):
             The list of TableDiff objects containing schema and summary differences.
         """
 
+        if "|" in source or "|" in target:
+            raise ConfigError(
+                "Cross-database table diffing is available in Tobiko Cloud. Read more here: "
+                "https://sqlmesh.readthedocs.io/en/stable/guides/tablediff/#diffing-tables-or-views-across-gateways"
+            )
+
         table_diffs: t.List[TableDiff] = []
 
         # Diffs multiple or a single model across two environments
diff --git a/sqlmesh/core/engine_adapter/duckdb.py b/sqlmesh/core/engine_adapter/duckdb.py
index f316c236..f7f72f96 100644
--- a/sqlmesh/core/engine_adapter/duckdb.py
+++ b/sqlmesh/core/engine_adapter/duckdb.py
@@ -1,7 +1,6 @@
 from __future__ import annotations
 
 import typing as t
-from duckdb import __version__ as duckdb_version
 from sqlglot import exp
 
 from sqlmesh.core.engine_adapter.mixins import (
@@ -18,7 +17,6 @@ from sqlmesh.core.engine_adapter.shared import (
     SourceQuery,
     set_catalog,
 )
-from sqlmesh.utils import major_minor
 from sqlmesh.core.schema_diff import SchemaDiffer
 
 if t.TYPE_CHECKING:
@@ -35,13 +33,8 @@ class DuckDBEngineAdapter(LogicalMergeMixin, GetCurrentCatalogFromFunctionMixin,
             exp.DataType.build("DECIMAL", dialect=DIALECT).this: [(18, 3), (0,)],
         },
     )
-
-    # TODO: remove once we stop supporting DuckDB 0.9
-    COMMENT_CREATION_TABLE, COMMENT_CREATION_VIEW = (
-        (CommentCreationTable.UNSUPPORTED, CommentCreationView.UNSUPPORTED)
-        if major_minor(duckdb_version) < (0, 10)
-        else (CommentCreationTable.COMMENT_COMMAND_ONLY, CommentCreationView.COMMENT_COMMAND_ONLY)
-    )
+    COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY
+    COMMENT_CREATION_VIEW = CommentCreationView.COMMENT_COMMAND_ONLY
 
     @property
     def catalog_support(self) -> CatalogSupport:
diff --git a/sqlmesh/core/macros.py b/sqlmesh/core/macros.py
index db530481..d573fdac 100644
--- a/sqlmesh/core/macros.py
+++ b/sqlmesh/core/macros.py
@@ -1452,6 +1452,26 @@ def _coerce(
         if base is SQL and isinstance(expr, exp.Expression):
             return expr.sql(dialect)
 
+        if base is t.Literal:
+            if not isinstance(expr, (exp.Literal, exp.Boolean)):
+                raise SQLMeshError(
+                    f"{base_err_msg} Coercion to {base} requires a literal expression."
+                )
+            literal_type_args = t.get_args(typ)
+            try:
+                for literal_type_arg in literal_type_args:
+                    expr_is_bool = isinstance(expr.this, bool)
+                    literal_is_bool = isinstance(literal_type_arg, bool)
+                    if (expr_is_bool and literal_is_bool and literal_type_arg == expr.this) or (
+                        not expr_is_bool
+                        and not literal_is_bool
+                        and str(literal_type_arg) == str(expr.this)
+                    ):
+                        return type(literal_type_arg)(expr.this)
+            except Exception:
+                raise SQLMeshError(base_err_msg)
+            raise SQLMeshError(base_err_msg)
+
         if isinstance(expr, base):
             return expr
         if issubclass(base, exp.Expression):


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index e0066ec5..54f8db04 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -14,7 +14,7 @@ from unittest.mock import PropertyMock
 import os
 import shutil
 
-import duckdb
+import duckdb  # noqa: TID253
 import pandas as pd  # noqa: TID253
 import pytest
 from pytest_mock.plugin import MockerFixture
diff --git a/tests/core/state_sync/test_state_sync.py b/tests/core/state_sync/test_state_sync.py
index 10ab17a0..173264f9 100644
--- a/tests/core/state_sync/test_state_sync.py
+++ b/tests/core/state_sync/test_state_sync.py
@@ -4,7 +4,7 @@ import re
 import typing as t
 from unittest.mock import call, patch
 
-import duckdb
+import duckdb  # noqa: TID253
 import pandas as pd  # noqa: TID253
 import pytest
 import time_machine
diff --git a/tests/core/test_macros.py b/tests/core/test_macros.py
index 5b79d5ff..7f4a417c 100644
--- a/tests/core/test_macros.py
+++ b/tests/core/test_macros.py
@@ -96,6 +96,12 @@ def macro_evaluator() -> MacroEvaluator:
     def test_select_macro(evaluator):
         return "SELECT 1 AS col"
 
+    @macro()
+    def test_literal_type(evaluator, a: t.Literal["test_literal_a", "test_literal_b", 1, True]):
+        if isinstance(a, exp.Expression):
+            raise SQLMeshError("Coercion failed")
+        return f"'{a}'"
+
     return MacroEvaluator(
         "hive",
         {"test": Executable(name="test", payload="def test(_):\n    return 'test'")},
@@ -1087,3 +1093,33 @@ def test_macro_with_spaces():
         ("d.@z", 'd.a."b c"'),
     ):
         assert evaluator.transform(parse_one(sql)).sql() == expected
+
+
+def test_macro_coerce_literal_type(macro_evaluator):
+    expression = d.parse_one("@TEST_LITERAL_TYPE('test_literal_a')")
+    assert macro_evaluator.transform(expression).sql() == "'test_literal_a'"
+
+    expression = d.parse_one("@TEST_LITERAL_TYPE('test_literal_b')")
+    assert macro_evaluator.transform(expression).sql() == "'test_literal_b'"
+
+    expression = d.parse_one("@TEST_LITERAL_TYPE(1)")
+    assert macro_evaluator.transform(expression).sql() == "'1'"
+
+    expression = d.parse_one("@TEST_LITERAL_TYPE(True)")
+    assert macro_evaluator.transform(expression).sql() == "'True'"
+
+    expression = d.parse_one("@TEST_LITERAL_TYPE('test_literal_c')")
+    with pytest.raises(MacroEvalError, match=".*Coercion failed"):
+        macro_evaluator.transform(expression)
+
+    expression = d.parse_one("@TEST_LITERAL_TYPE(2)")
+    with pytest.raises(MacroEvalError, match=".*Coercion failed"):
+        macro_evaluator.transform(expression)
+
+    expression = d.parse_one("@TEST_LITERAL_TYPE(False)")
+    with pytest.raises(MacroEvalError, match=".*Coercion failed"):
+        macro_evaluator.transform(expression)
+
+    expression = d.parse_one("@TEST_LITERAL_TYPE(1.0)")
+    with pytest.raises(MacroEvalError, match=".*Coercion failed"):
+        macro_evaluator.transform(expression)