SQLAlchemy 2.0.39__py3-none-any.whl → 2.0.40__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. sqlalchemy/__init__.py +1 -1
  2. sqlalchemy/connectors/pyodbc.py +2 -4
  3. sqlalchemy/dialects/mysql/base.py +15 -8
  4. sqlalchemy/dialects/mysql/mariadb.py +17 -11
  5. sqlalchemy/dialects/mysql/mysqlconnector.py +84 -19
  6. sqlalchemy/dialects/mysql/provision.py +4 -0
  7. sqlalchemy/dialects/mysql/reflection.py +1 -1
  8. sqlalchemy/dialects/mysql/types.py +4 -5
  9. sqlalchemy/dialects/postgresql/array.py +132 -58
  10. sqlalchemy/dialects/postgresql/base.py +44 -3
  11. sqlalchemy/dialects/postgresql/json.py +1 -1
  12. sqlalchemy/dialects/sqlite/base.py +15 -4
  13. sqlalchemy/engine/cursor.py +6 -1
  14. sqlalchemy/engine/default.py +27 -12
  15. sqlalchemy/engine/interfaces.py +11 -4
  16. sqlalchemy/ext/asyncio/base.py +7 -7
  17. sqlalchemy/ext/asyncio/engine.py +5 -3
  18. sqlalchemy/ext/asyncio/session.py +21 -2
  19. sqlalchemy/orm/decl_api.py +1 -1
  20. sqlalchemy/orm/decl_base.py +1 -1
  21. sqlalchemy/orm/exc.py +9 -0
  22. sqlalchemy/orm/properties.py +12 -3
  23. sqlalchemy/orm/util.py +6 -5
  24. sqlalchemy/pool/base.py +1 -1
  25. sqlalchemy/sql/_elements_constructors.py +14 -4
  26. sqlalchemy/sql/_selectable_constructors.py +2 -0
  27. sqlalchemy/sql/coercions.py +1 -1
  28. sqlalchemy/sql/compiler.py +237 -131
  29. sqlalchemy/sql/crud.py +12 -3
  30. sqlalchemy/sql/ddl.py +49 -46
  31. sqlalchemy/sql/elements.py +24 -17
  32. sqlalchemy/sql/functions.py +42 -7
  33. sqlalchemy/sql/schema.py +23 -5
  34. sqlalchemy/sql/sqltypes.py +98 -34
  35. sqlalchemy/sql/type_api.py +4 -0
  36. sqlalchemy/sql/util.py +1 -1
  37. sqlalchemy/testing/assertions.py +2 -2
  38. sqlalchemy/testing/requirements.py +13 -0
  39. sqlalchemy/testing/suite/test_reflection.py +44 -0
  40. sqlalchemy/testing/suite/test_results.py +2 -0
  41. sqlalchemy/util/_collections.py +3 -1
  42. sqlalchemy/util/typing.py +64 -21
  43. {sqlalchemy-2.0.39.dist-info → sqlalchemy-2.0.40.dist-info}/METADATA +10 -9
  44. {sqlalchemy-2.0.39.dist-info → sqlalchemy-2.0.40.dist-info}/RECORD +47 -47
  45. {sqlalchemy-2.0.39.dist-info → sqlalchemy-2.0.40.dist-info}/WHEEL +1 -1
  46. {sqlalchemy-2.0.39.dist-info → sqlalchemy-2.0.40.dist-info/licenses}/LICENSE +0 -0
  47. {sqlalchemy-2.0.39.dist-info → sqlalchemy-2.0.40.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py CHANGED
@@ -269,7 +269,7 @@ from .types import Uuid as Uuid
269
269
  from .types import VARBINARY as VARBINARY
270
270
  from .types import VARCHAR as VARCHAR
271
271
 
272
- __version__ = "2.0.39"
272
+ __version__ = "2.0.40"
273
273
 
274
274
 
275
275
  def __go(lcls: Any) -> None:
@@ -227,11 +227,9 @@ class PyODBCConnector(Connector):
227
227
  )
228
228
 
229
229
  def get_isolation_level_values(
230
- self, dbapi_connection: interfaces.DBAPIConnection
230
+ self, dbapi_conn: interfaces.DBAPIConnection
231
231
  ) -> List[IsolationLevel]:
232
- return super().get_isolation_level_values(dbapi_connection) + [
233
- "AUTOCOMMIT"
234
- ]
232
+ return [*super().get_isolation_level_values(dbapi_conn), "AUTOCOMMIT"]
235
233
 
236
234
  def set_isolation_level(
237
235
  self,
@@ -1928,12 +1928,13 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
1928
1928
  colspec.append("AUTO_INCREMENT")
1929
1929
  else:
1930
1930
  default = self.get_column_default_string(column)
1931
+
1931
1932
  if default is not None:
1932
1933
  if (
1933
- isinstance(
1934
- column.server_default.arg, functions.FunctionElement
1935
- )
1936
- and self.dialect._support_default_function
1934
+ self.dialect._support_default_function
1935
+ and not re.match(r"^\s*[\'\"\(]", default)
1936
+ and "ON UPDATE" not in default
1937
+ and re.match(r".*\W.*", default)
1937
1938
  ):
1938
1939
  colspec.append(f"DEFAULT ({default})")
1939
1940
  else:
@@ -2520,6 +2521,10 @@ class MySQLDialect(default.DefaultDialect):
2520
2521
  # allow for the "true" and "false" keywords, however
2521
2522
  supports_native_boolean = False
2522
2523
 
2524
+ # support for BIT type; mysqlconnector coerces result values automatically,
2525
+ # all other MySQL DBAPIs require a conversion routine
2526
+ supports_native_bit = False
2527
+
2523
2528
  # identifiers are 64, however aliases can be 255...
2524
2529
  max_identifier_length = 255
2525
2530
  max_index_name_length = 64
@@ -2721,10 +2726,12 @@ class MySQLDialect(default.DefaultDialect):
2721
2726
  % (".".join(map(str, server_version_info)),)
2722
2727
  )
2723
2728
  if is_mariadb:
2724
- self.preparer = MariaDBIdentifierPreparer
2725
- # this would have been set by the default dialect already,
2726
- # so set it again
2727
- self.identifier_preparer = self.preparer(self)
2729
+
2730
+ if not issubclass(self.preparer, MariaDBIdentifierPreparer):
2731
+ self.preparer = MariaDBIdentifierPreparer
2732
+ # this would have been set by the default dialect already,
2733
+ # so set it again
2734
+ self.identifier_preparer = self.preparer(self)
2728
2735
 
2729
2736
  # this will be updated on first connect in initialize()
2730
2737
  # if using older mariadb version
@@ -46,16 +46,22 @@ class MariaDBDialect(MySQLDialect):
46
46
 
47
47
 
48
48
  def loader(driver):
49
- driver_mod = __import__(
49
+ dialect_mod = __import__(
50
50
  "sqlalchemy.dialects.mysql.%s" % driver
51
51
  ).dialects.mysql
52
- driver_cls = getattr(driver_mod, driver).dialect
53
-
54
- return type(
55
- "MariaDBDialect_%s" % driver,
56
- (
57
- MariaDBDialect,
58
- driver_cls,
59
- ),
60
- {"supports_statement_cache": True},
61
- )
52
+
53
+ driver_mod = getattr(dialect_mod, driver)
54
+ if hasattr(driver_mod, "mariadb_dialect"):
55
+ driver_cls = driver_mod.mariadb_dialect
56
+ return driver_cls
57
+ else:
58
+ driver_cls = driver_mod.dialect
59
+
60
+ return type(
61
+ "MariaDBDialect_%s" % driver,
62
+ (
63
+ MariaDBDialect,
64
+ driver_cls,
65
+ ),
66
+ {"supports_statement_cache": True},
67
+ )
@@ -14,24 +14,51 @@ r"""
14
14
  :connectstring: mysql+mysqlconnector://<user>:<password>@<host>[:<port>]/<dbname>
15
15
  :url: https://pypi.org/project/mysql-connector-python/
16
16
 
17
- .. note::
17
+ Driver Status
18
+ -------------
19
+
20
+ MySQL Connector/Python is supported as of SQLAlchemy 2.0.39 to the
21
+ degree which the driver is functional. There are still ongoing issues
22
+ with features such as server side cursors which remain disabled until
23
+ upstream issues are repaired.
24
+
25
+ .. versionchanged:: 2.0.39
26
+
27
+ The MySQL Connector/Python dialect has been updated to support the
28
+ latest version of this DBAPI. Previously, MySQL Connector/Python
29
+ was not fully supported.
30
+
31
+ Connecting to MariaDB with MySQL Connector/Python
32
+ --------------------------------------------------
33
+
34
+ MySQL Connector/Python may attempt to pass an incompatible collation to the
35
+ database when connecting to MariaDB. Experimentation has shown that using
36
+ ``?charset=utf8mb4&collation=utfmb4_general_ci`` or similar MariaDB-compatible
37
+ charset/collation will allow connectivity.
18
38
 
19
- The MySQL Connector/Python DBAPI has had many issues since its release,
20
- some of which may remain unresolved, and the mysqlconnector dialect is
21
- **not tested as part of SQLAlchemy's continuous integration**.
22
- The recommended MySQL dialects are mysqlclient and PyMySQL.
23
39
 
24
40
  """ # noqa
25
41
 
26
42
  import re
27
43
 
28
44
  from .base import BIT
45
+ from .base import MariaDBIdentifierPreparer
29
46
  from .base import MySQLCompiler
30
47
  from .base import MySQLDialect
48
+ from .base import MySQLExecutionContext
31
49
  from .base import MySQLIdentifierPreparer
50
+ from .mariadb import MariaDBDialect
32
51
  from ... import util
33
52
 
34
53
 
54
+ class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext):
55
+ def create_server_side_cursor(self):
56
+ return self._dbapi_connection.cursor(buffered=False)
57
+
58
+ def create_default_cursor(self):
59
+ return self._dbapi_connection.cursor(buffered=True)
60
+
61
+
35
62
  class MySQLCompiler_mysqlconnector(MySQLCompiler):
36
63
  def visit_mod_binary(self, binary, operator, **kw):
37
64
  return (
@@ -41,7 +68,7 @@ class MySQLCompiler_mysqlconnector(MySQLCompiler):
41
68
  )
42
69
 
43
70
 
44
- class MySQLIdentifierPreparer_mysqlconnector(MySQLIdentifierPreparer):
71
+ class IdentifierPreparerCommon_mysqlconnector:
45
72
  @property
46
73
  def _double_percents(self):
47
74
  return False
@@ -55,6 +82,18 @@ class MySQLIdentifierPreparer_mysqlconnector(MySQLIdentifierPreparer):
55
82
  return value
56
83
 
57
84
 
85
+ class MySQLIdentifierPreparer_mysqlconnector(
86
+ IdentifierPreparerCommon_mysqlconnector, MySQLIdentifierPreparer
87
+ ):
88
+ pass
89
+
90
+
91
+ class MariaDBIdentifierPreparer_mysqlconnector(
92
+ IdentifierPreparerCommon_mysqlconnector, MariaDBIdentifierPreparer
93
+ ):
94
+ pass
95
+
96
+
58
97
  class _myconnpyBIT(BIT):
59
98
  def result_processor(self, dialect, coltype):
60
99
  """MySQL-connector already converts mysql bits, so."""
@@ -71,9 +110,16 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
71
110
 
72
111
  supports_native_decimal = True
73
112
 
113
+ supports_native_bit = True
114
+
115
+ # not until https://bugs.mysql.com/bug.php?id=117548
116
+ supports_server_side_cursors = False
117
+
74
118
  default_paramstyle = "format"
75
119
  statement_compiler = MySQLCompiler_mysqlconnector
76
120
 
121
+ execution_ctx_cls = MySQLExecutionContext_mysqlconnector
122
+
77
123
  preparer = MySQLIdentifierPreparer_mysqlconnector
78
124
 
79
125
  colspecs = util.update_copy(MySQLDialect.colspecs, {BIT: _myconnpyBIT})
@@ -111,9 +157,13 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
111
157
  util.coerce_kw_type(opts, "use_pure", bool)
112
158
  util.coerce_kw_type(opts, "use_unicode", bool)
113
159
 
114
- # unfortunately, MySQL/connector python refuses to release a
115
- # cursor without reading fully, so non-buffered isn't an option
116
- opts.setdefault("buffered", True)
160
+ # note that "buffered" is set to False by default in MySQL/connector
161
+ # python. If you set it to True, then there is no way to get a server
162
+ # side cursor because the logic is written to disallow that.
163
+
164
+ # leaving this at True until
165
+ # https://bugs.mysql.com/bug.php?id=117548 can be fixed
166
+ opts["buffered"] = True
117
167
 
118
168
  # FOUND_ROWS must be set in ClientFlag to enable
119
169
  # supports_sane_rowcount.
@@ -128,6 +178,7 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
128
178
  opts["client_flags"] = client_flags
129
179
  except Exception:
130
180
  pass
181
+
131
182
  return [[], opts]
132
183
 
133
184
  @util.memoized_property
@@ -145,7 +196,11 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
145
196
 
146
197
  def is_disconnect(self, e, connection, cursor):
147
198
  errnos = (2006, 2013, 2014, 2045, 2055, 2048)
148
- exceptions = (self.dbapi.OperationalError, self.dbapi.InterfaceError)
199
+ exceptions = (
200
+ self.dbapi.OperationalError,
201
+ self.dbapi.InterfaceError,
202
+ self.dbapi.ProgrammingError,
203
+ )
149
204
  if isinstance(e, exceptions):
150
205
  return (
151
206
  e.errno in errnos
@@ -161,20 +216,30 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
161
216
  def _compat_fetchone(self, rp, charset=None):
162
217
  return rp.fetchone()
163
218
 
164
- _isolation_lookup = {
165
- "SERIALIZABLE",
166
- "READ UNCOMMITTED",
167
- "READ COMMITTED",
168
- "REPEATABLE READ",
169
- "AUTOCOMMIT",
170
- }
219
+ def get_isolation_level_values(self, dbapi_connection):
220
+ return (
221
+ "SERIALIZABLE",
222
+ "READ UNCOMMITTED",
223
+ "READ COMMITTED",
224
+ "REPEATABLE READ",
225
+ "AUTOCOMMIT",
226
+ )
171
227
 
172
- def _set_isolation_level(self, connection, level):
228
+ def set_isolation_level(self, connection, level):
173
229
  if level == "AUTOCOMMIT":
174
230
  connection.autocommit = True
175
231
  else:
176
232
  connection.autocommit = False
177
- super()._set_isolation_level(connection, level)
233
+ super().set_isolation_level(connection, level)
234
+
235
+
236
+ class MariaDBDialect_mysqlconnector(
237
+ MariaDBDialect, MySQLDialect_mysqlconnector
238
+ ):
239
+ supports_statement_cache = True
240
+ _allows_uuid_binds = False
241
+ preparer = MariaDBIdentifierPreparer_mysqlconnector
178
242
 
179
243
 
180
244
  dialect = MySQLDialect_mysqlconnector
245
+ mariadb_dialect = MariaDBDialect_mysqlconnector
@@ -42,6 +42,10 @@ def generate_driver_url(url, driver, query_str):
42
42
 
43
43
  if driver == "mariadbconnector":
44
44
  new_url = new_url.difference_update_query(["charset"])
45
+ elif driver == "mysqlconnector":
46
+ new_url = new_url.update_query_pairs(
47
+ [("collation", "utf8mb4_general_ci")]
48
+ )
45
49
 
46
50
  try:
47
51
  new_url.get_dialect()
@@ -451,7 +451,7 @@ class MySQLTableDefinitionParser:
451
451
  r"(?: +COLLATE +(?P<collate>[\w_]+))?"
452
452
  r"(?: +(?P<notnull>(?:NOT )?NULL))?"
453
453
  r"(?: +DEFAULT +(?P<default>"
454
- r"(?:NULL|'(?:''|[^'])*'|[\-\w\.\(\)]+"
454
+ r"(?:NULL|'(?:''|[^'])*'|\(.+?\)|[\-\w\.\(\)]+"
455
455
  r"(?: +ON UPDATE [\-\w\.\(\)]+)?)"
456
456
  r"))?"
457
457
  r"(?: +(?:GENERATED ALWAYS)? ?AS +(?P<generated>\("
@@ -374,12 +374,11 @@ class BIT(sqltypes.TypeEngine):
374
374
  self.length = length
375
375
 
376
376
  def result_processor(self, dialect, coltype):
377
- """Convert a MySQL's 64 bit, variable length binary string to a long.
377
+ """Convert a MySQL's 64 bit, variable length binary string to a
378
+ long."""
378
379
 
379
- TODO: this is MySQL-db, pyodbc specific. OurSQL and mysqlconnector
380
- already do this, so this logic should be moved to those dialects.
381
-
382
- """
380
+ if dialect.supports_native_bit:
381
+ return None
383
382
 
384
383
  def process(value):
385
384
  if value is not None:
@@ -4,15 +4,18 @@
4
4
  #
5
5
  # This module is part of SQLAlchemy and is released under
6
6
  # the MIT License: https://www.opensource.org/licenses/mit-license.php
7
- # mypy: ignore-errors
8
7
 
9
8
 
10
9
  from __future__ import annotations
11
10
 
12
11
  import re
13
- from typing import Any
12
+ from typing import Any as typing_Any
13
+ from typing import Iterable
14
14
  from typing import Optional
15
+ from typing import Sequence
16
+ from typing import TYPE_CHECKING
15
17
  from typing import TypeVar
18
+ from typing import Union
16
19
 
17
20
  from .operators import CONTAINED_BY
18
21
  from .operators import CONTAINS
@@ -21,28 +24,52 @@ from ... import types as sqltypes
21
24
  from ... import util
22
25
  from ...sql import expression
23
26
  from ...sql import operators
24
- from ...sql._typing import _TypeEngineArgument
25
-
26
-
27
- _T = TypeVar("_T", bound=Any)
28
-
29
-
30
- def Any(other, arrexpr, operator=operators.eq):
27
+ from ...sql.visitors import InternalTraversal
28
+
29
+ if 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
+
48
+
49
+ def Any(
50
+ other: typing_Any,
51
+ arrexpr: _ColumnExpressionArgument[_T],
52
+ operator: OperatorType = operators.eq,
53
+ ) -> ColumnElement[bool]:
31
54
  """A synonym for the ARRAY-level :meth:`.ARRAY.Comparator.any` method.
32
55
  See that method for details.
33
56
 
34
57
  """
35
58
 
36
- return arrexpr.any(other, operator)
59
+ return arrexpr.any(other, operator) # type: ignore[no-any-return, union-attr] # noqa: E501
37
60
 
38
61
 
39
- def All(other, arrexpr, operator=operators.eq):
62
+ def All(
63
+ other: typing_Any,
64
+ arrexpr: _ColumnExpressionArgument[_T],
65
+ operator: OperatorType = operators.eq,
66
+ ) -> ColumnElement[bool]:
40
67
  """A synonym for the ARRAY-level :meth:`.ARRAY.Comparator.all` method.
41
68
  See that method for details.
42
69
 
43
70
  """
44
71
 
45
- return arrexpr.all(other, operator)
72
+ return arrexpr.all(other, operator) # type: ignore[no-any-return, union-attr] # noqa: E501
46
73
 
47
74
 
48
75
  class array(expression.ExpressionClauseList[_T]):
@@ -66,11 +93,32 @@ class array(expression.ExpressionClauseList[_T]):
66
93
  ARRAY[%(param_3)s, %(param_4)s, %(param_5)s]) AS anon_1
67
94
 
68
95
  An instance of :class:`.array` will always have the datatype
69
- :class:`_types.ARRAY`. The "inner" type of the array is inferred from
70
- the values present, unless the ``type_`` keyword argument is passed::
96
+ :class:`_types.ARRAY`. The "inner" type of the array is inferred from the
97
+ values present, unless the :paramref:`_postgresql.array.type_` keyword
98
+ argument is passed::
71
99
 
72
100
  array(["foo", "bar"], type_=CHAR)
73
101
 
102
+ When constructing an empty array, the :paramref:`_postgresql.array.type_`
103
+ argument is particularly important as PostgreSQL server typically requires
104
+ a cast to be rendered for the inner type in order to render an empty array.
105
+ SQLAlchemy's compilation for the empty array will produce this cast so
106
+ that::
107
+
108
+ stmt = array([], type_=Integer)
109
+ print(stmt.compile(dialect=postgresql.dialect()))
110
+
111
+ Produces:
112
+
113
+ .. sourcecode:: sql
114
+
115
+ ARRAY[]::INTEGER[]
116
+
117
+ As required by PostgreSQL for empty arrays.
118
+
119
+ .. versionadded:: 2.0.40 added support to render empty PostgreSQL array
120
+ literals with a required cast.
121
+
74
122
  Multidimensional arrays are produced by nesting :class:`.array` constructs.
75
123
  The dimensionality of the final :class:`_types.ARRAY`
76
124
  type is calculated by
@@ -105,18 +153,33 @@ class array(expression.ExpressionClauseList[_T]):
105
153
  __visit_name__ = "array"
106
154
 
107
155
  stringify_dialect = "postgresql"
108
- inherit_cache = True
109
156
 
110
- def __init__(self, clauses, **kw):
111
- type_arg = kw.pop("type_", None)
112
- super().__init__(operators.comma_op, *clauses, **kw)
157
+ _traverse_internals: _TraverseInternalsType = [
158
+ ("clauses", InternalTraversal.dp_clauseelement_tuple),
159
+ ("type", InternalTraversal.dp_type),
160
+ ]
113
161
 
114
- self._type_tuple = [arg.type for arg in self.clauses]
162
+ def __init__(
163
+ self,
164
+ clauses: Iterable[_T],
165
+ *,
166
+ type_: Optional[_TypeEngineArgument[_T]] = None,
167
+ **kw: typing_Any,
168
+ ):
169
+ r"""Construct an ARRAY literal.
170
+
171
+ :param clauses: iterable, such as a list, containing elements to be
172
+ rendered in the array
173
+ :param type\_: optional type. If omitted, the type is inferred
174
+ from the contents of the array.
175
+
176
+ """
177
+ super().__init__(operators.comma_op, *clauses, **kw)
115
178
 
116
179
  main_type = (
117
- type_arg
118
- if type_arg is not None
119
- else self._type_tuple[0] if self._type_tuple else sqltypes.NULLTYPE
180
+ type_
181
+ if type_ is not None
182
+ else self.clauses[0].type if self.clauses else sqltypes.NULLTYPE
120
183
  )
121
184
 
122
185
  if isinstance(main_type, ARRAY):
@@ -127,15 +190,21 @@ class array(expression.ExpressionClauseList[_T]):
127
190
  if main_type.dimensions is not None
128
191
  else 2
129
192
  ),
130
- )
193
+ ) # type: ignore[assignment]
131
194
  else:
132
- self.type = ARRAY(main_type)
195
+ self.type = ARRAY(main_type) # type: ignore[assignment]
133
196
 
134
197
  @property
135
- def _select_iterable(self):
198
+ def _select_iterable(self) -> _SelectIterable:
136
199
  return (self,)
137
200
 
138
- def _bind_param(self, operator, obj, _assume_scalar=False, type_=None):
201
+ def _bind_param(
202
+ self,
203
+ operator: OperatorType,
204
+ obj: typing_Any,
205
+ type_: Optional[TypeEngine[_T]] = None,
206
+ _assume_scalar: bool = False,
207
+ ) -> BindParameter[_T]:
139
208
  if _assume_scalar or operator is operators.getitem:
140
209
  return expression.BindParameter(
141
210
  None,
@@ -154,16 +223,18 @@ class array(expression.ExpressionClauseList[_T]):
154
223
  )
155
224
  for o in obj
156
225
  ]
157
- )
226
+ ) # type: ignore[return-value]
158
227
 
159
- def self_group(self, against=None):
228
+ def self_group(
229
+ self, against: Optional[OperatorType] = None
230
+ ) -> Union[Self, Grouping[_T]]:
160
231
  if against in (operators.any_op, operators.all_op, operators.getitem):
161
232
  return expression.Grouping(self)
162
233
  else:
163
234
  return self
164
235
 
165
236
 
166
- class ARRAY(sqltypes.ARRAY):
237
+ class ARRAY(sqltypes.ARRAY[_T]):
167
238
  """PostgreSQL ARRAY type.
168
239
 
169
240
  The :class:`_postgresql.ARRAY` type is constructed in the same way
@@ -237,7 +308,7 @@ class ARRAY(sqltypes.ARRAY):
237
308
 
238
309
  def __init__(
239
310
  self,
240
- item_type: _TypeEngineArgument[Any],
311
+ item_type: _TypeEngineArgument[_T],
241
312
  as_tuple: bool = False,
242
313
  dimensions: Optional[int] = None,
243
314
  zero_indexes: bool = False,
@@ -286,7 +357,7 @@ class ARRAY(sqltypes.ARRAY):
286
357
  self.dimensions = dimensions
287
358
  self.zero_indexes = zero_indexes
288
359
 
289
- class Comparator(sqltypes.ARRAY.Comparator):
360
+ class Comparator(sqltypes.ARRAY.Comparator[_T]):
290
361
  """Define comparison operations for :class:`_types.ARRAY`.
291
362
 
292
363
  Note that these operations are in addition to those provided
@@ -296,7 +367,9 @@ class ARRAY(sqltypes.ARRAY):
296
367
 
297
368
  """
298
369
 
299
- def contains(self, other, **kwargs):
370
+ def contains(
371
+ self, other: typing_Any, **kwargs: typing_Any
372
+ ) -> ColumnElement[bool]:
300
373
  """Boolean expression. Test if elements are a superset of the
301
374
  elements of the argument array expression.
302
375
 
@@ -305,7 +378,7 @@ class ARRAY(sqltypes.ARRAY):
305
378
  """
306
379
  return self.operate(CONTAINS, other, result_type=sqltypes.Boolean)
307
380
 
308
- def contained_by(self, other):
381
+ def contained_by(self, other: typing_Any) -> ColumnElement[bool]:
309
382
  """Boolean expression. Test if elements are a proper subset of the
310
383
  elements of the argument array expression.
311
384
  """
@@ -313,7 +386,7 @@ class ARRAY(sqltypes.ARRAY):
313
386
  CONTAINED_BY, other, result_type=sqltypes.Boolean
314
387
  )
315
388
 
316
- def overlap(self, other):
389
+ def overlap(self, other: typing_Any) -> ColumnElement[bool]:
317
390
  """Boolean expression. Test if array has elements in common with
318
391
  an argument array expression.
319
392
  """
@@ -321,35 +394,26 @@ class ARRAY(sqltypes.ARRAY):
321
394
 
322
395
  comparator_factory = Comparator
323
396
 
324
- @property
325
- def hashable(self):
326
- return self.as_tuple
327
-
328
- @property
329
- def python_type(self):
330
- return list
331
-
332
- def compare_values(self, x, y):
333
- return x == y
334
-
335
397
  @util.memoized_property
336
- def _against_native_enum(self):
398
+ def _against_native_enum(self) -> bool:
337
399
  return (
338
400
  isinstance(self.item_type, sqltypes.Enum)
339
- and self.item_type.native_enum
401
+ and self.item_type.native_enum # type: ignore[attr-defined]
340
402
  )
341
403
 
342
- def literal_processor(self, dialect):
404
+ def literal_processor(
405
+ self, dialect: Dialect
406
+ ) -> Optional[_LiteralProcessorType[_T]]:
343
407
  item_proc = self.item_type.dialect_impl(dialect).literal_processor(
344
408
  dialect
345
409
  )
346
410
  if item_proc is None:
347
411
  return None
348
412
 
349
- def to_str(elements):
413
+ def to_str(elements: Iterable[typing_Any]) -> str:
350
414
  return f"ARRAY[{', '.join(elements)}]"
351
415
 
352
- def process(value):
416
+ def process(value: Sequence[typing_Any]) -> str:
353
417
  inner = self._apply_item_processor(
354
418
  value, item_proc, self.dimensions, to_str
355
419
  )
@@ -357,12 +421,16 @@ class ARRAY(sqltypes.ARRAY):
357
421
 
358
422
  return process
359
423
 
360
- def bind_processor(self, dialect):
424
+ def bind_processor(
425
+ self, dialect: Dialect
426
+ ) -> Optional[_BindProcessorType[Sequence[typing_Any]]]:
361
427
  item_proc = self.item_type.dialect_impl(dialect).bind_processor(
362
428
  dialect
363
429
  )
364
430
 
365
- def process(value):
431
+ def process(
432
+ value: Optional[Sequence[typing_Any]],
433
+ ) -> Optional[list[typing_Any]]:
366
434
  if value is None:
367
435
  return value
368
436
  else:
@@ -372,12 +440,16 @@ class ARRAY(sqltypes.ARRAY):
372
440
 
373
441
  return process
374
442
 
375
- def result_processor(self, dialect, coltype):
443
+ def result_processor(
444
+ self, dialect: Dialect, coltype: object
445
+ ) -> _ResultProcessorType[Sequence[typing_Any]]:
376
446
  item_proc = self.item_type.dialect_impl(dialect).result_processor(
377
447
  dialect, coltype
378
448
  )
379
449
 
380
- def process(value):
450
+ def process(
451
+ value: Sequence[typing_Any],
452
+ ) -> Optional[Sequence[typing_Any]]:
381
453
  if value is None:
382
454
  return value
383
455
  else:
@@ -392,11 +464,13 @@ class ARRAY(sqltypes.ARRAY):
392
464
  super_rp = process
393
465
  pattern = re.compile(r"^{(.*)}$")
394
466
 
395
- def handle_raw_string(value):
396
- inner = pattern.match(value).group(1)
467
+ def handle_raw_string(value: str) -> list[str]:
468
+ inner = pattern.match(value).group(1) # type: ignore[union-attr] # noqa: E501
397
469
  return _split_enum_values(inner)
398
470
 
399
- def process(value):
471
+ def process(
472
+ value: Sequence[typing_Any],
473
+ ) -> Optional[Sequence[typing_Any]]:
400
474
  if value is None:
401
475
  return value
402
476
  # isinstance(value, str) is required to handle
@@ -411,7 +485,7 @@ class ARRAY(sqltypes.ARRAY):
411
485
  return process
412
486
 
413
487
 
414
- def _split_enum_values(array_string):
488
+ def _split_enum_values(array_string: str) -> list[str]:
415
489
  if '"' not in array_string:
416
490
  # no escape char is present so it can just split on the comma
417
491
  return array_string.split(",") if array_string else []