summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/unreleased_14/8800.rst8
-rw-r--r--lib/sqlalchemy/sql/compiler.py22
-rw-r--r--lib/sqlalchemy/sql/sqltypes.py7
-rw-r--r--lib/sqlalchemy/sql/util.py6
-rw-r--r--test/dialect/postgresql/test_types.py5
-rw-r--r--test/sql/test_compiler.py46
-rw-r--r--test/sql/test_types.py8
-rw-r--r--test/sql/test_values.py3
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)