json.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. # dialects/postgresql/json.py
  2. # Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. from __future__ import annotations
  8. from typing import Any
  9. from typing import Callable
  10. from typing import List
  11. from typing import Optional
  12. from typing import TYPE_CHECKING
  13. from typing import Union
  14. from .array import ARRAY
  15. from .array import array as _pg_array
  16. from .operators import ASTEXT
  17. from .operators import CONTAINED_BY
  18. from .operators import CONTAINS
  19. from .operators import DELETE_PATH
  20. from .operators import HAS_ALL
  21. from .operators import HAS_ANY
  22. from .operators import HAS_KEY
  23. from .operators import JSONPATH_ASTEXT
  24. from .operators import PATH_EXISTS
  25. from .operators import PATH_MATCH
  26. from ... import types as sqltypes
  27. from ...sql import cast
  28. from ...sql._typing import _T
  29. if TYPE_CHECKING:
  30. from ...engine.interfaces import Dialect
  31. from ...sql.elements import ColumnElement
  32. from ...sql.type_api import _BindProcessorType
  33. from ...sql.type_api import _LiteralProcessorType
  34. from ...sql.type_api import TypeEngine
  35. __all__ = ("JSON", "JSONB")
  36. class JSONPathType(sqltypes.JSON.JSONPathType):
  37. def _processor(
  38. self, dialect: Dialect, super_proc: Optional[Callable[[Any], Any]]
  39. ) -> Callable[[Any], Any]:
  40. def process(value: Any) -> Any:
  41. if isinstance(value, str):
  42. # If it's already a string assume that it's in json path
  43. # format. This allows using cast with json paths literals
  44. return value
  45. elif value:
  46. # If it's already a string assume that it's in json path
  47. # format. This allows using cast with json paths literals
  48. value = "{%s}" % (", ".join(map(str, value)))
  49. else:
  50. value = "{}"
  51. if super_proc:
  52. value = super_proc(value)
  53. return value
  54. return process
  55. def bind_processor(self, dialect: Dialect) -> _BindProcessorType[Any]:
  56. return self._processor(dialect, self.string_bind_processor(dialect)) # type: ignore[return-value] # noqa: E501
  57. def literal_processor(
  58. self, dialect: Dialect
  59. ) -> _LiteralProcessorType[Any]:
  60. return self._processor(dialect, self.string_literal_processor(dialect)) # type: ignore[return-value] # noqa: E501
  61. class JSONPATH(JSONPathType):
  62. """JSON Path Type.
  63. This is usually required to cast literal values to json path when using
  64. json search like function, such as ``jsonb_path_query_array`` or
  65. ``jsonb_path_exists``::
  66. stmt = sa.select(
  67. sa.func.jsonb_path_query_array(
  68. table.c.jsonb_col, cast("$.address.id", JSONPATH)
  69. )
  70. )
  71. """
  72. __visit_name__ = "JSONPATH"
  73. class JSON(sqltypes.JSON):
  74. """Represent the PostgreSQL JSON type.
  75. :class:`_postgresql.JSON` is used automatically whenever the base
  76. :class:`_types.JSON` datatype is used against a PostgreSQL backend,
  77. however base :class:`_types.JSON` datatype does not provide Python
  78. accessors for PostgreSQL-specific comparison methods such as
  79. :meth:`_postgresql.JSON.Comparator.astext`; additionally, to use
  80. PostgreSQL ``JSONB``, the :class:`_postgresql.JSONB` datatype should
  81. be used explicitly.
  82. .. seealso::
  83. :class:`_types.JSON` - main documentation for the generic
  84. cross-platform JSON datatype.
  85. The operators provided by the PostgreSQL version of :class:`_types.JSON`
  86. include:
  87. * Index operations (the ``->`` operator)::
  88. data_table.c.data["some key"]
  89. data_table.c.data[5]
  90. * Index operations returning text
  91. (the ``->>`` operator)::
  92. data_table.c.data["some key"].astext == "some value"
  93. Note that equivalent functionality is available via the
  94. :attr:`.JSON.Comparator.as_string` accessor.
  95. * Index operations with CAST
  96. (equivalent to ``CAST(col ->> ['some key'] AS <type>)``)::
  97. data_table.c.data["some key"].astext.cast(Integer) == 5
  98. Note that equivalent functionality is available via the
  99. :attr:`.JSON.Comparator.as_integer` and similar accessors.
  100. * Path index operations (the ``#>`` operator)::
  101. data_table.c.data[("key_1", "key_2", 5, ..., "key_n")]
  102. * Path index operations returning text (the ``#>>`` operator)::
  103. data_table.c.data[
  104. ("key_1", "key_2", 5, ..., "key_n")
  105. ].astext == "some value"
  106. Index operations return an expression object whose type defaults to
  107. :class:`_types.JSON` by default,
  108. so that further JSON-oriented instructions
  109. may be called upon the result type.
  110. Custom serializers and deserializers are specified at the dialect level,
  111. that is using :func:`_sa.create_engine`. The reason for this is that when
  112. using psycopg2, the DBAPI only allows serializers at the per-cursor
  113. or per-connection level. E.g.::
  114. engine = create_engine(
  115. "postgresql+psycopg2://scott:tiger@localhost/test",
  116. json_serializer=my_serialize_fn,
  117. json_deserializer=my_deserialize_fn,
  118. )
  119. When using the psycopg2 dialect, the json_deserializer is registered
  120. against the database using ``psycopg2.extras.register_default_json``.
  121. .. seealso::
  122. :class:`_types.JSON` - Core level JSON type
  123. :class:`_postgresql.JSONB`
  124. """ # noqa
  125. render_bind_cast = True
  126. astext_type: TypeEngine[str] = sqltypes.Text()
  127. def __init__(
  128. self,
  129. none_as_null: bool = False,
  130. astext_type: Optional[TypeEngine[str]] = None,
  131. ):
  132. """Construct a :class:`_types.JSON` type.
  133. :param none_as_null: if True, persist the value ``None`` as a
  134. SQL NULL value, not the JSON encoding of ``null``. Note that
  135. when this flag is False, the :func:`.null` construct can still
  136. be used to persist a NULL value::
  137. from sqlalchemy import null
  138. conn.execute(table.insert(), {"data": null()})
  139. .. seealso::
  140. :attr:`_types.JSON.NULL`
  141. :param astext_type: the type to use for the
  142. :attr:`.JSON.Comparator.astext`
  143. accessor on indexed attributes. Defaults to :class:`_types.Text`.
  144. """
  145. super().__init__(none_as_null=none_as_null)
  146. if astext_type is not None:
  147. self.astext_type = astext_type
  148. class Comparator(sqltypes.JSON.Comparator[_T]):
  149. """Define comparison operations for :class:`_types.JSON`."""
  150. type: JSON
  151. @property
  152. def astext(self) -> ColumnElement[str]:
  153. """On an indexed expression, use the "astext" (e.g. "->>")
  154. conversion when rendered in SQL.
  155. E.g.::
  156. select(data_table.c.data["some key"].astext)
  157. .. seealso::
  158. :meth:`_expression.ColumnElement.cast`
  159. """
  160. if isinstance(self.expr.right.type, sqltypes.JSON.JSONPathType):
  161. return self.expr.left.operate( # type: ignore[no-any-return]
  162. JSONPATH_ASTEXT,
  163. self.expr.right,
  164. result_type=self.type.astext_type,
  165. )
  166. else:
  167. return self.expr.left.operate( # type: ignore[no-any-return]
  168. ASTEXT, self.expr.right, result_type=self.type.astext_type
  169. )
  170. comparator_factory = Comparator
  171. class JSONB(JSON):
  172. """Represent the PostgreSQL JSONB type.
  173. The :class:`_postgresql.JSONB` type stores arbitrary JSONB format data,
  174. e.g.::
  175. data_table = Table(
  176. "data_table",
  177. metadata,
  178. Column("id", Integer, primary_key=True),
  179. Column("data", JSONB),
  180. )
  181. with engine.connect() as conn:
  182. conn.execute(
  183. data_table.insert(), data={"key1": "value1", "key2": "value2"}
  184. )
  185. The :class:`_postgresql.JSONB` type includes all operations provided by
  186. :class:`_types.JSON`, including the same behaviors for indexing
  187. operations.
  188. It also adds additional operators specific to JSONB, including
  189. :meth:`.JSONB.Comparator.has_key`, :meth:`.JSONB.Comparator.has_all`,
  190. :meth:`.JSONB.Comparator.has_any`, :meth:`.JSONB.Comparator.contains`,
  191. :meth:`.JSONB.Comparator.contained_by`,
  192. :meth:`.JSONB.Comparator.delete_path`,
  193. :meth:`.JSONB.Comparator.path_exists` and
  194. :meth:`.JSONB.Comparator.path_match`.
  195. Like the :class:`_types.JSON` type, the :class:`_postgresql.JSONB`
  196. type does not detect
  197. in-place changes when used with the ORM, unless the
  198. :mod:`sqlalchemy.ext.mutable` extension is used.
  199. Custom serializers and deserializers
  200. are shared with the :class:`_types.JSON` class,
  201. using the ``json_serializer``
  202. and ``json_deserializer`` keyword arguments. These must be specified
  203. at the dialect level using :func:`_sa.create_engine`. When using
  204. psycopg2, the serializers are associated with the jsonb type using
  205. ``psycopg2.extras.register_default_jsonb`` on a per-connection basis,
  206. in the same way that ``psycopg2.extras.register_default_json`` is used
  207. to register these handlers with the json type.
  208. .. seealso::
  209. :class:`_types.JSON`
  210. """
  211. __visit_name__ = "JSONB"
  212. class Comparator(JSON.Comparator[_T]):
  213. """Define comparison operations for :class:`_types.JSON`."""
  214. type: JSONB
  215. def has_key(self, other: Any) -> ColumnElement[bool]:
  216. """Boolean expression. Test for presence of a key (equivalent of
  217. the ``?`` operator). Note that the key may be a SQLA expression.
  218. """
  219. return self.operate(HAS_KEY, other, result_type=sqltypes.Boolean)
  220. def has_all(self, other: Any) -> ColumnElement[bool]:
  221. """Boolean expression. Test for presence of all keys in jsonb
  222. (equivalent of the ``?&`` operator)
  223. """
  224. return self.operate(HAS_ALL, other, result_type=sqltypes.Boolean)
  225. def has_any(self, other: Any) -> ColumnElement[bool]:
  226. """Boolean expression. Test for presence of any key in jsonb
  227. (equivalent of the ``?|`` operator)
  228. """
  229. return self.operate(HAS_ANY, other, result_type=sqltypes.Boolean)
  230. def contains(self, other: Any, **kwargs: Any) -> ColumnElement[bool]:
  231. """Boolean expression. Test if keys (or array) are a superset
  232. of/contained the keys of the argument jsonb expression
  233. (equivalent of the ``@>`` operator).
  234. kwargs may be ignored by this operator but are required for API
  235. conformance.
  236. """
  237. return self.operate(CONTAINS, other, result_type=sqltypes.Boolean)
  238. def contained_by(self, other: Any) -> ColumnElement[bool]:
  239. """Boolean expression. Test if keys are a proper subset of the
  240. keys of the argument jsonb expression
  241. (equivalent of the ``<@`` operator).
  242. """
  243. return self.operate(
  244. CONTAINED_BY, other, result_type=sqltypes.Boolean
  245. )
  246. def delete_path(
  247. self, array: Union[List[str], _pg_array[str]]
  248. ) -> ColumnElement[JSONB]:
  249. """JSONB expression. Deletes field or array element specified in
  250. the argument array (equivalent of the ``#-`` operator).
  251. The input may be a list of strings that will be coerced to an
  252. ``ARRAY`` or an instance of :meth:`_postgres.array`.
  253. .. versionadded:: 2.0
  254. """
  255. if not isinstance(array, _pg_array):
  256. array = _pg_array(array)
  257. right_side = cast(array, ARRAY(sqltypes.TEXT))
  258. return self.operate(DELETE_PATH, right_side, result_type=JSONB)
  259. def path_exists(self, other: Any) -> ColumnElement[bool]:
  260. """Boolean expression. Test for presence of item given by the
  261. argument JSONPath expression (equivalent of the ``@?`` operator).
  262. .. versionadded:: 2.0
  263. """
  264. return self.operate(
  265. PATH_EXISTS, other, result_type=sqltypes.Boolean
  266. )
  267. def path_match(self, other: Any) -> ColumnElement[bool]:
  268. """Boolean expression. Test if JSONPath predicate given by the
  269. argument JSONPath expression matches
  270. (equivalent of the ``@@`` operator).
  271. Only the first item of the result is taken into account.
  272. .. versionadded:: 2.0
  273. """
  274. return self.operate(
  275. PATH_MATCH, other, result_type=sqltypes.Boolean
  276. )
  277. comparator_factory = Comparator