diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-11-14 08:54:56 -0500 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-11-14 10:28:40 -0500 |
commit | eea0f44bbdb759368996dcdb241e837c7c809fb9 (patch) | |
tree | 12ecc66f8183896b833a08c90ebf39799610d816 | |
parent | 9f4ac8d155f58b59cf314cfbc73195ed51a0c146 (diff) | |
download | sqlalchemy-eea0f44bbdb759368996dcdb241e837c7c809fb9.tar.gz |
add informative exception context for literal render
An informative re-raise is now thrown in the case where any "literal
bindparam" render operation fails, indicating the value itself and
the datatype in use, to assist in debugging when literal params
are being rendered in a statement.
Fixes: #8800
Change-Id: Id658f8b03359312353ddbb0c7563026239579f7b
-rw-r--r-- | doc/build/changelog/unreleased_14/8800.rst | 8 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 22 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/sqltypes.py | 7 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/util.py | 6 | ||||
-rw-r--r-- | test/dialect/postgresql/test_types.py | 5 | ||||
-rw-r--r-- | test/sql/test_compiler.py | 46 | ||||
-rw-r--r-- | test/sql/test_types.py | 8 | ||||
-rw-r--r-- | test/sql/test_values.py | 3 |
8 files changed, 89 insertions, 16 deletions
diff --git a/doc/build/changelog/unreleased_14/8800.rst b/doc/build/changelog/unreleased_14/8800.rst new file mode 100644 index 000000000..8a42975df --- /dev/null +++ b/doc/build/changelog/unreleased_14/8800.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: usecase, sql + :tickets: 8800 + + An informative re-raise is now thrown in the case where any "literal + bindparam" render operation fails, indicating the value itself and + the datatype in use, to assist in debugging when literal params + are being rendered in a statement. diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 97397e9cf..9a00afc91 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -63,6 +63,7 @@ from . import roles from . import schema from . import selectable from . import sqltypes +from . import util as sql_util from ._typing import is_column_element from ._typing import is_dml from .base import _from_objects @@ -1530,7 +1531,8 @@ class SQLCompiler(Compiled): replacement_expressions[ escaped_name ] = self.render_literal_bindparam( - parameter, render_literal_value=value + parameter, + render_literal_value=value, ) continue @@ -3154,10 +3156,22 @@ class SQLCompiler(Compiled): processor = type_._cached_literal_processor(self.dialect) if processor: - return processor(value) + try: + return processor(value) + except Exception as e: + raise exc.CompileError( + f"Could not render literal value " + f'"{sql_util._repr_single_value(value)}" ' + f"with datatype " + f"{type_}; see parent stack trace for " + "more detail." + ) from e + else: - raise NotImplementedError( - "Don't know how to literal-quote value %r" % value + raise exc.CompileError( + f"No literal value renderer is available for literal value " + f'"{sql_util._repr_single_value(value)}" ' + f"with datatype {type_}" ) def _truncate_bindparam(self, bindparam): diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 414ff03c3..b98a16b6f 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -3381,12 +3381,7 @@ class NullType(TypeEngine[None]): _isnull = True def literal_processor(self, dialect): - def process(value): - raise exc.CompileError( - "Don't know how to render literal SQL value: %r" % (value,) - ) - - return process + return None class Comparator(TypeEngine.Comparator[_T]): __slots__ = () diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index 55c6a35f8..1f9944529 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -581,6 +581,12 @@ class _repr_base: return rep +def _repr_single_value(value): + rp = _repr_base() + rp.max_chars = 300 + return rp.trunc(value) + + class _repr_row(_repr_base): """Provide a string view of a row.""" diff --git a/test/dialect/postgresql/test_types.py b/test/dialect/postgresql/test_types.py index 83cea8f15..39e7d7317 100644 --- a/test/dialect/postgresql/test_types.py +++ b/test/dialect/postgresql/test_types.py @@ -1528,8 +1528,9 @@ class ArrayTest(AssertsCompiledSQL, fixtures.TestBase): return "MYTYPE" with expect_raises_message( - NotImplementedError, - r"Don't know how to literal-quote value \[1, 2, 3\]", + exc.CompileError, + r"No literal value renderer is available for literal " + r"value \"\[1, 2, 3\]\" with datatype ARRAY", ): self.assert_compile( select(literal([1, 2, 3], ARRAY(MyType()))), diff --git a/test/sql/test_compiler.py b/test/sql/test_compiler.py index 97b1b9124..4eea11795 100644 --- a/test/sql/test_compiler.py +++ b/test/sql/test_compiler.py @@ -98,6 +98,7 @@ from sqlalchemy.testing import is_true from sqlalchemy.testing import mock from sqlalchemy.testing import ne_ from sqlalchemy.testing.schema import pep435_enum +from sqlalchemy.types import UserDefinedType table1 = table( "mytable", @@ -4609,6 +4610,51 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): "OR mytable.myid = :myid_2 OR mytable.myid = :myid_3", ) + @testing.combinations("plain", "expanding", argnames="exprtype") + def test_literal_bind_typeerror(self, exprtype): + """test #8800""" + + if exprtype == "expanding": + stmt = select(table1).where( + table1.c.myid.in_([("tuple",), ("tuple",)]) + ) + elif exprtype == "plain": + stmt = select(table1).where(table1.c.myid == ("tuple",)) + else: + assert False + + with expect_raises_message( + exc.CompileError, + r"Could not render literal value \"\(\'tuple\',\)\" " + r"with datatype INTEGER; see parent " + r"stack trace for more detail.", + ): + stmt.compile(compile_kwargs={"literal_binds": True}) + + @testing.combinations("plain", "expanding", argnames="exprtype") + def test_literal_bind_dont_know_how_to_quote(self, exprtype): + """test #8800""" + + class MyType(UserDefinedType): + def get_col_spec(self, **kw): + return "MYTYPE" + + col = column("x", MyType()) + + if exprtype == "expanding": + stmt = select(table1).where(col.in_([("tuple",), ("tuple",)])) + elif exprtype == "plain": + stmt = select(table1).where(col == ("tuple",)) + else: + assert False + + with expect_raises_message( + exc.CompileError, + r"No literal value renderer is available for literal " + r"value \"\('tuple',\)\" with datatype MYTYPE", + ): + stmt.compile(compile_kwargs={"literal_binds": True}) + @testing.fixture def ansi_compiler_fixture(self): dialect = default.DefaultDialect() diff --git a/test/sql/test_types.py b/test/sql/test_types.py index a608d0040..3b1df3498 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -3125,8 +3125,9 @@ class ArrayTest(AssertsCompiledSQL, fixtures.TestBase): return "MYTYPE" with expect_raises_message( - NotImplementedError, - r"Don't know how to literal-quote value \[1, 2, 3\]", + exc.CompileError, + r"No literal value renderer is available for literal value " + r"\"\[1, 2, 3\]\" with datatype ARRAY", ): self.assert_compile( select(literal([1, 2, 3], ARRAY(MyType()))), @@ -3629,7 +3630,8 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL): def test_compile_err_formatting(self): with expect_raises_message( exc.CompileError, - r"Don't know how to render literal SQL value: \(1, 2, 3\)", + r"No literal value renderer is available for literal " + r"value \"\(1, 2, 3\)\" with datatype NULL", ): func.foo((1, 2, 3)).compile(compile_kwargs={"literal_binds": True}) diff --git a/test/sql/test_values.py b/test/sql/test_values.py index d14de9aee..b943c4701 100644 --- a/test/sql/test_values.py +++ b/test/sql/test_values.py @@ -277,7 +277,8 @@ class ValuesTest(fixtures.TablesTest, AssertsCompiledSQL): with expect_raises_message( exc.CompileError, - "Don't know how to render literal SQL value: 'textA'", + r"No literal value renderer is available for literal " + r"value \"'textA'\" with datatype NULL", ): str(stmt) |