diff options
| author | mike bayer <mike_mp@zzzcomputing.com> | 2022-08-17 17:11:50 +0000 |
|---|---|---|
| committer | Gerrit Code Review <gerrit@ci3.zzzcomputing.com> | 2022-08-17 17:11:50 +0000 |
| commit | a7e8cb1d71615e0478c666f9f8651d38379a6055 (patch) | |
| tree | dea8e91ff6509c8c85c20bb8ba623a7bd846c1bd /lib/sqlalchemy | |
| parent | d305f0130dd1cf77f8ffe9f22e63dd770630ec81 (diff) | |
| parent | 2e7117ab1b584e678380c70625ad1331cea551d0 (diff) | |
| download | sqlalchemy-a7e8cb1d71615e0478c666f9f8651d38379a6055.tar.gz | |
Merge "JSONPATH type can be used in casts in PostgreSQL" into main
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/dialects/postgresql/__init__.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/postgresql/asyncpg.py | 13 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/postgresql/base.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/postgresql/json.py | 47 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/sqltypes.py | 2 |
5 files changed, 51 insertions, 21 deletions
diff --git a/lib/sqlalchemy/dialects/postgresql/__init__.py b/lib/sqlalchemy/dialects/postgresql/__init__.py index 104077a17..8dbee1f7f 100644 --- a/lib/sqlalchemy/dialects/postgresql/__init__.py +++ b/lib/sqlalchemy/dialects/postgresql/__init__.py @@ -41,6 +41,7 @@ from .hstore import HSTORE from .hstore import hstore from .json import JSON from .json import JSONB +from .json import JSONPATH from .named_types import CreateDomainType from .named_types import CreateEnumType from .named_types import DropDomainType @@ -128,6 +129,7 @@ __all__ = ( "TSTZMULTIRANGE", "JSON", "JSONB", + "JSONPATH", "Any", "All", "DropEnumType", diff --git a/lib/sqlalchemy/dialects/postgresql/asyncpg.py b/lib/sqlalchemy/dialects/postgresql/asyncpg.py index 38f8fddee..6888959f0 100644 --- a/lib/sqlalchemy/dialects/postgresql/asyncpg.py +++ b/lib/sqlalchemy/dialects/postgresql/asyncpg.py @@ -122,7 +122,6 @@ client using this setting passed to :func:`_asyncio.create_async_engine`:: from __future__ import annotations import collections -import collections.abc as collections_abc import decimal import json as _py_json import re @@ -231,9 +230,15 @@ class AsyncpgJSONStrIndexType(sqltypes.JSON.JSONStrIndexType): class AsyncpgJSONPathType(json.JSONPathType): def bind_processor(self, dialect): def process(value): - assert isinstance(value, collections_abc.Sequence) - tokens = [str(elem) for elem in value] - return tokens + if isinstance(value, str): + # If it's already a string assume that it's in json path + # format. This allows using cast with json paths literals + return value + elif value: + tokens = [str(elem) for elem in value] + return tokens + else: + return [] return process diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 35a09cfa7..75fa3d7c7 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1512,7 +1512,7 @@ colspecs = { sqltypes.ARRAY: _array.ARRAY, sqltypes.Interval: INTERVAL, sqltypes.Enum: ENUM, - sqltypes.JSON.JSONPathType: _json.JSONPathType, + sqltypes.JSON.JSONPathType: _json.JSONPATH, sqltypes.JSON: _json.JSON, UUID: PGUuid, } @@ -2503,6 +2503,12 @@ class PGTypeCompiler(compiler.GenericTypeCompiler): count=1, ) + def visit_json_path(self, type_, **kw): + return self.visit_JSONPATH(type_, **kw) + + def visit_JSONPATH(self, type_, **kw): + return "JSONPATH" + class PGIdentifierPreparer(compiler.IdentifierPreparer): diff --git a/lib/sqlalchemy/dialects/postgresql/json.py b/lib/sqlalchemy/dialects/postgresql/json.py index 8763a0ca2..a8b03bd48 100644 --- a/lib/sqlalchemy/dialects/postgresql/json.py +++ b/lib/sqlalchemy/dialects/postgresql/json.py @@ -6,7 +6,6 @@ # the MIT License: https://www.opensource.org/licenses/mit-license.php # mypy: ignore-errors -import collections.abc as collections_abc from ... import types as sqltypes from ...sql import operators @@ -68,31 +67,47 @@ CONTAINED_BY = operators.custom_op( class JSONPathType(sqltypes.JSON.JSONPathType): - def bind_processor(self, dialect): - super_proc = self.string_bind_processor(dialect) - + def _processor(self, dialect, super_proc): def process(value): - assert isinstance(value, collections_abc.Sequence) - tokens = [str(elem) for elem in value] - value = "{%s}" % (", ".join(tokens)) + if isinstance(value, str): + # If it's already a string assume that it's in json path + # format. This allows using cast with json paths literals + return value + elif value: + # If it's already a string assume that it's in json path + # format. This allows using cast with json paths literals + value = "{%s}" % (", ".join(map(str, value))) + else: + value = "{}" if super_proc: value = super_proc(value) return value return process + def bind_processor(self, dialect): + return self._processor(dialect, self.string_bind_processor(dialect)) + def literal_processor(self, dialect): - super_proc = self.string_literal_processor(dialect) + return self._processor(dialect, self.string_literal_processor(dialect)) - def process(value): - assert isinstance(value, collections_abc.Sequence) - tokens = [str(elem) for elem in value] - value = "{%s}" % (", ".join(tokens)) - if super_proc: - value = super_proc(value) - return value - return process +class JSONPATH(JSONPathType): + """JSON Path Type. + + This is usually required to cast literal values to json path when using + json search like function, such as ``jsonb_path_query_array`` or + ``jsonb_path_exists``:: + + stmt = sa.select( + sa.func.jsonb_path_query_array( + table.c.jsonb_col, cast("$.address.id", JSONPATH) + ) + ) + + """ + + __visit_name__ = "JSONPATH" class JSON(sqltypes.JSON): diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index de833cd89..f04d583e1 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -2419,6 +2419,8 @@ class JSON(Indexable, TypeEngine[Any]): """ + __visit_name__ = "json_path" + class Comparator(Indexable.Comparator[_T], Concatenable.Comparator[_T]): """Define comparison operations for :class:`_types.JSON`.""" |
