]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Fixed JSONB path_match and path_exists operators to use correct type coercion
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 31 Dec 2025 20:48:44 +0000 (15:48 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 1 Jan 2026 16:48:51 +0000 (11:48 -0500)
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

doc/build/changelog/unreleased_20/13059.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/postgresql/json.py
test/dialect/postgresql/test_types.py

diff --git a/doc/build/changelog/unreleased_20/13059.rst b/doc/build/changelog/unreleased_20/13059.rst
new file mode 100644 (file)
index 0000000..caeca4a
--- /dev/null
@@ -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.
+
+
index 88ced21ce5260623e4d02fe4e0c9298e7eb2ab41..f4d8e9fa543d60ad7cbf7a03c78d700d5d275a52 100644 (file)
@@ -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`."""
 
index 62966247c5aa6cb4c815340505a9db59238a37ad..2f5df25d87b4d57fb5a659f85ef3588b8bd4e390 100644 (file)
@@ -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",)