From: Mike Bayer Date: Wed, 31 Dec 2025 20:48:44 +0000 (-0500) Subject: Fixed JSONB path_match and path_exists operators to use correct type coercion X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=39d31c5ea095577147a474be89e5d754a67aa514;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Fixed JSONB path_match and path_exists operators to use correct type coercion Fixed issue where PostgreSQL JSONB operators :meth:`_postgresql.JSONB.Comparator.path_match` and :meth:`_postgresql.JSONB.Comparator.path_exists` were applying incorrect ``VARCHAR`` casts to the right-hand side operand when used with newer PostgreSQL drivers such as psycopg. The operators now indicate the right-hand type as ``JSONPATH``, which currently results in no casting taking place, but is also compatible with explicit casts if the implementation were require it at a later point. Fixes: #13059 Change-Id: I8e1a58361456f7efabf4940339cb5ce2c5a1d5f9 --- diff --git a/doc/build/changelog/unreleased_20/13059.rst b/doc/build/changelog/unreleased_20/13059.rst new file mode 100644 index 0000000000..caeca4a7c3 --- /dev/null +++ b/doc/build/changelog/unreleased_20/13059.rst @@ -0,0 +1,14 @@ +.. change:: + :tags: bug, postgresql + :tickets: 13059 + + Fixed issue where PostgreSQL JSONB operators + :meth:`_postgresql.JSONB.Comparator.path_match` and + :meth:`_postgresql.JSONB.Comparator.path_exists` were applying incorrect + ``VARCHAR`` casts to the right-hand side operand when used with newer + PostgreSQL drivers such as psycopg. The operators now indicate the + right-hand type as ``JSONPATH``, which currently results in no casting + taking place, but is also compatible with explicit casts if the + implementation were require it at a later point. + + diff --git a/lib/sqlalchemy/dialects/postgresql/json.py b/lib/sqlalchemy/dialects/postgresql/json.py index 88ced21ce5..f4d8e9fa54 100644 --- a/lib/sqlalchemy/dialects/postgresql/json.py +++ b/lib/sqlalchemy/dialects/postgresql/json.py @@ -34,6 +34,7 @@ from ...sql.operators import OperatorClass if TYPE_CHECKING: from ...engine.interfaces import Dialect from ...sql.elements import ColumnElement + from ...sql.operators import OperatorType from ...sql.type_api import _BindProcessorType from ...sql.type_api import _LiteralProcessorType from ...sql.type_api import TypeEngine @@ -314,6 +315,14 @@ class JSONB(JSON): operator_classes = OperatorClass.JSON | OperatorClass.CONCATENABLE + def coerce_compared_value( + self, op: Optional[OperatorType], value: Any + ) -> TypeEngine[Any]: + if op in (PATH_MATCH, PATH_EXISTS): + return JSON.JSONPathType() + else: + return super().coerce_compared_value(op, value) + class Comparator(JSON.Comparator[_T]): """Define comparison operations for :class:`_types.JSON`.""" diff --git a/test/dialect/postgresql/test_types.py b/test/dialect/postgresql/test_types.py index 62966247c5..2f5df25d87 100644 --- a/test/dialect/postgresql/test_types.py +++ b/test/dialect/postgresql/test_types.py @@ -6891,7 +6891,7 @@ class JSONBTest(JSONTest): ), ( lambda self: self.jsoncol.path_exists("$.k1"), - "test_table.test_column @? %(test_column_1)s::VARCHAR", + "test_table.test_column @? %(test_column_1)s", ), ( lambda self: self.jsoncol.path_exists(self.any_), @@ -6899,7 +6899,7 @@ class JSONBTest(JSONTest): ), ( lambda self: self.jsoncol.path_match("$.k1[0] > 2"), - "test_table.test_column @@ %(test_column_1)s::VARCHAR", + "test_table.test_column @@ %(test_column_1)s", ), ( lambda self: self.jsoncol.path_match(self.any_), @@ -7022,6 +7022,50 @@ class JSONBRoundTripTest(JSONRoundTripTest): res = connection.scalar(q) eq_(res, {"k1": {"r6v1": {"subr": [1, 3]}}}) + @testing.only_on("postgresql >= 12") + def test_path_exists(self, connection): + self._fixture_data(connection) + + q = select(self.data_table.c.name).where( + self.data_table.c.data.path_exists("$.k1") + ) + res = connection.scalars(q).all() + eq_(set(res), {"r1", "r2", "r3", "r4", "r5", "r6"}) + + q = select(self.data_table.c.name).where( + self.data_table.c.data.path_exists("$.k3") + ) + res = connection.scalars(q).all() + eq_(res, ["r5"]) + + q = select(self.data_table.c.name).where( + self.data_table.c.data.path_exists("$.k1.r6v1") + ) + res = connection.scalars(q).all() + eq_(res, ["r6"]) + + @testing.only_on("postgresql >= 12") + def test_path_match(self, connection): + self._fixture_data(connection) + + q = select(self.data_table.c.name).where( + self.data_table.c.data.path_match("$.k3 > 0") + ) + res = connection.scalars(q).all() + eq_(res, ["r5"]) + + q = select(self.data_table.c.name).where( + self.data_table.c.data.path_match("$.k3 == 5") + ) + res = connection.scalars(q).all() + eq_(res, ["r5"]) + + q = select(self.data_table.c.name).where( + self.data_table.c.data.path_match('$.k1 == "r1v1"') + ) + res = connection.scalars(q).all() + eq_(res, ["r1"]) + class JSONBSuiteTest(suite.JSONTest): __requires__ = ("postgresql_jsonb",)