--- /dev/null
+.. change::
+ :tags: usecase, sql
+ :tickets: 9752
+
+
+ Generalized the MSSQL :func:`_sql.try_cast` function into the
+ ``sqlalchemy.`` import namespace so that it may be implemented by third
+ party dialects as well. Within SQLAlchemy, the :func:`_sql.try_cast`
+ function remains a SQL Server-only construct that will raise
+ :class:`.CompileError` if used with backends that don't support it.
+
+ :func:`_sql.try_cast` implements a CAST where un-castable conversions are
+ returned as NULL, instead of raising an error. Theoretically, the construct
+ could be implemented by third party dialects for Google BigQuery, DuckDB,
+ and Snowflake, and possibly others.
+
+ Pull request courtesy Nick Crews.
.. autofunction:: true
+.. autofunction:: try_cast
+
.. autofunction:: tuple_
.. autofunction:: type_coerce
.. autoclass:: TextClause
:members:
+.. autoclass:: TryCast
+ :members:
+
.. autoclass:: Tuple
:members:
from .sql.expression import TextualSelect as TextualSelect
from .sql.expression import true as true
from .sql.expression import True_ as True_
+from .sql.expression import try_cast as try_cast
+from .sql.expression import TryCast as TryCast
from .sql.expression import Tuple as Tuple
from .sql.expression import tuple_ as tuple_
from .sql.expression import type_coerce as type_coerce
# the MIT License: https://www.opensource.org/licenses/mit-license.php
# mypy: ignore-errors
-
from . import base # noqa
from . import pymssql # noqa
from . import pyodbc # noqa
from .base import TIME
from .base import TIMESTAMP
from .base import TINYINT
-from .base import try_cast
from .base import UNIQUEIDENTIFIER
from .base import VARBINARY
from .base import VARCHAR
from .base import XML
+from ...sql import try_cast
base.dialect = dialect = pyodbc.dialect
from ...sql import quoted_name
from ...sql import roles
from ...sql import sqltypes
+from ...sql import try_cast as try_cast # noqa: F401
from ...sql import util as sql_util
from ...sql._typing import is_sql_compiler
from ...sql.compiler import InsertmanyvaluesSentinelOpts
+from ...sql.elements import TryCast as TryCast # noqa: F401
from ...types import BIGINT
from ...types import BINARY
from ...types import CHAR
__visit_name__ = "SQL_VARIANT"
-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
-
- """
- 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
from .expression import text as text
from .expression import true as true
from .expression import True_ as True_
+from .expression import try_cast as try_cast
from .expression import tuple_ as tuple_
from .expression import type_coerce as type_coerce
from .expression import union as union
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
:ref:`tutorial_casts`
+ :func:`.try_cast` - an alternative to CAST that results in
+ NULLs when the cast fails, instead of raising an error.
+ Only supported by some dialects.
+
: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.
return Cast(expression, type_)
+def try_cast(
+ expression: _ColumnExpressionOrLiteralArgument[Any],
+ type_: _TypeEngineArgument[_T],
+) -> TryCast[_T]:
+ """Produce a ``TRY_CAST`` expression for backends which support it;
+ this is a ``CAST`` which returns NULL for un-castable conversions.
+
+ In SQLAlchemy, this construct is supported **only** by the SQL Server
+ dialect, and will raise a :class:`.CompileError` if used on other
+ included backends. However, third party backends may also support
+ this construct.
+
+ .. tip:: As :func:`_sql.try_cast` originates from the SQL Server dialect,
+ it's importable both from ``sqlalchemy.`` as well as from
+ ``sqlalchemy.dialects.mssql``.
+
+ :func:`_sql.try_cast` returns an instance of :class:`.TryCast` and
+ generally behaves similarly to the :class:`.Cast` construct;
+ at the SQL level, the difference between ``CAST`` and ``TRY_CAST``
+ is that ``TRY_CAST`` returns NULL for an un-castable expression,
+ such as attempting to cast a string ``"hi"`` to an integer value.
+
+ E.g.::
+
+ from sqlalchemy import select, try_cast, Numeric
+
+ stmt = select(
+ try_cast(product_table.c.unit_price, Numeric(10, 4))
+ )
+
+ The above would render on Microsoft SQL Server as::
+
+ SELECT TRY_CAST (product_table.unit_price AS NUMERIC(10, 4))
+ FROM product_table
+
+ .. versionadded:: 2.0.14 :func:`.try_cast` has been
+ generalized from the SQL Server dialect into a general use
+ construct that may be supported by additional dialects.
+
+ """
+ return TryCast(expression, type_)
+
+
def column(
text: str,
type_: Optional[_TypeEngineArgument[_T]] = None,
replacement._compiler_dispatch(self, **kw),
)
+ def visit_try_cast(self, cast, **kwargs):
+ return "TRY_CAST(%s AS %s)" % (
+ cast.clause._compiler_dispatch(self, **kwargs),
+ cast.typeclause._compiler_dispatch(self, **kwargs),
+ )
+
class DDLCompiler(Compiled):
is_ddl = True
: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.
return self.clause
+class TryCast(Cast[_T]):
+ """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.
from ._elements_constructors import over as over
from ._elements_constructors import text as text
from ._elements_constructors import true as true
+from ._elements_constructors import try_cast as try_cast
from ._elements_constructors import tuple_ as tuple_
from ._elements_constructors import type_coerce as type_coerce
from ._elements_constructors import within_group as within_group
from .elements import SQLColumnExpression as SQLColumnExpression
from .elements import TextClause as TextClause
from .elements import True_ as True_
+from .elements import TryCast as TryCast
from .elements import Tuple as Tuple
from .elements import TypeClause as TypeClause
from .elements import TypeCoerce as TypeCoerce
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
from sqlalchemy.dialects import mssql
from sqlalchemy.dialects.mssql import base as mssql_base
-from sqlalchemy.dialects.mssql.base import try_cast
from sqlalchemy.sql import column
from sqlalchemy.sql import quoted_name
from sqlalchemy.sql import table
"CREATE INDEX foo ON test (x) INCLUDE (y) WHERE y > 1",
)
- def test_try_cast(self):
- metadata = MetaData()
- t1 = Table("t1", metadata, Column("id", Integer, primary_key=True))
+ @testing.variation("use_mssql_version", [True, False])
+ def test_try_cast(self, use_mssql_version):
+ t1 = Table("t1", MetaData(), Column("id", Integer, primary_key=True))
+
+ if use_mssql_version:
+ stmt = select(mssql.try_cast(t1.c.id, Integer))
+ else:
+ stmt = select(try_cast(t1.c.id, Integer))
self.assert_compile(
- select(try_cast(t1.c.id, Integer)),
+ stmt,
"SELECT TRY_CAST (t1.id AS INTEGER) AS id FROM t1",
)
from sqlalchemy import text
from sqlalchemy import TIMESTAMP
from sqlalchemy import true
+from sqlalchemy import try_cast
from sqlalchemy import tuple_
from sqlalchemy import type_coerce
from sqlalchemy import types
"ALTER TABLE testtbl ADD EXCLUDE USING gist " "(room WITH =)",
)
+ def test_try_cast(self):
+ t1 = Table("t1", MetaData(), Column("id", Integer, primary_key=True))
+ expr = select(try_cast(t1.c.id, Integer))
+
+ eq_ignore_whitespace(
+ str(expr),
+ "SELECT TRY_CAST(t1.id AS INTEGER) AS id FROM t1",
+ )
+
class KwargPropagationTest(fixtures.TestBase):
@classmethod