SQLAlchemy 2.0.38__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 (59) hide show
  1. sqlalchemy/__init__.py +1 -1
  2. sqlalchemy/connectors/pyodbc.py +2 -4
  3. sqlalchemy/dialects/mysql/base.py +40 -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 +53 -9
  11. sqlalchemy/dialects/postgresql/json.py +51 -22
  12. sqlalchemy/dialects/postgresql/types.py +14 -4
  13. sqlalchemy/dialects/sqlite/base.py +27 -10
  14. sqlalchemy/engine/cursor.py +6 -1
  15. sqlalchemy/engine/default.py +27 -12
  16. sqlalchemy/engine/interfaces.py +11 -4
  17. sqlalchemy/engine/result.py +3 -3
  18. sqlalchemy/ext/asyncio/base.py +7 -7
  19. sqlalchemy/ext/asyncio/engine.py +5 -3
  20. sqlalchemy/ext/asyncio/result.py +1 -0
  21. sqlalchemy/ext/asyncio/session.py +21 -2
  22. sqlalchemy/orm/context.py +102 -34
  23. sqlalchemy/orm/decl_api.py +1 -1
  24. sqlalchemy/orm/decl_base.py +1 -1
  25. sqlalchemy/orm/exc.py +9 -0
  26. sqlalchemy/orm/mapper.py +5 -4
  27. sqlalchemy/orm/properties.py +12 -3
  28. sqlalchemy/orm/query.py +11 -4
  29. sqlalchemy/orm/util.py +6 -5
  30. sqlalchemy/pool/base.py +2 -1
  31. sqlalchemy/sql/_elements_constructors.py +14 -4
  32. sqlalchemy/sql/_selectable_constructors.py +94 -16
  33. sqlalchemy/sql/_typing.py +4 -1
  34. sqlalchemy/sql/base.py +4 -1
  35. sqlalchemy/sql/coercions.py +1 -1
  36. sqlalchemy/sql/compiler.py +256 -137
  37. sqlalchemy/sql/crud.py +12 -3
  38. sqlalchemy/sql/ddl.py +105 -52
  39. sqlalchemy/sql/dml.py +10 -0
  40. sqlalchemy/sql/elements.py +37 -19
  41. sqlalchemy/sql/functions.py +42 -7
  42. sqlalchemy/sql/schema.py +23 -5
  43. sqlalchemy/sql/selectable.py +38 -34
  44. sqlalchemy/sql/sqltypes.py +113 -40
  45. sqlalchemy/sql/type_api.py +20 -1
  46. sqlalchemy/sql/util.py +1 -1
  47. sqlalchemy/testing/assertions.py +2 -2
  48. sqlalchemy/testing/requirements.py +19 -0
  49. sqlalchemy/testing/suite/test_reflection.py +48 -0
  50. sqlalchemy/testing/suite/test_results.py +2 -0
  51. sqlalchemy/testing/suite/test_select.py +9 -0
  52. sqlalchemy/testing/suite/test_types.py +4 -0
  53. sqlalchemy/util/_collections.py +3 -1
  54. sqlalchemy/util/typing.py +64 -21
  55. {SQLAlchemy-2.0.38.dist-info → sqlalchemy-2.0.40.dist-info}/METADATA +10 -9
  56. {SQLAlchemy-2.0.38.dist-info → sqlalchemy-2.0.40.dist-info}/RECORD +59 -59
  57. {SQLAlchemy-2.0.38.dist-info → sqlalchemy-2.0.40.dist-info}/WHEEL +1 -1
  58. {SQLAlchemy-2.0.38.dist-info → sqlalchemy-2.0.40.dist-info/licenses}/LICENSE +0 -0
  59. {SQLAlchemy-2.0.38.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.38"
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,
@@ -182,6 +182,31 @@ For fully atomic transactions as well as support for foreign key
182
182
  constraints, all participating ``CREATE TABLE`` statements must specify a
183
183
  transactional engine, which in the vast majority of cases is ``InnoDB``.
184
184
 
185
+ Partitioning can similarly be specified using similar options.
186
+ In the example below the create table will specify ``PARTITION_BY``,
187
+ ``PARTITIONS``, ``SUBPARTITIONS`` and ``SUBPARTITION_BY``::
188
+
189
+ # can also use mariadb_* prefix
190
+ Table(
191
+ "testtable",
192
+ MetaData(),
193
+ Column("id", Integer(), primary_key=True, autoincrement=True),
194
+ Column("other_id", Integer(), primary_key=True, autoincrement=False),
195
+ mysql_partitions="2",
196
+ mysql_partition_by="KEY(other_id)",
197
+ mysql_subpartition_by="HASH(some_expr)",
198
+ mysql_subpartitions="2",
199
+ )
200
+
201
+ This will render:
202
+
203
+ .. sourcecode:: sql
204
+
205
+ CREATE TABLE testtable (
206
+ id INTEGER NOT NULL AUTO_INCREMENT,
207
+ other_id INTEGER NOT NULL,
208
+ PRIMARY KEY (id, other_id)
209
+ )PARTITION BY KEY(other_id) PARTITIONS 2 SUBPARTITION BY HASH(some_expr) SUBPARTITIONS 2
185
210
 
186
211
  Case Sensitivity and Table Reflection
187
212
  -------------------------------------
@@ -1903,12 +1928,13 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
1903
1928
  colspec.append("AUTO_INCREMENT")
1904
1929
  else:
1905
1930
  default = self.get_column_default_string(column)
1931
+
1906
1932
  if default is not None:
1907
1933
  if (
1908
- isinstance(
1909
- column.server_default.arg, functions.FunctionElement
1910
- )
1911
- 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)
1912
1938
  ):
1913
1939
  colspec.append(f"DEFAULT ({default})")
1914
1940
  else:
@@ -2495,6 +2521,10 @@ class MySQLDialect(default.DefaultDialect):
2495
2521
  # allow for the "true" and "false" keywords, however
2496
2522
  supports_native_boolean = False
2497
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
+
2498
2528
  # identifiers are 64, however aliases can be 255...
2499
2529
  max_identifier_length = 255
2500
2530
  max_index_name_length = 64
@@ -2696,10 +2726,12 @@ class MySQLDialect(default.DefaultDialect):
2696
2726
  % (".".join(map(str, server_version_info)),)
2697
2727
  )
2698
2728
  if is_mariadb:
2699
- self.preparer = MariaDBIdentifierPreparer
2700
- # this would have been set by the default dialect already,
2701
- # so set it again
2702
- 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)
2703
2735
 
2704
2736
  # this will be updated on first connect in initialize()
2705
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: