From 52728f7052e643e9890da75ef2c756d4bead41f0 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 3 Feb 2021 10:42:08 -0500 Subject: [PATCH] Add coercions to literal() To prevent literal() from receiving a ClauseElement which then leads to confusing results, add a new LiteralValueRole coercion that does an _is_literal() check and implement for literal(). Fixes: #5639 Change-Id: Ibd686544af2d7c013765278f984baf237de88caf --- lib/sqlalchemy/sql/coercions.py | 23 ++++++++++++++++++++--- lib/sqlalchemy/sql/elements.py | 2 +- lib/sqlalchemy/sql/roles.py | 4 ++++ test/sql/test_roles.py | 13 +++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py index 3b972be41d..0d5ffd43aa 100644 --- a/lib/sqlalchemy/sql/coercions.py +++ b/lib/sqlalchemy/sql/coercions.py @@ -138,7 +138,7 @@ def expect(role, element, apply_propagate_attrs=None, argname=None, **kw): ): resolved = None - if impl._resolve_string_only: + if impl._resolve_literal_only: resolved = impl._literal_coercion(element, **kw) else: @@ -223,7 +223,7 @@ class RoleImpl(object): raise NotImplementedError() _post_coercion = None - _resolve_string_only = False + _resolve_literal_only = False def __init__(self, role_class): self._role_class = role_class @@ -274,7 +274,7 @@ class _Deannotate(object): class _StringOnly(object): __slots__ = () - _resolve_string_only = True + _resolve_literal_only = True class _ReturnsStringKey(object): @@ -380,6 +380,23 @@ class _CoerceLiterals(object): self._raise_for_expected(element, argname) +class LiteralValueImpl(RoleImpl): + _resolve_literal_only = True + + def _implicit_coercions( + self, element, resolved, argname, type_=None, **kw + ): + if not _is_literal(resolved): + self._raise_for_expected( + element, resolved=resolved, argname=argname, **kw + ) + + return elements.BindParameter(None, element, type_=type_, unique=True) + + def _literal_coercion(self, element, argname=None, type_=None, **kw): + return element + + class _SelectIsNotFrom(object): __slots__ = () diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index a9f21cd5f8..3d66435039 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -150,7 +150,7 @@ def literal(value, type_=None): will provide bind-parameter translation for this literal. """ - return BindParameter(None, value, type_=type_, unique=True) + return coercions.expect(roles.LiteralValueRole, value, type_=type_) def outparam(key, type_=None): diff --git a/lib/sqlalchemy/sql/roles.py b/lib/sqlalchemy/sql/roles.py index 2c4ff75c4e..4f9ef1f75c 100644 --- a/lib/sqlalchemy/sql/roles.py +++ b/lib/sqlalchemy/sql/roles.py @@ -36,6 +36,10 @@ class HasCacheKeyRole(SQLRole): _role_name = "Cacheable Core or ORM object" +class LiteralValueRole(SQLRole): + _role_name = "Literal Python value" + + class ColumnArgumentRole(SQLRole): _role_name = "Column expression" diff --git a/test/sql/test_roles.py b/test/sql/test_roles.py index 0ef90e89e9..4d905aca94 100644 --- a/test/sql/test_roles.py +++ b/test/sql/test_roles.py @@ -145,6 +145,19 @@ class RoleTest(fixtures.TestBase): ) ) + def test_no_clauseelement_in_bind(self): + with testing.expect_raises_message( + exc.ArgumentError, + r"Literal Python value expected, got BindParameter", + ): + literal(bindparam("x")) + + with testing.expect_raises_message( + exc.ArgumentError, + r"Literal Python value expected, got .*ColumnClause", + ): + literal(column("q")) + def test_scalar_select_no_coercion(self): with testing.expect_warnings( "implicitly coercing SELECT object to scalar subquery" -- 2.47.2