1# dialects/postgresql/array.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
8
9from __future__ import annotations
10
11import re
12from typing import Any as typing_Any
13from typing import Iterable
14from typing import Optional
15from typing import Sequence
16from typing import TYPE_CHECKING
17from typing import TypeVar
18from typing import Union
19
20from .operators import CONTAINED_BY
21from .operators import CONTAINS
22from .operators import OVERLAP
23from ... import types as sqltypes
24from ... import util
25from ...sql import expression
26from ...sql import operators
27from ...sql.visitors import InternalTraversal
28
29if TYPE_CHECKING:
30 from ...engine.interfaces import Dialect
31 from ...sql._typing import _ColumnExpressionArgument
32 from ...sql._typing import _TypeEngineArgument
33 from ...sql.elements import ColumnElement
34 from ...sql.elements import Grouping
35 from ...sql.expression import BindParameter
36 from ...sql.operators import OperatorType
37 from ...sql.selectable import _SelectIterable
38 from ...sql.type_api import _BindProcessorType
39 from ...sql.type_api import _LiteralProcessorType
40 from ...sql.type_api import _ResultProcessorType
41 from ...sql.type_api import TypeEngine
42 from ...sql.visitors import _TraverseInternalsType
43 from ...util.typing import Self
44
45
46_T = TypeVar("_T", bound=typing_Any)
47_CT = TypeVar("_CT", bound=typing_Any)
48
49
50def Any(
51 other: typing_Any,
52 arrexpr: _ColumnExpressionArgument[_T],
53 operator: OperatorType = operators.eq,
54) -> ColumnElement[bool]:
55 """A synonym for the ARRAY-level :meth:`.ARRAY.Comparator.any` method.
56 See that method for details.
57
58 .. deprecated:: 2.1
59
60 The :meth:`_types.ARRAY.Comparator.any` and
61 :meth:`_types.ARRAY.Comparator.all` methods for arrays are deprecated
62 for removal, along with the PG-specific :func:`_postgresql.Any` and
63 :func:`_postgresql.All` functions. See :func:`_sql.any_` and
64 :func:`_sql.all_` functions for modern use.
65
66
67 """
68
69 return arrexpr.any(other, operator) # type: ignore[no-any-return, union-attr] # noqa: E501
70
71
72def All(
73 other: typing_Any,
74 arrexpr: _ColumnExpressionArgument[_T],
75 operator: OperatorType = operators.eq,
76) -> ColumnElement[bool]:
77 """A synonym for the ARRAY-level :meth:`.ARRAY.Comparator.all` method.
78 See that method for details.
79
80 .. deprecated:: 2.1
81
82 The :meth:`_types.ARRAY.Comparator.any` and
83 :meth:`_types.ARRAY.Comparator.all` methods for arrays are deprecated
84 for removal, along with the PG-specific :func:`_postgresql.Any` and
85 :func:`_postgresql.All` functions. See :func:`_sql.any_` and
86 :func:`_sql.all_` functions for modern use.
87
88 """
89
90 return arrexpr.all(other, operator) # type: ignore[no-any-return, union-attr] # noqa: E501
91
92
93class array(expression.ExpressionClauseList[_T]):
94 """A PostgreSQL ARRAY literal.
95
96 This is used to produce ARRAY literals in SQL expressions, e.g.::
97
98 from sqlalchemy.dialects.postgresql import array
99 from sqlalchemy.dialects import postgresql
100 from sqlalchemy import select, func
101
102 stmt = select(array([1, 2]) + array([3, 4, 5]))
103
104 print(stmt.compile(dialect=postgresql.dialect()))
105
106 Produces the SQL:
107
108 .. sourcecode:: sql
109
110 SELECT ARRAY[%(param_1)s, %(param_2)s] ||
111 ARRAY[%(param_3)s, %(param_4)s, %(param_5)s]) AS anon_1
112
113 An instance of :class:`.array` will always have the datatype
114 :class:`_types.ARRAY`. The "inner" type of the array is inferred from the
115 values present, unless the :paramref:`_postgresql.array.type_` keyword
116 argument is passed::
117
118 array(["foo", "bar"], type_=CHAR)
119
120 When constructing an empty array, the :paramref:`_postgresql.array.type_`
121 argument is particularly important as PostgreSQL server typically requires
122 a cast to be rendered for the inner type in order to render an empty array.
123 SQLAlchemy's compilation for the empty array will produce this cast so
124 that::
125
126 stmt = array([], type_=Integer)
127 print(stmt.compile(dialect=postgresql.dialect()))
128
129 Produces:
130
131 .. sourcecode:: sql
132
133 ARRAY[]::INTEGER[]
134
135 As required by PostgreSQL for empty arrays.
136
137 .. versionadded:: 2.0.40 added support to render empty PostgreSQL array
138 literals with a required cast.
139
140 Multidimensional arrays are produced by nesting :class:`.array` constructs.
141 The dimensionality of the final :class:`_types.ARRAY`
142 type is calculated by
143 recursively adding the dimensions of the inner :class:`_types.ARRAY`
144 type::
145
146 stmt = select(
147 array(
148 [array([1, 2]), array([3, 4]), array([column("q"), column("x")])]
149 )
150 )
151 print(stmt.compile(dialect=postgresql.dialect()))
152
153 Produces:
154
155 .. sourcecode:: sql
156
157 SELECT ARRAY[
158 ARRAY[%(param_1)s, %(param_2)s],
159 ARRAY[%(param_3)s, %(param_4)s],
160 ARRAY[q, x]
161 ] AS anon_1
162
163 .. seealso::
164
165 :class:`_postgresql.ARRAY`
166
167 """ # noqa: E501
168
169 __visit_name__ = "array"
170
171 stringify_dialect = "postgresql"
172
173 _traverse_internals: _TraverseInternalsType = [
174 ("clauses", InternalTraversal.dp_clauseelement_tuple),
175 ("type", InternalTraversal.dp_type),
176 ]
177
178 def __init__(
179 self,
180 clauses: Iterable[_T],
181 *,
182 type_: Optional[_TypeEngineArgument[_T]] = None,
183 **kw: typing_Any,
184 ):
185 r"""Construct an ARRAY literal.
186
187 :param clauses: iterable, such as a list, containing elements to be
188 rendered in the array
189 :param type\_: optional type. If omitted, the type is inferred
190 from the contents of the array.
191
192 """
193 super().__init__(operators.comma_op, *clauses, **kw)
194
195 main_type = (
196 type_
197 if type_ is not None
198 else self.clauses[0].type if self.clauses else sqltypes.NULLTYPE
199 )
200
201 if isinstance(main_type, ARRAY):
202 self.type = ARRAY(
203 main_type.item_type,
204 dimensions=(
205 main_type.dimensions + 1
206 if main_type.dimensions is not None
207 else 2
208 ),
209 ) # type: ignore[assignment]
210 else:
211 self.type = ARRAY(main_type) # type: ignore[assignment]
212
213 @property
214 def _select_iterable(self) -> _SelectIterable:
215 return (self,)
216
217 def _bind_param(
218 self,
219 operator: OperatorType,
220 obj: typing_Any,
221 type_: Optional[TypeEngine[_T]] = None,
222 _assume_scalar: bool = False,
223 ) -> BindParameter[_T]:
224 if _assume_scalar or operator is operators.getitem:
225 return expression.BindParameter(
226 None,
227 obj,
228 _compared_to_operator=operator,
229 type_=type_,
230 _compared_to_type=self.type,
231 unique=True,
232 )
233
234 else:
235 return array(
236 [
237 self._bind_param(
238 operator, o, _assume_scalar=True, type_=type_
239 )
240 for o in obj
241 ]
242 ) # type: ignore[return-value]
243
244 def self_group(
245 self, against: Optional[OperatorType] = None
246 ) -> Union[Self, Grouping[_T]]:
247 if against in (operators.any_op, operators.all_op, operators.getitem):
248 return expression.Grouping(self)
249 else:
250 return self
251
252
253class ARRAY(sqltypes.ARRAY[_T]):
254 """PostgreSQL ARRAY type.
255
256 The :class:`_postgresql.ARRAY` type is constructed in the same way
257 as the core :class:`_types.ARRAY` type; a member type is required, and a
258 number of dimensions is recommended if the type is to be used for more
259 than one dimension::
260
261 from sqlalchemy.dialects import postgresql
262
263 mytable = Table(
264 "mytable",
265 metadata,
266 Column("data", postgresql.ARRAY(Integer, dimensions=2)),
267 )
268
269 The :class:`_postgresql.ARRAY` type provides all operations defined on the
270 core :class:`_types.ARRAY` type, including support for "dimensions",
271 indexed access, and simple matching such as
272 :meth:`.types.ARRAY.Comparator.any` and
273 :meth:`.types.ARRAY.Comparator.all`. :class:`_postgresql.ARRAY`
274 class also
275 provides PostgreSQL-specific methods for containment operations, including
276 :meth:`.postgresql.ARRAY.Comparator.contains`
277 :meth:`.postgresql.ARRAY.Comparator.contained_by`, and
278 :meth:`.postgresql.ARRAY.Comparator.overlap`, e.g.::
279
280 mytable.c.data.contains([1, 2])
281
282 Indexed access is one-based by default, to match that of PostgreSQL;
283 for zero-based indexed access, set
284 :paramref:`_postgresql.ARRAY.zero_indexes`.
285
286 Additionally, the :class:`_postgresql.ARRAY`
287 type does not work directly in
288 conjunction with the :class:`.ENUM` type. For a workaround, see the
289 special type at :ref:`postgresql_array_of_enum`.
290
291 .. container:: topic
292
293 **Detecting Changes in ARRAY columns when using the ORM**
294
295 The :class:`_postgresql.ARRAY` type, when used with the SQLAlchemy ORM,
296 does not detect in-place mutations to the array. In order to detect
297 these, the :mod:`sqlalchemy.ext.mutable` extension must be used, using
298 the :class:`.MutableList` class::
299
300 from sqlalchemy.dialects.postgresql import ARRAY
301 from sqlalchemy.ext.mutable import MutableList
302
303
304 class SomeOrmClass(Base):
305 # ...
306
307 data = Column(MutableList.as_mutable(ARRAY(Integer)))
308
309 This extension will allow "in-place" changes such to the array
310 such as ``.append()`` to produce events which will be detected by the
311 unit of work. Note that changes to elements **inside** the array,
312 including subarrays that are mutated in place, are **not** detected.
313
314 Alternatively, assigning a new array value to an ORM element that
315 replaces the old one will always trigger a change event.
316
317 .. seealso::
318
319 :class:`_types.ARRAY` - base array type
320
321 :class:`_postgresql.array` - produces a literal array value.
322
323 """
324
325 def __init__(
326 self,
327 item_type: _TypeEngineArgument[_T],
328 as_tuple: bool = False,
329 dimensions: Optional[int] = None,
330 zero_indexes: bool = False,
331 ):
332 """Construct an ARRAY.
333
334 E.g.::
335
336 Column("myarray", ARRAY(Integer))
337
338 Arguments are:
339
340 :param item_type: The data type of items of this array. Note that
341 dimensionality is irrelevant here, so multi-dimensional arrays like
342 ``INTEGER[][]``, are constructed as ``ARRAY(Integer)``, not as
343 ``ARRAY(ARRAY(Integer))`` or such.
344
345 :param as_tuple=False: Specify whether return results
346 should be converted to tuples from lists. DBAPIs such
347 as psycopg2 return lists by default. When tuples are
348 returned, the results are hashable.
349
350 :param dimensions: if non-None, the ARRAY will assume a fixed
351 number of dimensions. This will cause the DDL emitted for this
352 ARRAY to include the exact number of bracket clauses ``[]``,
353 and will also optimize the performance of the type overall.
354 Note that PG arrays are always implicitly "non-dimensioned",
355 meaning they can store any number of dimensions no matter how
356 they were declared.
357
358 :param zero_indexes=False: when True, index values will be converted
359 between Python zero-based and PostgreSQL one-based indexes, e.g.
360 a value of one will be added to all index values before passing
361 to the database.
362
363 """
364 if isinstance(item_type, ARRAY):
365 raise ValueError(
366 "Do not nest ARRAY types; ARRAY(basetype) "
367 "handles multi-dimensional arrays of basetype"
368 )
369 if isinstance(item_type, type):
370 item_type = item_type()
371 self.item_type = item_type
372 self.as_tuple = as_tuple
373 self.dimensions = dimensions
374 self.zero_indexes = zero_indexes
375
376 class Comparator(sqltypes.ARRAY.Comparator[_CT]):
377 """Define comparison operations for :class:`_types.ARRAY`.
378
379 Note that these operations are in addition to those provided
380 by the base :class:`.types.ARRAY.Comparator` class, including
381 :meth:`.types.ARRAY.Comparator.any` and
382 :meth:`.types.ARRAY.Comparator.all`.
383
384 """
385
386 def contains(
387 self, other: typing_Any, **kwargs: typing_Any
388 ) -> ColumnElement[bool]:
389 """Boolean expression. Test if elements are a superset of the
390 elements of the argument array expression.
391
392 kwargs may be ignored by this operator but are required for API
393 conformance.
394 """
395 return self.operate(CONTAINS, other, result_type=sqltypes.Boolean)
396
397 def contained_by(self, other: typing_Any) -> ColumnElement[bool]:
398 """Boolean expression. Test if elements are a proper subset of the
399 elements of the argument array expression.
400 """
401 return self.operate(
402 CONTAINED_BY, other, result_type=sqltypes.Boolean
403 )
404
405 def overlap(self, other: typing_Any) -> ColumnElement[bool]:
406 """Boolean expression. Test if array has elements in common with
407 an argument array expression.
408 """
409 return self.operate(OVERLAP, other, result_type=sqltypes.Boolean)
410
411 comparator_factory = Comparator
412
413 @util.memoized_property
414 def _against_native_enum(self) -> bool:
415 return (
416 isinstance(self.item_type, sqltypes.Enum)
417 and self.item_type.native_enum
418 )
419
420 def literal_processor(
421 self, dialect: Dialect
422 ) -> Optional[_LiteralProcessorType[_T]]:
423 item_proc = self.item_type.dialect_impl(dialect).literal_processor(
424 dialect
425 )
426 if item_proc is None:
427 return None
428
429 def to_str(elements: Iterable[typing_Any]) -> str:
430 return f"ARRAY[{', '.join(elements)}]"
431
432 def process(value: Sequence[typing_Any]) -> str:
433 inner = self._apply_item_processor(
434 value, item_proc, self.dimensions, to_str
435 )
436 return inner
437
438 return process
439
440 def bind_processor(
441 self, dialect: Dialect
442 ) -> Optional[_BindProcessorType[Sequence[typing_Any]]]:
443 item_proc = self.item_type.dialect_impl(dialect).bind_processor(
444 dialect
445 )
446
447 def process(
448 value: Optional[Sequence[typing_Any]],
449 ) -> Optional[list[typing_Any]]:
450 if value is None:
451 return value
452 else:
453 return self._apply_item_processor(
454 value, item_proc, self.dimensions, list
455 )
456
457 return process
458
459 def result_processor(
460 self, dialect: Dialect, coltype: object
461 ) -> _ResultProcessorType[Sequence[typing_Any]]:
462 item_proc = self.item_type.dialect_impl(dialect).result_processor(
463 dialect, coltype
464 )
465
466 def process(
467 value: Sequence[typing_Any],
468 ) -> Optional[Sequence[typing_Any]]:
469 if value is None:
470 return value
471 else:
472 return self._apply_item_processor(
473 value,
474 item_proc,
475 self.dimensions,
476 tuple if self.as_tuple else list,
477 )
478
479 if self._against_native_enum:
480 super_rp = process
481 pattern = re.compile(r"^{(.*)}$")
482
483 def handle_raw_string(value: str) -> Sequence[Optional[str]]:
484 inner = pattern.match(value).group(1) # type: ignore[union-attr] # noqa: E501
485 return _split_enum_values(inner)
486
487 def process(
488 value: Sequence[typing_Any],
489 ) -> Optional[Sequence[typing_Any]]:
490 if value is None:
491 return value
492 # isinstance(value, str) is required to handle
493 # the case where a TypeDecorator for and Array of Enum is
494 # used like was required in sa < 1.3.17
495 return super_rp(
496 handle_raw_string(value)
497 if isinstance(value, str)
498 else value
499 )
500
501 return process
502
503
504def _split_enum_values(array_string: str) -> Sequence[Optional[str]]:
505 if '"' not in array_string:
506 # no escape char is present so it can just split on the comma
507 return [
508 r if r != "NULL" else None
509 for r in (array_string.split(",") if array_string else [])
510 ]
511
512 # handles quoted strings from:
513 # r'abc,"quoted","also\\\\quoted", "quoted, comma", "esc \" quot", qpr'
514 # returns
515 # ['abc', 'quoted', 'also\\quoted', 'quoted, comma', 'esc " quot', 'qpr']
516 text = array_string.replace(r"\"", "_$ESC_QUOTE$_")
517 text = text.replace(r"\\", "\\")
518 result = []
519 on_quotes = re.split(r'(")', text)
520 in_quotes = False
521 for tok in on_quotes:
522 if tok == '"':
523 in_quotes = not in_quotes
524 elif in_quotes:
525 result.append(tok.replace("_$ESC_QUOTE$_", '"'))
526 else:
527 # interpret NULL (without quotes!) as None
528 result.extend(
529 [
530 r if r != "NULL" else None
531 for r in re.findall(r"([^\s,]+),?", tok)
532 ]
533 )
534 return result