]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Move try_cast() from mssql to base
authorNick Crews <nicholas.b.crews@gmail.com>
Mon, 8 May 2023 23:07:58 +0000 (15:07 -0800)
committerNick Crews <nicholas.b.crews@gmail.com>
Mon, 8 May 2023 23:07:58 +0000 (15:07 -0800)
Other backends besides mssql support an operation like try_cast. Move it
to a common place so they can all implement it.

Fixes: #9752
Closes: #9752
doc/build/changelog/unreleased_20/9752.rst [new file with mode: 0644]
lib/sqlalchemy/__init__.py
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/sql/_elements_constructors.py
lib/sqlalchemy/sql/elements.py
test/dialect/mssql/test_compiler.py

diff --git a/doc/build/changelog/unreleased_20/9752.rst b/doc/build/changelog/unreleased_20/9752.rst
new file mode 100644 (file)
index 0000000..519c75f
--- /dev/null
@@ -0,0 +1,14 @@
+.. change::
+    :tags: usecase, sql
+    :tickets: 9752
+
+
+    Added new :func:`.try_cast` factory function and corresponding
+    :class:`.TryCast` SQL Element, which implements a cast where
+    un-castable values are returned as NULL, instead of raising an error.
+
+    This is currently implemented as ``TRY_CAST`` in Microsoft SQL Server.
+    and could be implemented in other backends:
+    * ``SAFE_CAST`` in Google BigQuery, and
+    * ``TRY_CAST`` in DuckDB.
+    * ``TRY_CAST`` in Snowflake.
index 0bf16401c5b28430041c0ead0a9e67c6d1441674..da30e7edc2e922e3b580719773c5a5520348e35a 100644 (file)
@@ -99,6 +99,8 @@ from .sql.expression import Case as Case
 from .sql.expression import case as case
 from .sql.expression import Cast as Cast
 from .sql.expression import cast as cast
+from .sql.expression import TryCast as TryCast
+from .sql.expression import try_cast as try_cast
 from .sql.expression import ClauseElement as ClauseElement
 from .sql.expression import ClauseList as ClauseList
 from .sql.expression import collate as collate
index b33ce4aec8e6618fa9e12ed6af71d5991a14b80a..e37f92af33edeb5e7ec2bffac0d5e740ddbfe092 100644 (file)
@@ -941,6 +941,7 @@ from ...sql import sqltypes
 from ...sql import util as sql_util
 from ...sql._typing import is_sql_compiler
 from ...sql.compiler import InsertmanyvaluesSentinelOpts
+from ...sql.elements import TryCast
 from ...types import BIGINT
 from ...types import BINARY
 from ...types import CHAR
@@ -1604,40 +1605,14 @@ class SQL_VARIANT(sqltypes.TypeEngine):
 
 
 def try_cast(*arg, **kw):
-    """Create a TRY_CAST expression.
-
-    :class:`.TryCast` is a subclass of SQLAlchemy's :class:`.Cast`
-    construct, and works in the same way, except that the SQL expression
-    rendered is "TRY_CAST" rather than "CAST"::
-
-        from sqlalchemy import select
-        from sqlalchemy import Numeric
-        from sqlalchemy.dialects.mssql import try_cast
-
-        stmt = select(
-            try_cast(product_table.c.unit_price, Numeric(10, 4))
-        )
-
-    The above would render::
-
-        SELECT TRY_CAST (product_table.unit_price AS NUMERIC(10, 4))
-        FROM product_table
-
-    .. versionadded:: 1.3.7
-
-    """
+    util.warn_deprecated(
+        "`sqlalchemy.dialects.mssql.base.try_cast` is deprecated. "
+        "Use directly from sqlalchemy instead, i.e. `sa.try_cast(...)`",
+        "2.1",
+    )
     return TryCast(*arg, **kw)
 
 
-class TryCast(sql.elements.Cast):
-    """Represent a SQL Server TRY_CAST expression."""
-
-    __visit_name__ = "try_cast"
-
-    stringify_dialect = "mssql"
-    inherit_cache = True
-
-
 # old names.
 MSDateTime = _MSDateTime
 MSDate = _MSDate
index 99a839cc909cf20e1e9f1669536126d35066a325..9a52e59e4e49abe82d0159438ff097b540c36744 100644 (file)
@@ -40,6 +40,7 @@ from .elements import Null
 from .elements import Over
 from .elements import TextClause
 from .elements import True_
+from .elements import TryCast
 from .elements import Tuple
 from .elements import TypeCoerce
 from .elements import UnaryExpression
@@ -893,6 +894,9 @@ def cast(
 
         :ref:`tutorial_casts`
 
+        :func:`.try_cast` - an alternative to CAST that results in
+        NULLs when the cast fails, instead of raising an error.
+
         :func:`.type_coerce` - an alternative to CAST that coerces the type
         on the Python side only, which is often sufficient to generate the
         correct SQL and data coercion.
@@ -902,6 +906,30 @@ def cast(
     return Cast(expression, type_)
 
 
+def try_cast(*arg, **kw):
+    """Create a TRY_CAST expression.
+
+    :class:`.TryCast` is a subclass of SQLAlchemy's :class:`.Cast`
+    construct, and works in the same way, except that the SQL expression
+    rendered is "TRY_CAST" rather than "CAST"::
+
+        from sqlalchemy import select, try_cast, Numeric
+
+        stmt = select(
+            try_cast(product_table.c.unit_price, Numeric(10, 4))
+        )
+
+    The above would render with mssql as::
+
+        SELECT TRY_CAST (product_table.unit_price AS NUMERIC(10, 4))
+        FROM product_table
+
+    .. versionadded:: 2.1.0
+
+    """
+    return TryCast(*arg, **kw)
+
+
 def column(
     text: str,
     type_: Optional[_TypeEngineArgument[_T]] = None,
index 2e32da75408a63fc0cce2d7b42b14336efe65f25..c70bac477285a049a99ee0d0bad250f33fd3fa58 100644 (file)
@@ -3372,6 +3372,8 @@ class Cast(WrapsColumnExpression[_T]):
 
         :func:`.cast`
 
+        :func:`.try_cast`
+
         :func:`.type_coerce` - an alternative to CAST that coerces the type
         on the Python side only, which is often sufficient to generate the
         correct SQL and data coercion.
@@ -3412,6 +3414,23 @@ class Cast(WrapsColumnExpression[_T]):
         return self.clause
 
 
+
+class TryCast(Cast):
+    """Represent a TRY_CAST expression.
+
+    Details on :class:`.TryCast` usage is at :func:`.try_cast`.
+
+    .. seealso::
+
+        :func:`.try_cast`
+
+        :ref:`tutorial_casts`
+    """
+
+    __visit_name__ = "try_cast"
+    inherit_cache = True
+
+
 class TypeCoerce(WrapsColumnExpression[_T]):
     """Represent a Python-side type-coercion wrapper.
 
index 0076c76fd905e47e1b55649c2c888aed655ed91c..3dbd436d3c40c920412cdc6633369cf03034e3be 100644 (file)
@@ -20,6 +20,7 @@ from sqlalchemy import String
 from sqlalchemy import Table
 from sqlalchemy import testing
 from sqlalchemy import text
+from sqlalchemy import try_cast
 from sqlalchemy import union
 from sqlalchemy import UniqueConstraint
 from sqlalchemy import update
@@ -1477,10 +1478,15 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
         metadata = MetaData()
         t1 = Table("t1", metadata, Column("id", Integer, primary_key=True))
 
-        self.assert_compile(
-            select(try_cast(t1.c.id, Integer)),
-            "SELECT TRY_CAST (t1.id AS INTEGER) AS id FROM t1",
-        )
+        def call(func):
+            self.assert_compile(
+                select(func(t1.c.id, Integer)),
+                "SELECT TRY_CAST (t1.id AS INTEGER) AS id FROM t1",
+            )
+
+        with testing.expect_deprecated(".*try_cast.*"):
+            call(mssql_base.try_cast)
+        call(try_cast)
 
     @testing.combinations(
         ("no_persisted", "", "ignore"),