]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
add informative exception context for literal render
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 14 Nov 2022 13:54:56 +0000 (08:54 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 14 Nov 2022 18:49:38 +0000 (13:49 -0500)
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)

doc/build/changelog/unreleased_14/8800.rst [new file with mode: 0644]
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/sqltypes.py
lib/sqlalchemy/sql/util.py
test/sql/test_compiler.py
test/sql/test_types.py
test/sql/test_values.py

diff --git a/doc/build/changelog/unreleased_14/8800.rst b/doc/build/changelog/unreleased_14/8800.rst
new file mode 100644 (file)
index 0000000..8a42975
--- /dev/null
@@ -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.
index a7232f096d64b21b514178765af4a2b993f67e47..611cd182187bacfb7f850dce242804830adf8a65 100644 (file)
@@ -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):
index 4a988755cd29d807b38328b96e10b7ff6a6a08b9..c2b8bbbe4a00188a6fef098ac9f37aab40221ae3 100644 (file)
@@ -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):
index 019b29e3d1e9d7b4ff4c983f313e7ff57c64f317..1a5143fa5869563fb361c07dd834e46a61fc5de7 100644 (file)
@@ -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."""
 
index 5953c6449e40451630f460869317d6ea6bccfed6..831ef1887203bcfaa37d3bc93b193ae3d74d2bc2 100644 (file)
@@ -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()
index c4f2f27260a769d8589813e0fa2150fba8e6d8eb..4fdbcf95116d55de11c40aa8abe5f881fb7f4f74 100644 (file)
@@ -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},
index dcd32a6791ae30a6ab5dc9268001336824c386ae..1c5e0a1fbb63839579577f20f18dd7024799db23 100644 (file)
@@ -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)