From: Mike Bayer Date: Mon, 14 Nov 2022 13:54:56 +0000 (-0500) Subject: add informative exception context for literal render X-Git-Tag: rel_1_4_45~32 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3ea8273c7926d19d4745bf5a859927838fa0783c;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git 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 (cherry picked from commit c7baf6e0aa624c9378c3bc3c4923d1e188d62dc9) --- diff --git a/doc/build/changelog/unreleased_14/8800.rst b/doc/build/changelog/unreleased_14/8800.rst new file mode 100644 index 0000000000..8a42975df7 --- /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 a7232f096d..611cd18218 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -38,6 +38,7 @@ from . import operators from . import schema from . import selectable from . import sqltypes +from . import util as sql_util from .base import NO_ARG from .base import prefix_anon_map from .elements import quoted_name @@ -1216,7 +1217,8 @@ class SQLCompiler(Compiled): replacement_expressions[ escaped_name ] = self.render_literal_bindparam( - parameter, render_literal_value=value + parameter, + render_literal_value=value, ) continue @@ -2590,10 +2592,29 @@ class SQLCompiler(Compiled): processor = type_._cached_literal_processor(self.dialect) if processor: - return processor(value) + try: + return processor(value) + except Exception as e: + util.raise_( + exc.CompileError( + "Could not render literal value " + '"%s" ' + "with datatype " + "%s; see parent stack trace for " + "more detail." + % ( + sql_util._repr_single_value(value), + type_, + ) + ), + from_=e, + ) + else: - raise NotImplementedError( - "Don't know how to literal-quote value %r" % value + raise exc.CompileError( + "No literal value renderer is available for literal value " + '"%s" with datatype %s' + % (sql_util._repr_single_value(value), type_) ) def _truncate_bindparam(self, bindparam): diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 4a988755cd..c2b8bbbe4a 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -3262,12 +3262,7 @@ class NullType(TypeEngine): _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): def _adapt_expression(self, op, other_comparator): diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index 019b29e3d1..1a5143fa58 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -484,6 +484,12 @@ class _repr_base(object): 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/sql/test_compiler.py b/test/sql/test_compiler.py index 5953c6449e..831ef18872 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 from sqlalchemy.util import u table1 = table( @@ -4519,6 +4520,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 c4f2f27260..4fdbcf9511 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -3615,7 +3615,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}) @@ -4230,8 +4231,8 @@ class LiteralTest(fixtures.TestBase): lit = literal(value) assert_raises_message( - NotImplementedError, - "Don't know how to literal-quote value.*", + exc.CompileError, + r"No literal value renderer is available for literal value.*", lit.compile, dialect=testing.db.dialect, compile_kwargs={"literal_binds": True}, diff --git a/test/sql/test_values.py b/test/sql/test_values.py index dcd32a6791..1c5e0a1fbb 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)