Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/json.py: 56%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

94 statements  

1# dialects/postgresql/json.py 

2# Copyright (C) 2005-2026 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 

8from __future__ import annotations 

9 

10from typing import Any 

11from typing import Callable 

12from typing import List 

13from typing import Optional 

14from typing import TYPE_CHECKING 

15from typing import Union 

16 

17from .array import ARRAY 

18from .array import array as _pg_array 

19from .operators import ASTEXT 

20from .operators import CONTAINED_BY 

21from .operators import CONTAINS 

22from .operators import DELETE_PATH 

23from .operators import HAS_ALL 

24from .operators import HAS_ANY 

25from .operators import HAS_KEY 

26from .operators import JSONPATH_ASTEXT 

27from .operators import PATH_EXISTS 

28from .operators import PATH_MATCH 

29from ... import types as sqltypes 

30from ...sql import cast 

31from ...sql._typing import _T 

32 

33if TYPE_CHECKING: 

34 from ...engine.interfaces import Dialect 

35 from ...sql.elements import ColumnElement 

36 from ...sql.operators import OperatorType 

37 from ...sql.type_api import _BindProcessorType 

38 from ...sql.type_api import _LiteralProcessorType 

39 from ...sql.type_api import TypeEngine 

40 

41__all__ = ("JSON", "JSONB") 

42 

43 

44class JSONPathType(sqltypes.JSON.JSONPathType): 

45 def _processor( 

46 self, dialect: Dialect, super_proc: Optional[Callable[[Any], Any]] 

47 ) -> Callable[[Any], Any]: 

48 def process(value: Any) -> Any: 

49 if isinstance(value, str): 

50 # If it's already a string assume that it's in json path 

51 # format. This allows using cast with json paths literals 

52 # Still need to process through super_proc for proper escaping 

53 if super_proc: 

54 value = super_proc(value) 

55 return value 

56 elif value: 

57 # If it's already a string assume that it's in json path 

58 # format. This allows using cast with json paths literals 

59 value = "{%s}" % (", ".join(map(str, value))) 

60 else: 

61 value = "{}" 

62 if super_proc: 

63 value = super_proc(value) 

64 return value 

65 

66 return process 

67 

68 def bind_processor(self, dialect: Dialect) -> _BindProcessorType[Any]: 

69 return self._processor(dialect, self.string_bind_processor(dialect)) # type: ignore[return-value] # noqa: E501 

70 

71 def literal_processor( 

72 self, dialect: Dialect 

73 ) -> _LiteralProcessorType[Any]: 

74 return self._processor(dialect, self.string_literal_processor(dialect)) # type: ignore[return-value] # noqa: E501 

75 

76 

77class JSONPATH(JSONPathType): 

78 """JSON Path Type. 

79 

80 This is usually required to cast literal values to json path when using 

81 json search like function, such as ``jsonb_path_query_array`` or 

82 ``jsonb_path_exists``:: 

83 

84 stmt = sa.select( 

85 sa.func.jsonb_path_query_array( 

86 table.c.jsonb_col, cast("$.address.id", JSONPATH) 

87 ) 

88 ) 

89 

90 """ 

91 

92 __visit_name__ = "JSONPATH" 

93 

94 

95class JSON(sqltypes.JSON): 

96 """Represent the PostgreSQL JSON type. 

97 

98 :class:`_postgresql.JSON` is used automatically whenever the base 

99 :class:`_types.JSON` datatype is used against a PostgreSQL backend, 

100 however base :class:`_types.JSON` datatype does not provide Python 

101 accessors for PostgreSQL-specific comparison methods such as 

102 :meth:`_postgresql.JSON.Comparator.astext`; additionally, to use 

103 PostgreSQL ``JSONB``, the :class:`_postgresql.JSONB` datatype should 

104 be used explicitly. 

105 

106 .. seealso:: 

107 

108 :class:`_types.JSON` - main documentation for the generic 

109 cross-platform JSON datatype. 

110 

111 The operators provided by the PostgreSQL version of :class:`_types.JSON` 

112 include: 

113 

114 * Index operations (the ``->`` operator):: 

115 

116 data_table.c.data["some key"] 

117 

118 data_table.c.data[5] 

119 

120 * Index operations returning text 

121 (the ``->>`` operator):: 

122 

123 data_table.c.data["some key"].astext == "some value" 

124 

125 Note that equivalent functionality is available via the 

126 :attr:`.JSON.Comparator.as_string` accessor. 

127 

128 * Index operations with CAST 

129 (equivalent to ``CAST(col ->> ['some key'] AS <type>)``):: 

130 

131 data_table.c.data["some key"].astext.cast(Integer) == 5 

132 

133 Note that equivalent functionality is available via the 

134 :attr:`.JSON.Comparator.as_integer` and similar accessors. 

135 

136 * Path index operations (the ``#>`` operator):: 

137 

138 data_table.c.data[("key_1", "key_2", 5, ..., "key_n")] 

139 

140 * Path index operations returning text (the ``#>>`` operator):: 

141 

142 data_table.c.data[ 

143 ("key_1", "key_2", 5, ..., "key_n") 

144 ].astext == "some value" 

145 

146 Index operations return an expression object whose type defaults to 

147 :class:`_types.JSON` by default, 

148 so that further JSON-oriented instructions 

149 may be called upon the result type. 

150 

151 Custom serializers and deserializers are specified at the dialect level, 

152 that is using :func:`_sa.create_engine`. The reason for this is that when 

153 using psycopg2, the DBAPI only allows serializers at the per-cursor 

154 or per-connection level. E.g.:: 

155 

156 engine = create_engine( 

157 "postgresql+psycopg2://scott:tiger@localhost/test", 

158 json_serializer=my_serialize_fn, 

159 json_deserializer=my_deserialize_fn, 

160 ) 

161 

162 When using the psycopg2 dialect, the json_deserializer is registered 

163 against the database using ``psycopg2.extras.register_default_json``. 

164 

165 .. seealso:: 

166 

167 :class:`_types.JSON` - Core level JSON type 

168 

169 :class:`_postgresql.JSONB` 

170 

171 """ # noqa 

172 

173 render_bind_cast = True 

174 astext_type: TypeEngine[str] = sqltypes.Text() 

175 

176 def __init__( 

177 self, 

178 none_as_null: bool = False, 

179 astext_type: Optional[TypeEngine[str]] = None, 

180 ): 

181 """Construct a :class:`_types.JSON` type. 

182 

183 :param none_as_null: if True, persist the value ``None`` as a 

184 SQL NULL value, not the JSON encoding of ``null``. Note that 

185 when this flag is False, the :func:`.null` construct can still 

186 be used to persist a NULL value:: 

187 

188 from sqlalchemy import null 

189 

190 conn.execute(table.insert(), {"data": null()}) 

191 

192 .. seealso:: 

193 

194 :attr:`_types.JSON.NULL` 

195 

196 :param astext_type: the type to use for the 

197 :attr:`.JSON.Comparator.astext` 

198 accessor on indexed attributes. Defaults to :class:`_types.Text`. 

199 

200 """ 

201 super().__init__(none_as_null=none_as_null) 

202 if astext_type is not None: 

203 self.astext_type = astext_type 

204 

205 class Comparator(sqltypes.JSON.Comparator[_T]): 

206 """Define comparison operations for :class:`_types.JSON`.""" 

207 

208 type: JSON 

209 

210 @property 

211 def astext(self) -> ColumnElement[str]: 

212 """On an indexed expression, use the "astext" (e.g. "->>") 

213 conversion when rendered in SQL. 

214 

215 E.g.:: 

216 

217 select(data_table.c.data["some key"].astext) 

218 

219 .. seealso:: 

220 

221 :meth:`_expression.ColumnElement.cast` 

222 

223 """ 

224 if isinstance(self.expr.right.type, sqltypes.JSON.JSONPathType): 

225 return self.expr.left.operate( # type: ignore[no-any-return] 

226 JSONPATH_ASTEXT, 

227 self.expr.right, 

228 result_type=self.type.astext_type, 

229 ) 

230 else: 

231 return self.expr.left.operate( # type: ignore[no-any-return] 

232 ASTEXT, self.expr.right, result_type=self.type.astext_type 

233 ) 

234 

235 comparator_factory = Comparator 

236 

237 

238class JSONB(JSON): 

239 """Represent the PostgreSQL JSONB type. 

240 

241 The :class:`_postgresql.JSONB` type stores arbitrary JSONB format data, 

242 e.g.:: 

243 

244 data_table = Table( 

245 "data_table", 

246 metadata, 

247 Column("id", Integer, primary_key=True), 

248 Column("data", JSONB), 

249 ) 

250 

251 with engine.connect() as conn: 

252 conn.execute( 

253 data_table.insert(), data={"key1": "value1", "key2": "value2"} 

254 ) 

255 

256 The :class:`_postgresql.JSONB` type includes all operations provided by 

257 :class:`_types.JSON`, including the same behaviors for indexing 

258 operations. 

259 It also adds additional operators specific to JSONB, including 

260 :meth:`.JSONB.Comparator.has_key`, :meth:`.JSONB.Comparator.has_all`, 

261 :meth:`.JSONB.Comparator.has_any`, :meth:`.JSONB.Comparator.contains`, 

262 :meth:`.JSONB.Comparator.contained_by`, 

263 :meth:`.JSONB.Comparator.delete_path`, 

264 :meth:`.JSONB.Comparator.path_exists` and 

265 :meth:`.JSONB.Comparator.path_match`. 

266 

267 Like the :class:`_types.JSON` type, the :class:`_postgresql.JSONB` 

268 type does not detect 

269 in-place changes when used with the ORM, unless the 

270 :mod:`sqlalchemy.ext.mutable` extension is used. 

271 

272 Custom serializers and deserializers 

273 are shared with the :class:`_types.JSON` class, 

274 using the ``json_serializer`` 

275 and ``json_deserializer`` keyword arguments. These must be specified 

276 at the dialect level using :func:`_sa.create_engine`. When using 

277 psycopg2, the serializers are associated with the jsonb type using 

278 ``psycopg2.extras.register_default_jsonb`` on a per-connection basis, 

279 in the same way that ``psycopg2.extras.register_default_json`` is used 

280 to register these handlers with the json type. 

281 

282 .. seealso:: 

283 

284 :class:`_types.JSON` 

285 

286 .. warning:: 

287 

288 **For applications that have indexes against JSONB subscript 

289 expressions** 

290 

291 SQLAlchemy 2.0.42 made a change in how the subscript operation for 

292 :class:`.JSONB` is rendered, from ``-> 'element'`` to ``['element']``, 

293 for PostgreSQL versions greater than 14. This change caused an 

294 unintended side effect for indexes that were created against 

295 expressions that use subscript notation, e.g. 

296 ``Index("ix_entity_json_ab_text", data["a"]["b"].astext)``. If these 

297 indexes were generated with the older syntax e.g. ``((entity.data -> 

298 'a') ->> 'b')``, they will not be used by the PostgreSQL query planner 

299 when a query is made using SQLAlchemy 2.0.42 or higher on PostgreSQL 

300 versions 14 or higher. This occurs because the new text will resemble 

301 ``(entity.data['a'] ->> 'b')`` which will fail to produce the exact 

302 textual syntax match required by the PostgreSQL query planner. 

303 Therefore, for users upgrading to SQLAlchemy 2.0.42 or higher, existing 

304 indexes that were created against :class:`.JSONB` expressions that use 

305 subscripting would need to be dropped and re-created in order for them 

306 to work with the new query syntax, e.g. an expression like 

307 ``((entity.data -> 'a') ->> 'b')`` would become ``(entity.data['a'] ->> 

308 'b')``. 

309 

310 .. seealso:: 

311 

312 :ticket:`12868` - discussion of this issue 

313 

314 """ 

315 

316 __visit_name__ = "JSONB" 

317 

318 def coerce_compared_value( 

319 self, op: Optional[OperatorType], value: Any 

320 ) -> TypeEngine[Any]: 

321 if op in (PATH_MATCH, PATH_EXISTS): 

322 return JSON.JSONPathType() 

323 else: 

324 return super().coerce_compared_value(op, value) 

325 

326 class Comparator(JSON.Comparator[_T]): 

327 """Define comparison operations for :class:`_types.JSON`.""" 

328 

329 type: JSONB 

330 

331 def has_key(self, other: Any) -> ColumnElement[bool]: 

332 """Boolean expression. Test for presence of a key (equivalent of 

333 the ``?`` operator). Note that the key may be a SQLA expression. 

334 """ 

335 return self.operate(HAS_KEY, other, result_type=sqltypes.Boolean) 

336 

337 def has_all(self, other: Any) -> ColumnElement[bool]: 

338 """Boolean expression. Test for presence of all keys in jsonb 

339 (equivalent of the ``?&`` operator) 

340 """ 

341 return self.operate(HAS_ALL, other, result_type=sqltypes.Boolean) 

342 

343 def has_any(self, other: Any) -> ColumnElement[bool]: 

344 """Boolean expression. Test for presence of any key in jsonb 

345 (equivalent of the ``?|`` operator) 

346 """ 

347 return self.operate(HAS_ANY, other, result_type=sqltypes.Boolean) 

348 

349 def contains(self, other: Any, **kwargs: Any) -> ColumnElement[bool]: 

350 """Boolean expression. Test if keys (or array) are a superset 

351 of/contained the keys of the argument jsonb expression 

352 (equivalent of the ``@>`` operator). 

353 

354 kwargs may be ignored by this operator but are required for API 

355 conformance. 

356 """ 

357 return self.operate(CONTAINS, other, result_type=sqltypes.Boolean) 

358 

359 def contained_by(self, other: Any) -> ColumnElement[bool]: 

360 """Boolean expression. Test if keys are a proper subset of the 

361 keys of the argument jsonb expression 

362 (equivalent of the ``<@`` operator). 

363 """ 

364 return self.operate( 

365 CONTAINED_BY, other, result_type=sqltypes.Boolean 

366 ) 

367 

368 def delete_path( 

369 self, array: Union[List[str], _pg_array[str]] 

370 ) -> ColumnElement[JSONB]: 

371 """JSONB expression. Deletes field or array element specified in 

372 the argument array (equivalent of the ``#-`` operator). 

373 

374 The input may be a list of strings that will be coerced to an 

375 ``ARRAY`` or an instance of :meth:`_postgres.array`. 

376 

377 .. versionadded:: 2.0 

378 """ 

379 if not isinstance(array, _pg_array): 

380 array = _pg_array(array) 

381 right_side = cast(array, ARRAY(sqltypes.TEXT)) 

382 return self.operate(DELETE_PATH, right_side, result_type=JSONB) 

383 

384 def path_exists(self, other: Any) -> ColumnElement[bool]: 

385 """Boolean expression. Test for presence of item given by the 

386 argument JSONPath expression (equivalent of the ``@?`` operator). 

387 

388 .. versionadded:: 2.0 

389 """ 

390 return self.operate( 

391 PATH_EXISTS, other, result_type=sqltypes.Boolean 

392 ) 

393 

394 def path_match(self, other: Any) -> ColumnElement[bool]: 

395 """Boolean expression. Test if JSONPath predicate given by the 

396 argument JSONPath expression matches 

397 (equivalent of the ``@@`` operator). 

398 

399 Only the first item of the result is taken into account. 

400 

401 .. versionadded:: 2.0 

402 """ 

403 return self.operate( 

404 PATH_MATCH, other, result_type=sqltypes.Boolean 

405 ) 

406 

407 comparator_factory = Comparator