array.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. # dialects/postgresql/array.py
  2. # Copyright (C) 2005-2024 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. # mypy: ignore-errors
  8. from __future__ import annotations
  9. import re
  10. from typing import Any
  11. from typing import Optional
  12. from typing import TypeVar
  13. from .operators import CONTAINED_BY
  14. from .operators import CONTAINS
  15. from .operators import OVERLAP
  16. from ... import types as sqltypes
  17. from ... import util
  18. from ...sql import expression
  19. from ...sql import operators
  20. from ...sql._typing import _TypeEngineArgument
  21. _T = TypeVar("_T", bound=Any)
  22. def Any(other, arrexpr, operator=operators.eq):
  23. """A synonym for the ARRAY-level :meth:`.ARRAY.Comparator.any` method.
  24. See that method for details.
  25. """
  26. return arrexpr.any(other, operator)
  27. def All(other, arrexpr, operator=operators.eq):
  28. """A synonym for the ARRAY-level :meth:`.ARRAY.Comparator.all` method.
  29. See that method for details.
  30. """
  31. return arrexpr.all(other, operator)
  32. class array(expression.ExpressionClauseList[_T]):
  33. """A PostgreSQL ARRAY literal.
  34. This is used to produce ARRAY literals in SQL expressions, e.g.::
  35. from sqlalchemy.dialects.postgresql import array
  36. from sqlalchemy.dialects import postgresql
  37. from sqlalchemy import select, func
  38. stmt = select(array([1,2]) + array([3,4,5]))
  39. print(stmt.compile(dialect=postgresql.dialect()))
  40. Produces the SQL::
  41. SELECT ARRAY[%(param_1)s, %(param_2)s] ||
  42. ARRAY[%(param_3)s, %(param_4)s, %(param_5)s]) AS anon_1
  43. An instance of :class:`.array` will always have the datatype
  44. :class:`_types.ARRAY`. The "inner" type of the array is inferred from
  45. the values present, unless the ``type_`` keyword argument is passed::
  46. array(['foo', 'bar'], type_=CHAR)
  47. Multidimensional arrays are produced by nesting :class:`.array` constructs.
  48. The dimensionality of the final :class:`_types.ARRAY`
  49. type is calculated by
  50. recursively adding the dimensions of the inner :class:`_types.ARRAY`
  51. type::
  52. stmt = select(
  53. array([
  54. array([1, 2]), array([3, 4]), array([column('q'), column('x')])
  55. ])
  56. )
  57. print(stmt.compile(dialect=postgresql.dialect()))
  58. Produces::
  59. SELECT ARRAY[ARRAY[%(param_1)s, %(param_2)s],
  60. ARRAY[%(param_3)s, %(param_4)s], ARRAY[q, x]] AS anon_1
  61. .. versionadded:: 1.3.6 added support for multidimensional array literals
  62. .. seealso::
  63. :class:`_postgresql.ARRAY`
  64. """
  65. __visit_name__ = "array"
  66. stringify_dialect = "postgresql"
  67. inherit_cache = True
  68. def __init__(self, clauses, **kw):
  69. type_arg = kw.pop("type_", None)
  70. super().__init__(operators.comma_op, *clauses, **kw)
  71. self._type_tuple = [arg.type for arg in self.clauses]
  72. main_type = (
  73. type_arg
  74. if type_arg is not None
  75. else self._type_tuple[0]
  76. if self._type_tuple
  77. else sqltypes.NULLTYPE
  78. )
  79. if isinstance(main_type, ARRAY):
  80. self.type = ARRAY(
  81. main_type.item_type,
  82. dimensions=main_type.dimensions + 1
  83. if main_type.dimensions is not None
  84. else 2,
  85. )
  86. else:
  87. self.type = ARRAY(main_type)
  88. @property
  89. def _select_iterable(self):
  90. return (self,)
  91. def _bind_param(self, operator, obj, _assume_scalar=False, type_=None):
  92. if _assume_scalar or operator is operators.getitem:
  93. return expression.BindParameter(
  94. None,
  95. obj,
  96. _compared_to_operator=operator,
  97. type_=type_,
  98. _compared_to_type=self.type,
  99. unique=True,
  100. )
  101. else:
  102. return array(
  103. [
  104. self._bind_param(
  105. operator, o, _assume_scalar=True, type_=type_
  106. )
  107. for o in obj
  108. ]
  109. )
  110. def self_group(self, against=None):
  111. if against in (operators.any_op, operators.all_op, operators.getitem):
  112. return expression.Grouping(self)
  113. else:
  114. return self
  115. class ARRAY(sqltypes.ARRAY):
  116. """PostgreSQL ARRAY type.
  117. The :class:`_postgresql.ARRAY` type is constructed in the same way
  118. as the core :class:`_types.ARRAY` type; a member type is required, and a
  119. number of dimensions is recommended if the type is to be used for more
  120. than one dimension::
  121. from sqlalchemy.dialects import postgresql
  122. mytable = Table("mytable", metadata,
  123. Column("data", postgresql.ARRAY(Integer, dimensions=2))
  124. )
  125. The :class:`_postgresql.ARRAY` type provides all operations defined on the
  126. core :class:`_types.ARRAY` type, including support for "dimensions",
  127. indexed access, and simple matching such as
  128. :meth:`.types.ARRAY.Comparator.any` and
  129. :meth:`.types.ARRAY.Comparator.all`. :class:`_postgresql.ARRAY`
  130. class also
  131. provides PostgreSQL-specific methods for containment operations, including
  132. :meth:`.postgresql.ARRAY.Comparator.contains`
  133. :meth:`.postgresql.ARRAY.Comparator.contained_by`, and
  134. :meth:`.postgresql.ARRAY.Comparator.overlap`, e.g.::
  135. mytable.c.data.contains([1, 2])
  136. The :class:`_postgresql.ARRAY` type may not be supported on all
  137. PostgreSQL DBAPIs; it is currently known to work on psycopg2 only.
  138. Additionally, the :class:`_postgresql.ARRAY`
  139. type does not work directly in
  140. conjunction with the :class:`.ENUM` type. For a workaround, see the
  141. special type at :ref:`postgresql_array_of_enum`.
  142. .. container:: topic
  143. **Detecting Changes in ARRAY columns when using the ORM**
  144. The :class:`_postgresql.ARRAY` type, when used with the SQLAlchemy ORM,
  145. does not detect in-place mutations to the array. In order to detect
  146. these, the :mod:`sqlalchemy.ext.mutable` extension must be used, using
  147. the :class:`.MutableList` class::
  148. from sqlalchemy.dialects.postgresql import ARRAY
  149. from sqlalchemy.ext.mutable import MutableList
  150. class SomeOrmClass(Base):
  151. # ...
  152. data = Column(MutableList.as_mutable(ARRAY(Integer)))
  153. This extension will allow "in-place" changes such to the array
  154. such as ``.append()`` to produce events which will be detected by the
  155. unit of work. Note that changes to elements **inside** the array,
  156. including subarrays that are mutated in place, are **not** detected.
  157. Alternatively, assigning a new array value to an ORM element that
  158. replaces the old one will always trigger a change event.
  159. .. seealso::
  160. :class:`_types.ARRAY` - base array type
  161. :class:`_postgresql.array` - produces a literal array value.
  162. """
  163. class Comparator(sqltypes.ARRAY.Comparator):
  164. """Define comparison operations for :class:`_types.ARRAY`.
  165. Note that these operations are in addition to those provided
  166. by the base :class:`.types.ARRAY.Comparator` class, including
  167. :meth:`.types.ARRAY.Comparator.any` and
  168. :meth:`.types.ARRAY.Comparator.all`.
  169. """
  170. def contains(self, other, **kwargs):
  171. """Boolean expression. Test if elements are a superset of the
  172. elements of the argument array expression.
  173. kwargs may be ignored by this operator but are required for API
  174. conformance.
  175. """
  176. return self.operate(CONTAINS, other, result_type=sqltypes.Boolean)
  177. def contained_by(self, other):
  178. """Boolean expression. Test if elements are a proper subset of the
  179. elements of the argument array expression.
  180. """
  181. return self.operate(
  182. CONTAINED_BY, other, result_type=sqltypes.Boolean
  183. )
  184. def overlap(self, other):
  185. """Boolean expression. Test if array has elements in common with
  186. an argument array expression.
  187. """
  188. return self.operate(OVERLAP, other, result_type=sqltypes.Boolean)
  189. comparator_factory = Comparator
  190. def __init__(
  191. self,
  192. item_type: _TypeEngineArgument[Any],
  193. as_tuple: bool = False,
  194. dimensions: Optional[int] = None,
  195. zero_indexes: bool = False,
  196. ):
  197. """Construct an ARRAY.
  198. E.g.::
  199. Column('myarray', ARRAY(Integer))
  200. Arguments are:
  201. :param item_type: The data type of items of this array. Note that
  202. dimensionality is irrelevant here, so multi-dimensional arrays like
  203. ``INTEGER[][]``, are constructed as ``ARRAY(Integer)``, not as
  204. ``ARRAY(ARRAY(Integer))`` or such.
  205. :param as_tuple=False: Specify whether return results
  206. should be converted to tuples from lists. DBAPIs such
  207. as psycopg2 return lists by default. When tuples are
  208. returned, the results are hashable.
  209. :param dimensions: if non-None, the ARRAY will assume a fixed
  210. number of dimensions. This will cause the DDL emitted for this
  211. ARRAY to include the exact number of bracket clauses ``[]``,
  212. and will also optimize the performance of the type overall.
  213. Note that PG arrays are always implicitly "non-dimensioned",
  214. meaning they can store any number of dimensions no matter how
  215. they were declared.
  216. :param zero_indexes=False: when True, index values will be converted
  217. between Python zero-based and PostgreSQL one-based indexes, e.g.
  218. a value of one will be added to all index values before passing
  219. to the database.
  220. """
  221. if isinstance(item_type, ARRAY):
  222. raise ValueError(
  223. "Do not nest ARRAY types; ARRAY(basetype) "
  224. "handles multi-dimensional arrays of basetype"
  225. )
  226. if isinstance(item_type, type):
  227. item_type = item_type()
  228. self.item_type = item_type
  229. self.as_tuple = as_tuple
  230. self.dimensions = dimensions
  231. self.zero_indexes = zero_indexes
  232. @property
  233. def hashable(self):
  234. return self.as_tuple
  235. @property
  236. def python_type(self):
  237. return list
  238. def compare_values(self, x, y):
  239. return x == y
  240. @util.memoized_property
  241. def _against_native_enum(self):
  242. return (
  243. isinstance(self.item_type, sqltypes.Enum)
  244. and self.item_type.native_enum
  245. )
  246. def literal_processor(self, dialect):
  247. item_proc = self.item_type.dialect_impl(dialect).literal_processor(
  248. dialect
  249. )
  250. if item_proc is None:
  251. return None
  252. def to_str(elements):
  253. return f"ARRAY[{', '.join(elements)}]"
  254. def process(value):
  255. inner = self._apply_item_processor(
  256. value, item_proc, self.dimensions, to_str
  257. )
  258. return inner
  259. return process
  260. def bind_processor(self, dialect):
  261. item_proc = self.item_type.dialect_impl(dialect).bind_processor(
  262. dialect
  263. )
  264. def process(value):
  265. if value is None:
  266. return value
  267. else:
  268. return self._apply_item_processor(
  269. value, item_proc, self.dimensions, list
  270. )
  271. return process
  272. def result_processor(self, dialect, coltype):
  273. item_proc = self.item_type.dialect_impl(dialect).result_processor(
  274. dialect, coltype
  275. )
  276. def process(value):
  277. if value is None:
  278. return value
  279. else:
  280. return self._apply_item_processor(
  281. value,
  282. item_proc,
  283. self.dimensions,
  284. tuple if self.as_tuple else list,
  285. )
  286. if self._against_native_enum:
  287. super_rp = process
  288. pattern = re.compile(r"^{(.*)}$")
  289. def handle_raw_string(value):
  290. inner = pattern.match(value).group(1)
  291. return _split_enum_values(inner)
  292. def process(value):
  293. if value is None:
  294. return value
  295. # isinstance(value, str) is required to handle
  296. # the case where a TypeDecorator for and Array of Enum is
  297. # used like was required in sa < 1.3.17
  298. return super_rp(
  299. handle_raw_string(value)
  300. if isinstance(value, str)
  301. else value
  302. )
  303. return process
  304. def _split_enum_values(array_string):
  305. if '"' not in array_string:
  306. # no escape char is present so it can just split on the comma
  307. return array_string.split(",") if array_string else []
  308. # handles quoted strings from:
  309. # r'abc,"quoted","also\\\\quoted", "quoted, comma", "esc \" quot", qpr'
  310. # returns
  311. # ['abc', 'quoted', 'also\\quoted', 'quoted, comma', 'esc " quot', 'qpr']
  312. text = array_string.replace(r"\"", "_$ESC_QUOTE$_")
  313. text = text.replace(r"\\", "\\")
  314. result = []
  315. on_quotes = re.split(r'(")', text)
  316. in_quotes = False
  317. for tok in on_quotes:
  318. if tok == '"':
  319. in_quotes = not in_quotes
  320. elif in_quotes:
  321. result.append(tok.replace("_$ESC_QUOTE$_", '"'))
  322. else:
  323. result.extend(re.findall(r"([^\s,]+),?", tok))
  324. return result