]> 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 15:28:40 +0000 (10:28 -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

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/dialect/postgresql/test_types.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 97397e9cf4337bf6f1d385a607246ee6ec29f814..9a00afc91cef150733b49bb40e0e04f8627610b4 100644 (file)
@@ -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):
index 414ff03c3e0fb8da6bc00ec0155e17a5df43f287..b98a16b6fb30835bb6ce02ec49d7fd76d3c0ecd5 100644 (file)
@@ -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__ = ()
index 55c6a35f8879ed5742d7c82736bb259cb6b18c36..1f9944529d749981850d0ec6059f2749a5e8aadf 100644 (file)
@@ -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."""
 
index 83cea8f15a2f5c79ac3b27c13971ab3c9f315273..39e7d731723f60693f7a0b1f81620cd7e21a35e1 100644 (file)
@@ -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()))),
index 97b1b9124363eacd9a0618b7d06a4d689efcc1da..4eea117957dbdddb49adae5fbff3b7446d6ac40a 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
 
 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()
index a608d0040b6697d30f1a2e742eea1b4ee917b55a..3b1df34987a773a7b19e55caaa448aa831c2fef3 100644 (file)
@@ -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})
 
index d14de9aeeda009acfd9ca7e32f3ffab74799e865..b943c4701ded21f12dc31b93d4db366fce0acd00 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)