SQLAlchemy 2.1.0b3__py3-none-any.whl → 2.1.0rc2__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 (137) hide show
  1. sqlalchemy/__init__.py +2 -1
  2. sqlalchemy/connectors/asyncio.py +1 -1
  3. sqlalchemy/connectors/pyodbc.py +5 -16
  4. sqlalchemy/dialects/__init__.py +1 -1
  5. sqlalchemy/dialects/mssql/base.py +98 -50
  6. sqlalchemy/dialects/mssql/mssqlpython.py +35 -19
  7. sqlalchemy/dialects/mssql/pymssql.py +4 -1
  8. sqlalchemy/dialects/mssql/pyodbc.py +4 -4
  9. sqlalchemy/dialects/mysql/aiomysql.py +11 -2
  10. sqlalchemy/dialects/mysql/asyncmy.py +7 -2
  11. sqlalchemy/dialects/mysql/base.py +10 -9
  12. sqlalchemy/dialects/mysql/enumerated.py +1 -1
  13. sqlalchemy/dialects/mysql/mariadbconnector.py +5 -22
  14. sqlalchemy/dialects/mysql/mysqlconnector.py +5 -11
  15. sqlalchemy/dialects/mysql/mysqldb.py +21 -16
  16. sqlalchemy/dialects/mysql/pymysql.py +10 -0
  17. sqlalchemy/dialects/oracle/base.py +15 -13
  18. sqlalchemy/dialects/oracle/cx_oracle.py +171 -37
  19. sqlalchemy/dialects/oracle/oracledb.py +15 -27
  20. sqlalchemy/dialects/postgresql/_psycopg_common.py +3 -0
  21. sqlalchemy/dialects/postgresql/asyncpg.py +5 -13
  22. sqlalchemy/dialects/postgresql/base.py +261 -36
  23. sqlalchemy/dialects/postgresql/hstore.py +1 -1
  24. sqlalchemy/dialects/postgresql/json.py +3 -0
  25. sqlalchemy/dialects/postgresql/named_types.py +14 -0
  26. sqlalchemy/dialects/postgresql/pg8000.py +4 -17
  27. sqlalchemy/dialects/postgresql/pg_catalog.py +1 -0
  28. sqlalchemy/dialects/postgresql/provision.py +18 -0
  29. sqlalchemy/dialects/postgresql/psycopg.py +13 -13
  30. sqlalchemy/dialects/postgresql/psycopg2.py +11 -12
  31. sqlalchemy/dialects/postgresql/ranges.py +7 -7
  32. sqlalchemy/dialects/sqlite/aiosqlite.py +53 -0
  33. sqlalchemy/dialects/sqlite/base.py +131 -41
  34. sqlalchemy/dialects/sqlite/dml.py +47 -12
  35. sqlalchemy/dialects/sqlite/provision.py +8 -0
  36. sqlalchemy/dialects/sqlite/pysqlcipher.py +6 -0
  37. sqlalchemy/dialects/sqlite/pysqlite.py +219 -15
  38. sqlalchemy/engine/_result_cy.py +8 -8
  39. sqlalchemy/engine/create.py +2 -2
  40. sqlalchemy/engine/cursor.py +2 -2
  41. sqlalchemy/engine/default.py +115 -6
  42. sqlalchemy/engine/events.py +2 -2
  43. sqlalchemy/engine/interfaces.py +143 -2
  44. sqlalchemy/engine/reflection.py +44 -4
  45. sqlalchemy/engine/result.py +4 -4
  46. sqlalchemy/engine/url.py +1 -1
  47. sqlalchemy/engine/util.py +15 -7
  48. sqlalchemy/exc.py +16 -0
  49. sqlalchemy/ext/associationproxy.py +8 -8
  50. sqlalchemy/ext/asyncio/base.py +2 -2
  51. sqlalchemy/ext/asyncio/engine.py +9 -7
  52. sqlalchemy/ext/asyncio/exc.py +9 -0
  53. sqlalchemy/ext/asyncio/result.py +4 -4
  54. sqlalchemy/ext/asyncio/scoping.py +124 -16
  55. sqlalchemy/ext/asyncio/session.py +271 -9
  56. sqlalchemy/ext/hybrid.py +6 -6
  57. sqlalchemy/inspection.py +14 -1
  58. sqlalchemy/log.py +3 -7
  59. sqlalchemy/orm/_typing.py +5 -1
  60. sqlalchemy/orm/attributes.py +5 -5
  61. sqlalchemy/orm/base.py +17 -2
  62. sqlalchemy/orm/bulk_persistence.py +16 -0
  63. sqlalchemy/orm/clsregistry.py +1 -1
  64. sqlalchemy/orm/collections.py +2 -2
  65. sqlalchemy/orm/decl_api.py +57 -5
  66. sqlalchemy/orm/decl_base.py +18 -24
  67. sqlalchemy/orm/descriptor_props.py +8 -8
  68. sqlalchemy/orm/exc.py +1 -1
  69. sqlalchemy/orm/identity.py +1 -1
  70. sqlalchemy/orm/instrumentation.py +7 -3
  71. sqlalchemy/orm/interfaces.py +10 -3
  72. sqlalchemy/orm/loading.py +21 -2
  73. sqlalchemy/orm/mapped_collection.py +2 -2
  74. sqlalchemy/orm/mapper.py +32 -7
  75. sqlalchemy/orm/path_registry.py +100 -22
  76. sqlalchemy/orm/properties.py +5 -5
  77. sqlalchemy/orm/query.py +35 -35
  78. sqlalchemy/orm/relationships.py +43 -26
  79. sqlalchemy/orm/scoping.py +24 -6
  80. sqlalchemy/orm/session.py +72 -45
  81. sqlalchemy/orm/state.py +2 -2
  82. sqlalchemy/orm/strategies.py +31 -20
  83. sqlalchemy/orm/strategy_options.py +103 -28
  84. sqlalchemy/orm/util.py +17 -7
  85. sqlalchemy/orm/writeonly.py +7 -0
  86. sqlalchemy/pool/base.py +70 -48
  87. sqlalchemy/schema.py +1 -0
  88. sqlalchemy/sql/_cache_key_cy.py +363 -0
  89. sqlalchemy/sql/_elements_constructors.py +29 -3
  90. sqlalchemy/sql/_typing.py +16 -3
  91. sqlalchemy/sql/_util_cy.pxd +11 -0
  92. sqlalchemy/sql/_util_cy.py +1 -1
  93. sqlalchemy/sql/annotation.py +6 -6
  94. sqlalchemy/sql/base.py +13 -9
  95. sqlalchemy/sql/cache_key.py +132 -283
  96. sqlalchemy/sql/compiler.py +158 -21
  97. sqlalchemy/sql/crud.py +5 -5
  98. sqlalchemy/sql/ddl.py +59 -25
  99. sqlalchemy/sql/default_comparator.py +8 -2
  100. sqlalchemy/sql/elements.py +56 -30
  101. sqlalchemy/sql/functions.py +8 -8
  102. sqlalchemy/sql/lambdas.py +1 -1
  103. sqlalchemy/sql/naming.py +1 -6
  104. sqlalchemy/sql/operators.py +18 -6
  105. sqlalchemy/sql/schema.py +563 -190
  106. sqlalchemy/sql/selectable.py +107 -15
  107. sqlalchemy/sql/sqltypes.py +47 -10
  108. sqlalchemy/sql/traversals.py +6 -6
  109. sqlalchemy/sql/type_api.py +14 -10
  110. sqlalchemy/sql/util.py +14 -14
  111. sqlalchemy/sql/visitors.py +8 -12
  112. sqlalchemy/testing/__init__.py +1 -0
  113. sqlalchemy/testing/cancellation.py +237 -0
  114. sqlalchemy/testing/engines.py +25 -11
  115. sqlalchemy/testing/exclusions.py +28 -3
  116. sqlalchemy/testing/profiles_file.py +350 -0
  117. sqlalchemy/testing/profiling.py +98 -132
  118. sqlalchemy/testing/provision.py +21 -0
  119. sqlalchemy/testing/requirements.py +20 -27
  120. sqlalchemy/testing/suite/test_reflection.py +264 -0
  121. sqlalchemy/testing/util.py +41 -0
  122. sqlalchemy/util/__init__.py +8 -1
  123. sqlalchemy/util/_collections.py +2 -2
  124. sqlalchemy/util/_has_cython.py +2 -0
  125. sqlalchemy/util/compat.py +0 -1
  126. sqlalchemy/util/concurrency.py +2 -52
  127. sqlalchemy/util/cython.py +17 -1
  128. sqlalchemy/util/deprecations.py +1 -1
  129. sqlalchemy/util/langhelpers.py +487 -13
  130. sqlalchemy/util/typing.py +6 -6
  131. {sqlalchemy-2.1.0b3.dist-info → sqlalchemy-2.1.0rc2.dist-info}/METADATA +8 -7
  132. sqlalchemy-2.1.0rc2.dist-info/RECORD +268 -0
  133. {sqlalchemy-2.1.0b3.dist-info → sqlalchemy-2.1.0rc2.dist-info}/WHEEL +1 -1
  134. sqlalchemy-2.1.0rc2.dist-info/licenses/AUTHORS +30 -0
  135. sqlalchemy-2.1.0b3.dist-info/RECORD +0 -263
  136. {sqlalchemy-2.1.0b3.dist-info → sqlalchemy-2.1.0rc2.dist-info}/licenses/LICENSE +0 -0
  137. {sqlalchemy-2.1.0b3.dist-info → sqlalchemy-2.1.0rc2.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py CHANGED
@@ -74,6 +74,7 @@ from .schema import ExecutableDDLElement as ExecutableDDLElement
74
74
  from .schema import FetchedValue as FetchedValue
75
75
  from .schema import ForeignKey as ForeignKey
76
76
  from .schema import ForeignKeyConstraint as ForeignKeyConstraint
77
+ from .schema import ForeignKeyTarget as ForeignKeyTarget
77
78
  from .schema import Identity as Identity
78
79
  from .schema import Index as Index
79
80
  from .schema import insert_sentinel as insert_sentinel
@@ -284,7 +285,7 @@ from .types import Uuid as Uuid
284
285
  from .types import VARBINARY as VARBINARY
285
286
  from .types import VARCHAR as VARCHAR
286
287
 
287
- __version__ = "2.1.0b3"
288
+ __version__ = "2.1.0rc2"
288
289
 
289
290
 
290
291
  def __go(lcls: Any) -> None:
@@ -337,7 +337,7 @@ class AsyncAdapt_dbapi_ss_cursor(AsyncAdapt_dbapi_cursor):
337
337
  def close(self) -> None:
338
338
  if self._cursor is not None:
339
339
  await_(self._cursor.close())
340
- self._cursor = None # type: ignore
340
+ self._cursor = None # type: ignore[assignment]
341
341
 
342
342
  def fetchone(self) -> Optional[Any]:
343
343
  return await_(self._cursor.fetchone())
@@ -164,25 +164,14 @@ class PyODBCConnector(Connector):
164
164
  else:
165
165
  return False
166
166
 
167
- def _dbapi_version(self) -> interfaces.VersionInfoType:
168
- if not self.dbapi:
169
- return ()
170
- return self._parse_dbapi_version(self.dbapi.version)
171
-
172
- def _parse_dbapi_version(self, vers: str) -> interfaces.VersionInfoType:
173
- m = re.match(r"(?:py.*-)?([\d\.]+)(?:-(\w+))?", vers)
174
- if not m:
175
- return ()
176
- vers_tuple: interfaces.VersionInfoType = tuple(
177
- [int(x) for x in m.group(1).split(".")]
178
- )
179
- if m.group(2):
180
- vers_tuple += (m.group(2),)
181
- return vers_tuple
167
+ def retrieve_dbapi_version(
168
+ self, dbapi: interfaces.DBAPIModule
169
+ ) -> util.VersionInfo:
170
+ return util.parse_version_string(dbapi.version)
182
171
 
183
172
  def _get_server_version_info(
184
173
  self, connection: Connection
185
- ) -> interfaces.VersionInfoType:
174
+ ) -> interfaces.ServerVersionInfoType:
186
175
  # NOTE: this function is not reliable, particularly when
187
176
  # freetds is in use. Implement database-specific server version
188
177
  # queries.
@@ -43,7 +43,7 @@ def _auto_fn(name: str) -> Optional[Callable[[], Type[Dialect]]]:
43
43
  module: Any = __import__(
44
44
  "sqlalchemy.dialects.mysql.mariadb"
45
45
  ).dialects.mysql.mariadb
46
- return module.loader(driver) # type: ignore
46
+ return module.loader(driver) # type: ignore[no-any-return]
47
47
  else:
48
48
  module = __import__("sqlalchemy.dialects.%s" % (dialect,)).dialects
49
49
  module = getattr(module, dialect)
@@ -1561,7 +1561,7 @@ class MSUUid(sqltypes.Uuid):
1561
1561
  if self.native_uuid:
1562
1562
 
1563
1563
  def process(value):
1564
- return f"""'{str(value).replace("''", "'")}'"""
1564
+ return f"""'{str(value).replace("'", "''")}'"""
1565
1565
 
1566
1566
  return process
1567
1567
  else:
@@ -3373,11 +3373,29 @@ class MSDialect(default._BackendsMultiReflection, default.DefaultDialect):
3373
3373
  else:
3374
3374
  return self.schema_name
3375
3375
 
3376
+ @reflection.cache
3376
3377
  @_db_plus_owner
3377
3378
  def has_table(self, connection, tablename, dbname, owner, schema, **kw):
3378
3379
  self._ensure_has_table_connection(connection)
3379
3380
 
3380
- return self._internal_has_table(connection, tablename, owner, **kw)
3381
+ return self._internal_has_multi_table(
3382
+ connection, [tablename], owner, schema, **kw
3383
+ )[schema, tablename]
3384
+
3385
+ def has_multi_table(self, connection, table_names, schema=None, **kw):
3386
+ # this does not really matche the signature of _db_plus_owner_multi
3387
+ # so manually do the unpacking here
3388
+ dbname, owner = _owner_plus_db(self, schema)
3389
+ return _switch_db(
3390
+ dbname,
3391
+ connection,
3392
+ self._internal_has_multi_table,
3393
+ connection,
3394
+ table_names,
3395
+ owner,
3396
+ schema,
3397
+ **kw,
3398
+ ).items()
3381
3399
 
3382
3400
  @reflection.cache
3383
3401
  @_db_plus_owner
@@ -3452,37 +3470,38 @@ class MSDialect(default._BackendsMultiReflection, default.DefaultDialect):
3452
3470
  view_names = [r[0] for r in connection.execute(s)]
3453
3471
  return view_names
3454
3472
 
3455
- @reflection.cache
3456
- def _internal_has_table(self, connection, tablename, owner, **kw):
3457
- if tablename.startswith("#"): # temporary table
3458
- # mssql does not support temporary views
3459
- # SQL Error [4103] [S0001]: "#v": Temporary views are not allowed
3460
- return bool(
3461
- connection.scalar(
3462
- # U filters on user tables only.
3463
- text("SELECT object_id(:table_name, 'U')"),
3464
- {"table_name": f"tempdb.dbo.[{tablename}]"},
3465
- )
3466
- )
3467
- else:
3468
- tables = ischema.tables
3469
-
3470
- s = sql.select(tables.c.table_name).where(
3471
- sql.and_(
3472
- sql.or_(
3473
- tables.c.table_type == "BASE TABLE",
3474
- tables.c.table_type == "VIEW",
3475
- ),
3476
- tables.c.table_name == tablename,
3477
- )
3473
+ def _internal_has_multi_table(
3474
+ self, connection, table_names, owner, schema, **kw
3475
+ ):
3476
+ regular_names, multi_object_names, temp_names = (
3477
+ self._partition_filter_names(
3478
+ connection, owner, table_names, ObjectScope.ANY, ObjectKind.ANY
3478
3479
  )
3480
+ )
3479
3481
 
3480
- if owner:
3481
- s = s.where(tables.c.table_schema == owner)
3482
+ result = {}
3483
+ if multi_object_names:
3484
+ # in multi_object_names we already have the names that exist in
3485
+ # the database, no need to do another query here
3486
+ name_map = self._multi_name_map(regular_names)
3487
+ for server_name in multi_object_names:
3488
+ result[(schema, name_map(server_name))] = True
3482
3489
 
3483
- c = connection.execute(s)
3490
+ if temp_names:
3491
+ # mssql does not support temporary views
3492
+ # U filters on user tables only.
3493
+ query = text("SELECT object_id(:table_name, 'U')")
3494
+ for temp_name in temp_names:
3495
+ result[(schema, temp_name)] = bool(
3496
+ connection.scalar(
3497
+ query,
3498
+ {"table_name": f"tempdb..[{temp_name}]"},
3499
+ )
3500
+ )
3484
3501
 
3485
- return c.first() is not None
3502
+ for name in table_names:
3503
+ result.setdefault((schema, name), False)
3504
+ return result
3486
3505
 
3487
3506
  @reflection.cache
3488
3507
  @_db_plus_owner
@@ -3570,14 +3589,20 @@ class MSDialect(default._BackendsMultiReflection, default.DefaultDialect):
3570
3589
 
3571
3590
  if coltype in (MSBinary, MSVarBinary, sqltypes.LargeBinary):
3572
3591
  kwargs["length"] = maxlen if maxlen != -1 else None
3573
- elif coltype in (MSString, MSChar, MSText):
3592
+ elif coltype in (MSString, MSChar):
3574
3593
  kwargs["length"] = maxlen if maxlen != -1 else None
3575
3594
  if collation:
3576
3595
  kwargs["collation"] = collation
3577
- elif coltype in (MSNVarchar, MSNChar, MSNText):
3596
+ elif coltype in (MSNVarchar, MSNChar):
3578
3597
  kwargs["length"] = maxlen // 2 if maxlen != -1 else None
3579
3598
  if collation:
3580
3599
  kwargs["collation"] = collation
3600
+ elif coltype in (MSText, MSNText):
3601
+ # TEXT / NTEXT are unlengthed LOB types. sys.columns.max_length
3602
+ # reports 16 for these, which is the size of the in-row LOB
3603
+ # pointer and not a character length, so no length is applied.
3604
+ if collation:
3605
+ kwargs["collation"] = collation
3581
3606
 
3582
3607
  if coltype is None:
3583
3608
  if base_type is not None and base_type != type_:
@@ -3853,6 +3878,34 @@ index_info AS (
3853
3878
 
3854
3879
  # --- multi-reflection API ---
3855
3880
 
3881
+ @lru_cache
3882
+ def _partition_filter_names_query(self, add_filter_names: bool):
3883
+ sys_objects = ischema.sys_objects
3884
+ sys_schemas = ischema.sys_schemas
3885
+
3886
+ s = (
3887
+ sql.select(sys_objects.c.name)
3888
+ .select_from(sys_objects)
3889
+ .join(
3890
+ sys_schemas,
3891
+ onclause=sys_objects.c.schema_id == sys_schemas.c.schema_id,
3892
+ )
3893
+ .where(
3894
+ sys_schemas.c.name == sql.bindparam("schema"),
3895
+ sys_objects.c.type.in_(
3896
+ sql.bindparam("type_filter", expanding=True)
3897
+ ),
3898
+ )
3899
+ )
3900
+ if add_filter_names:
3901
+ s = s.where(
3902
+ sys_objects.c.name.in_(
3903
+ sql.bindparam("filter_names", expanding=True)
3904
+ )
3905
+ )
3906
+
3907
+ return s
3908
+
3856
3909
  def _partition_filter_names(
3857
3910
  self, connection, owner, filter_names, scope, kind
3858
3911
  ):
@@ -3914,29 +3967,20 @@ index_info AS (
3914
3967
 
3915
3968
  type_filter = []
3916
3969
  if ObjectKind.TABLE in kind:
3917
- type_filter.append("'U'")
3970
+ type_filter.append("U")
3918
3971
  if ObjectKind.VIEW in kind:
3919
- type_filter.append("'V'")
3972
+ type_filter.append("V")
3920
3973
  # SQL Server does not support materialized views, so ignore them
3921
3974
  if not type_filter:
3922
3975
  return (regular_names, [], temp_names)
3923
3976
 
3924
- query = (
3925
- "SELECT o.name "
3926
- "FROM sys.objects o "
3927
- "JOIN sys.schemas s ON o.schema_id = s.schema_id "
3928
- "WHERE s.name = :owner "
3929
- "AND o.type IN (%s)" % ", ".join(type_filter)
3930
- )
3931
- params = [sql.bindparam("owner", owner, ischema.CoerceUnicode())]
3932
- if regular_names:
3933
- query += " AND o.name IN :filter_names"
3934
- params.append(
3935
- sql.bindparam("filter_names", regular_names, expanding=True)
3936
- )
3977
+ has_regular_names = bool(regular_names)
3978
+ query = self._partition_filter_names_query(has_regular_names)
3979
+ params = {"schema": owner, "type_filter": type_filter}
3980
+ if has_regular_names:
3981
+ params["filter_names"] = regular_names
3937
3982
 
3938
- rp = connection.execute(sql.text(query).bindparams(*params))
3939
- multi_object_names = [row[0] for row in rp]
3983
+ multi_object_names = connection.scalars(query, params).all()
3940
3984
 
3941
3985
  return (regular_names, multi_object_names, temp_names)
3942
3986
 
@@ -4305,9 +4349,13 @@ index_info AS (
4305
4349
  # return empty foreign_keys for temp tables that exist. We
4306
4350
  # preserve that here by checking existence and returning the
4307
4351
  # default (empty) reflection for each temp that is reachable.
4352
+ has_temp_tables = self._internal_has_multi_table(
4353
+ connection, temp_names, owner="dbo", schema=schema, **kw
4354
+ )
4308
4355
  for name in temp_names:
4309
- if self._internal_has_table(connection, name, owner="dbo"):
4310
- final[(schema, name)] = ReflectionDefaults.foreign_keys()
4356
+ key = (schema, name)
4357
+ if has_temp_tables.get(key):
4358
+ final[key] = ReflectionDefaults.foreign_keys()
4311
4359
 
4312
4360
  return final.items()
4313
4361
 
@@ -83,6 +83,24 @@ class MSDialect_mssqlpython(MSDialect):
83
83
 
84
84
  supports_native_decimal = True
85
85
 
86
+ _disconnect_driver_errors = frozenset(
87
+ {
88
+ # mssql-python converts the originating SQLSTATE into these
89
+ # values and exposes them on ``Exception.driver_error``.
90
+ "Disconnect error", # 01002
91
+ "Client unable to establish connection", # 08001
92
+ "Connection not open", # 08003
93
+ "Connection failure during transaction", # 08007
94
+ "Communication link failure", # 08S01
95
+ # mssql-python does not map these SQLSTATE values to a
96
+ # dedicated driver error yet.
97
+ "An error occurred with SQLSTATE code: 08S02",
98
+ "An error occurred with SQLSTATE code: 10054",
99
+ "Connection timeout expired", # HYT01
100
+ "Function sequence error", # HY010
101
+ }
102
+ )
103
+
86
104
  # used by pyodbc _ms_numeric_pyodbc class
87
105
  _need_decimal_fix = True
88
106
 
@@ -163,32 +181,30 @@ class MSDialect_mssqlpython(MSDialect):
163
181
  ],
164
182
  cursor: Optional[interfaces.DBAPICursor],
165
183
  ) -> bool:
184
+ if not isinstance(e, self.loaded_dbapi.Error):
185
+ return False
186
+
187
+ if getattr(e, "driver_error", None) in self._disconnect_driver_errors:
188
+ return True
189
+
166
190
  if isinstance(e, self.loaded_dbapi.ProgrammingError):
167
191
  return (
168
192
  "The cursor's connection has been closed." in str(e)
169
193
  or "Attempt to use a closed connection." in str(e)
170
194
  or "Driver Error: Operation cannot be performed" in str(e)
171
195
  )
172
- elif isinstance(e, self.loaded_dbapi.InterfaceError):
173
- return bool(re.search(r"Cannot .* on closed connection", str(e)))
174
- else:
175
- return False
176
196
 
177
- def _dbapi_version(self) -> interfaces.VersionInfoType:
178
- if not self.dbapi:
179
- return ()
180
- return self._parse_dbapi_version(self.dbapi.version)
181
-
182
- def _parse_dbapi_version(self, vers: str) -> interfaces.VersionInfoType:
183
- m = re.match(r"(?:py.*-)?([\d\.]+)(?:-(\w+))?", vers)
184
- if not m:
185
- return ()
186
- vers_tuple: interfaces.VersionInfoType = tuple(
187
- [int(x) for x in m.group(1).split(".")]
188
- )
189
- if m.group(2):
190
- vers_tuple += (m.group(2),)
191
- return vers_tuple
197
+ if isinstance(e, self.loaded_dbapi.InterfaceError):
198
+ return bool(
199
+ re.search(r"Cannot .* on (?:a )?closed connection", str(e))
200
+ )
201
+
202
+ return False
203
+
204
+ def retrieve_dbapi_version(
205
+ self, dbapi: interfaces.DBAPIModule
206
+ ) -> util.VersionInfo:
207
+ return util.parse_version_string(dbapi.version)
192
208
 
193
209
  def _get_server_version_info(self, connection):
194
210
  vers = connection.exec_driver_sql("select @@version").scalar()
@@ -61,11 +61,14 @@ class MSDialect_pymssql(MSDialect):
61
61
  {sqltypes.Numeric: _MSNumeric_pymssql, sqltypes.Float: sqltypes.Float},
62
62
  )
63
63
 
64
+ def retrieve_dbapi_version(self, dbapi):
65
+ return util.parse_version_string(getattr(dbapi, "__version__", None))
66
+
64
67
  @classmethod
65
68
  def import_dbapi(cls):
66
69
  module = __import__("pymssql")
67
70
  # pymmsql < 2.1.1 doesn't have a Binary method. we use string
68
- client_ver = tuple(int(x) for x in module.__version__.split("."))
71
+ client_ver = util.parse_version_string(module.__version__)
69
72
  if client_ver < (2, 1, 1):
70
73
  # TODO: monkeypatching here is less than ideal
71
74
  module.Binary = lambda x: x if hasattr(x, "decode") else str(x)
@@ -606,10 +606,10 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect):
606
606
  and self.dbapi
607
607
  and hasattr(self.dbapi.Cursor, "nextset")
608
608
  )
609
- self._need_decimal_fix = self.dbapi and self._dbapi_version() < (
610
- 2,
611
- 1,
612
- 8,
609
+ # a version which can't be determined is assumed to be an old one
610
+ version = self._dbapi_version_or_none
611
+ self._need_decimal_fix = bool(self.dbapi) and (
612
+ version is None or version < (2, 1, 8)
613
613
  )
614
614
  self.fast_executemany = fast_executemany
615
615
  if fast_executemany:
@@ -39,6 +39,7 @@ from typing import Union
39
39
 
40
40
  from .pymysql import _connection_ping_reconnects_true
41
41
  from .pymysql import MySQLDialect_pymysql
42
+ from ... import util
42
43
  from ...connectors.asyncio import AsyncAdapt_dbapi_connection
43
44
  from ...connectors.asyncio import AsyncAdapt_dbapi_cursor
44
45
  from ...connectors.asyncio import AsyncAdapt_dbapi_module
@@ -104,7 +105,7 @@ class AsyncAdapt_aiomysql_connection(
104
105
  await_(self._connection.autocommit(value))
105
106
 
106
107
  def get_autocommit(self) -> bool:
107
- return self._connection.get_autocommit() # type: ignore
108
+ return self._connection.get_autocommit() # type: ignore[no-any-return]
108
109
 
109
110
  def close(self) -> None:
110
111
  await_(self._connection.ensure_closed())
@@ -215,6 +216,14 @@ class MySQLDialect_aiomysql(MySQLDialect_pymysql):
215
216
  __import__("aiomysql"), __import__("pymysql")
216
217
  )
217
218
 
219
+ def retrieve_dbapi_version(self, dbapi: DBAPIModule) -> util.VersionInfo:
220
+ # the version of aiomysql itself, rather than that of the pymysql
221
+ # module it makes use of
222
+ aiomysql = getattr(dbapi, "aiomysql", None)
223
+ return util.parse_version_string(
224
+ getattr(aiomysql, "__version__", None)
225
+ )
226
+
218
227
  def do_terminate(self, dbapi_connection: DBAPIConnection) -> None:
219
228
  dbapi_connection.terminate()
220
229
 
@@ -238,7 +247,7 @@ class MySQLDialect_aiomysql(MySQLDialect_pymysql):
238
247
  return "not connected" in str_e
239
248
 
240
249
  def _found_rows_client_flag(self) -> int:
241
- from pymysql.constants import CLIENT # type: ignore
250
+ from pymysql.constants import CLIENT # type: ignore[import-untyped]
242
251
 
243
252
  return CLIENT.FOUND_ROWS # type: ignore[no-any-return]
244
253
 
@@ -116,7 +116,7 @@ class AsyncAdapt_asyncmy_connection(
116
116
  await_(self._connection.autocommit(value))
117
117
 
118
118
  def get_autocommit(self) -> bool:
119
- return self._connection.get_autocommit() # type: ignore
119
+ return self._connection.get_autocommit() # type: ignore[no-any-return]
120
120
 
121
121
  def close(self) -> None:
122
122
  await_(self._connection.ensure_closed())
@@ -200,6 +200,11 @@ class MySQLDialect_asyncmy(MySQLDialect_pymysql):
200
200
  def import_dbapi(cls) -> DBAPIModule:
201
201
  return AsyncAdapt_asyncmy_dbapi(__import__("asyncmy"))
202
202
 
203
+ def retrieve_dbapi_version(self, dbapi: DBAPIModule) -> util.VersionInfo:
204
+ # asyncmy publishes no version of its own within the module, so
205
+ # the installed distribution is consulted
206
+ return util.parse_version_from_metadata("asyncmy")
207
+
203
208
  def do_terminate(self, dbapi_connection: DBAPIConnection) -> None:
204
209
  dbapi_connection.terminate()
205
210
 
@@ -223,7 +228,7 @@ class MySQLDialect_asyncmy(MySQLDialect_pymysql):
223
228
  )
224
229
 
225
230
  def _found_rows_client_flag(self) -> int:
226
- from asyncmy.constants import CLIENT # type: ignore
231
+ from asyncmy.constants import CLIENT # type: ignore[import-not-found]
227
232
 
228
233
  return CLIENT.FOUND_ROWS # type: ignore[no-any-return]
229
234
 
@@ -1149,6 +1149,7 @@ from ...types import DATE
1149
1149
  from ...types import LargeBinary
1150
1150
  from ...types import UUID
1151
1151
  from ...types import VARBINARY
1152
+ from ...util import OrderedSet
1152
1153
  from ...util import topological
1153
1154
 
1154
1155
  if TYPE_CHECKING:
@@ -2135,8 +2136,8 @@ class MySQLDDLCompiler(
2135
2136
  "SUBPARTITION_BY",
2136
2137
  ]
2137
2138
 
2138
- nonpart_options = set(opts).difference(partition_options)
2139
- part_options = set(opts).intersection(partition_options)
2139
+ nonpart_options = OrderedSet(opts).difference(partition_options)
2140
+ part_options = OrderedSet(opts).intersection(partition_options)
2140
2141
 
2141
2142
  for opt in topological.sort(
2142
2143
  [
@@ -2976,9 +2977,9 @@ class MySQLDialect(_mariadb_shim.MariaDBShim, default.DefaultDialect):
2976
2977
  if isinstance(
2977
2978
  e,
2978
2979
  (
2979
- self.dbapi.OperationalError, # type: ignore
2980
- self.dbapi.ProgrammingError, # type: ignore
2981
- self.dbapi.InterfaceError, # type: ignore
2980
+ self.dbapi.OperationalError, # type: ignore[union-attr]
2981
+ self.dbapi.ProgrammingError, # type: ignore[union-attr]
2982
+ self.dbapi.InterfaceError, # type: ignore[union-attr]
2982
2983
  ),
2983
2984
  ) and self._extract_error_code(e) in (
2984
2985
  1927,
@@ -2991,7 +2992,7 @@ class MySQLDialect(_mariadb_shim.MariaDBShim, default.DefaultDialect):
2991
2992
  ):
2992
2993
  return True
2993
2994
  elif isinstance(
2994
- e, (self.dbapi.InterfaceError, self.dbapi.InternalError) # type: ignore # noqa: E501
2995
+ e, (self.dbapi.InterfaceError, self.dbapi.InternalError) # type: ignore[union-attr] # noqa: E501
2995
2996
  ):
2996
2997
  # if underlying connection is closed,
2997
2998
  # this is the error you get
@@ -3087,7 +3088,7 @@ class MySQLDialect(_mariadb_shim.MariaDBShim, default.DefaultDialect):
3087
3088
  #
3088
3089
  # there's more "doesn't exist" kinds of messages but they are
3089
3090
  # less clear if mysql 8 would suddenly start using one of those
3090
- if self._extract_error_code(e.orig) in (1146, 1049, 1051): # type: ignore # noqa: E501
3091
+ if self._extract_error_code(e.orig) in (1146, 1049, 1051): # type: ignore[arg-type, operator] # noqa: E501
3091
3092
  return False
3092
3093
  raise
3093
3094
 
@@ -3224,7 +3225,7 @@ class MySQLDialect(_mariadb_shim.MariaDBShim, default.DefaultDialect):
3224
3225
  if schema is not None:
3225
3226
  current_schema: str = schema
3226
3227
  else:
3227
- current_schema = self.default_schema_name # type: ignore
3228
+ current_schema = self.default_schema_name # type: ignore[assignment] # noqa: E501
3228
3229
 
3229
3230
  charset = self._connection_charset
3230
3231
 
@@ -3385,7 +3386,7 @@ class MySQLDialect(_mariadb_shim.MariaDBShim, default.DefaultDialect):
3385
3386
  def lower(s: str) -> str:
3386
3387
  return s
3387
3388
 
3388
- default_schema_name: str = connection.dialect.default_schema_name # type: ignore # noqa: E501
3389
+ default_schema_name: str = connection.dialect.default_schema_name # type: ignore[assignment] # noqa: E501
3389
3390
 
3390
3391
  # NOTE: using (table_schema, table_name, lower(column_name)) in (...)
3391
3392
  # is very slow since mysql does not seem able to properly use indexse.
@@ -257,7 +257,7 @@ class SET(_StringType):
257
257
  if value is not None and not isinstance(value, (int, str)):
258
258
  value = ",".join(value)
259
259
  if super_convert:
260
- return super_convert(value) # type: ignore
260
+ return super_convert(value) # type: ignore[arg-type, no-any-return] # noqa: E501
261
261
  else:
262
262
  return value
263
263
 
@@ -30,7 +30,6 @@ be ``mysqldb``. ``mariadb+mariadbconnector://`` is required to use this driver.
30
30
 
31
31
  from __future__ import annotations
32
32
 
33
- import re
34
33
  from typing import Any
35
34
  from typing import Optional
36
35
  from typing import Sequence
@@ -60,7 +59,7 @@ if TYPE_CHECKING:
60
59
  from ...sql.type_api import _ResultProcessorType
61
60
 
62
61
 
63
- mariadb_cpy_minimum_version = (1, 0, 1)
62
+ mariadb_cpy_minimum_version = util.VersionInfo((1, 0, 1))
64
63
 
65
64
 
66
65
  class _MariaDBUUID(sqltypes.UUID[sqltypes._UUID_RETURN]):
@@ -125,6 +124,8 @@ class MySQLDialect_mariadbconnector(MySQLDialect):
125
124
  driver = "mariadbconnector"
126
125
  supports_statement_cache = True
127
126
 
127
+ minimum_dbapi_version = mariadb_cpy_minimum_version
128
+
128
129
  # set this to True at the module level to prevent the driver from running
129
130
  # against a backend that server detects as MySQL. currently this appears to
130
131
  # be unnecessary as MariaDB client libraries have always worked against
@@ -151,30 +152,12 @@ class MySQLDialect_mariadbconnector(MySQLDialect):
151
152
  MySQLDialect.colspecs, {sqltypes.Uuid: _MariaDBUUID}
152
153
  )
153
154
 
154
- @util.memoized_property
155
- def _dbapi_version(self) -> tuple[int, ...]:
156
- if self.dbapi and hasattr(self.dbapi, "__version__"):
157
- return tuple(
158
- [
159
- int(x)
160
- for x in re.findall(
161
- r"(\d+)(?:[-\.]?|$)", self.dbapi.__version__
162
- )
163
- ]
164
- )
165
- else:
166
- return (99, 99, 99)
155
+ def retrieve_dbapi_version(self, dbapi: DBAPIModule) -> util.VersionInfo:
156
+ return util.parse_version_string(getattr(dbapi, "__version__", None))
167
157
 
168
158
  def __init__(self, **kwargs: Any) -> None:
169
159
  super().__init__(**kwargs)
170
160
  self.paramstyle = "qmark"
171
- if self.dbapi is not None:
172
- if self._dbapi_version < mariadb_cpy_minimum_version:
173
- raise NotImplementedError(
174
- "The minimum required version for MariaDB "
175
- "Connector/Python is %s"
176
- % ".".join(str(x) for x in mariadb_cpy_minimum_version)
177
- )
178
161
 
179
162
  @classmethod
180
163
  def import_dbapi(cls) -> DBAPIModule:
@@ -48,7 +48,6 @@ charset/collation will allow connectivity.
48
48
 
49
49
  from __future__ import annotations
50
50
 
51
- import re
52
51
  from typing import Any
53
52
  from typing import cast
54
53
  from typing import Optional
@@ -198,7 +197,7 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
198
197
  # supports_sane_rowcount.
199
198
  if self.dbapi is not None:
200
199
  try:
201
- from mysql.connector import constants # type: ignore
200
+ from mysql.connector import constants # type: ignore[import-not-found] # noqa: E501
202
201
 
203
202
  ClientFlag = constants.ClientFlag
204
203
 
@@ -212,19 +211,14 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
212
211
 
213
212
  return [], opts
214
213
 
215
- @util.memoized_property
216
- def _mysqlconnector_version_info(self) -> Optional[tuple[int, ...]]:
217
- if self.dbapi and hasattr(self.dbapi, "__version__"):
218
- m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", self.dbapi.__version__)
219
- if m:
220
- return tuple(int(x) for x in m.group(1, 2, 3) if x is not None)
221
- return None
214
+ def retrieve_dbapi_version(self, dbapi: DBAPIModule) -> util.VersionInfo:
215
+ return util.parse_version_string(getattr(dbapi, "__version__", None))
222
216
 
223
217
  def _detect_charset(self, connection: Connection) -> str:
224
- return connection.connection.charset # type: ignore
218
+ return connection.connection.charset # type: ignore[no-any-return]
225
219
 
226
220
  def _extract_error_code(self, exception: BaseException) -> int:
227
- return exception.errno # type: ignore
221
+ return exception.errno # type: ignore[attr-defined, no-any-return]
228
222
 
229
223
  def is_disconnect(
230
224
  self,