SQLAlchemy 2.1.0rc2__py3-none-any.whl → 2.1.2__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.
- sqlalchemy/__init__.py +1 -1
- sqlalchemy/dialects/mssql/base.py +21 -0
- sqlalchemy/dialects/mssql/mssqlpython.py +13 -4
- sqlalchemy/dialects/mssql/pyodbc.py +6 -9
- sqlalchemy/dialects/oracle/oracledb.py +24 -1
- sqlalchemy/dialects/postgresql/base.py +62 -8
- sqlalchemy/engine/_row_cy.py +1 -1
- sqlalchemy/engine/default.py +12 -0
- sqlalchemy/engine/interfaces.py +31 -2
- sqlalchemy/ext/asyncio/base.py +31 -0
- sqlalchemy/ext/asyncio/engine.py +3 -5
- sqlalchemy/ext/asyncio/session.py +3 -5
- sqlalchemy/ext/automap.py +8 -8
- sqlalchemy/orm/_orm_constructors.py +17 -0
- sqlalchemy/orm/descriptor_props.py +24 -2
- sqlalchemy/orm/relationships.py +29 -12
- sqlalchemy/orm/strategies.py +13 -4
- sqlalchemy/orm/util.py +93 -4
- sqlalchemy/pool/base.py +22 -6
- sqlalchemy/sql/_elements_constructors.py +26 -0
- sqlalchemy/sql/base.py +151 -1
- sqlalchemy/sql/compiler.py +58 -17
- sqlalchemy/sql/elements.py +32 -3
- sqlalchemy/sql/schema.py +5 -0
- sqlalchemy/sql/selectable.py +23 -11
- sqlalchemy/sql/sqltypes.py +10 -4
- sqlalchemy/sql/type_api.py +53 -24
- sqlalchemy/testing/cancellation.py +203 -73
- sqlalchemy/testing/requirements.py +35 -2
- sqlalchemy/testing/suite/test_dialect.py +1 -0
- sqlalchemy/testing/suite/test_types.py +129 -3
- sqlalchemy/testing/suite/test_update_delete.py +37 -0
- sqlalchemy/util/langhelpers.py +8 -2
- {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/METADATA +2 -24
- {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/RECORD +39 -39
- {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/WHEEL +0 -0
- {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/licenses/AUTHORS +0 -0
- {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py
CHANGED
|
@@ -3315,6 +3315,27 @@ class MSDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
3315
3315
|
finally:
|
|
3316
3316
|
cursor.close()
|
|
3317
3317
|
|
|
3318
|
+
def min_year_for_type(self, typeobj):
|
|
3319
|
+
typeobj = sqltypes.to_instance(typeobj)
|
|
3320
|
+
|
|
3321
|
+
if isinstance(typeobj, SMALLDATETIME):
|
|
3322
|
+
return 1900
|
|
3323
|
+
elif isinstance(typeobj, (DATETIME2, DATETIMEOFFSET)):
|
|
3324
|
+
return datetime.MINYEAR
|
|
3325
|
+
elif isinstance(typeobj, sqltypes.DateTime):
|
|
3326
|
+
# DateTime renders DATETIMEOFFSET with timezone=True,
|
|
3327
|
+
# else DATETIME
|
|
3328
|
+
return datetime.MINYEAR if typeobj.timezone else 1753
|
|
3329
|
+
elif (
|
|
3330
|
+
isinstance(typeobj, sqltypes.Date)
|
|
3331
|
+
and not isinstance(typeobj, sqltypes.DATE)
|
|
3332
|
+
and self.server_version_info < MS_2008_VERSION
|
|
3333
|
+
):
|
|
3334
|
+
# Date renders DATETIME before SQL Server 2008
|
|
3335
|
+
return 1753
|
|
3336
|
+
else:
|
|
3337
|
+
return datetime.MINYEAR
|
|
3338
|
+
|
|
3318
3339
|
def initialize(self, connection):
|
|
3319
3340
|
super().initialize(connection)
|
|
3320
3341
|
self._setup_version_attributes()
|
|
@@ -101,9 +101,6 @@ class MSDialect_mssqlpython(MSDialect):
|
|
|
101
101
|
}
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
-
# used by pyodbc _ms_numeric_pyodbc class
|
|
105
|
-
_need_decimal_fix = True
|
|
106
|
-
|
|
107
104
|
colspecs = util.update_copy(
|
|
108
105
|
MSDialect.colspecs,
|
|
109
106
|
{
|
|
@@ -117,6 +114,18 @@ class MSDialect_mssqlpython(MSDialect):
|
|
|
117
114
|
if not enable_pooling and self.dbapi is not None:
|
|
118
115
|
self.loaded_dbapi.pooling(enabled=False)
|
|
119
116
|
|
|
117
|
+
# used by pyodbc _ms_numeric_pyodbc class. mssql-python 1.15
|
|
118
|
+
# binds every Decimal as SQL_NUMERIC; prior versions bound Decimal
|
|
119
|
+
# values within the MONEY range as VARCHAR, so that very large and
|
|
120
|
+
# very small values needed to be passed as strings as well. On
|
|
121
|
+
# 1.15 those strings are instead coerced to the NUMERIC type of
|
|
122
|
+
# the Decimal values alongside them, which overflows.
|
|
123
|
+
# a version which can't be determined is assumed to be an old one
|
|
124
|
+
version = self._dbapi_version_or_none
|
|
125
|
+
self._need_decimal_fix = bool(self.dbapi) and (
|
|
126
|
+
version is None or version < (1, 15)
|
|
127
|
+
)
|
|
128
|
+
|
|
120
129
|
@classmethod
|
|
121
130
|
def import_dbapi(cls) -> DBAPIModule:
|
|
122
131
|
return __import__("mssql_python")
|
|
@@ -204,7 +213,7 @@ class MSDialect_mssqlpython(MSDialect):
|
|
|
204
213
|
def retrieve_dbapi_version(
|
|
205
214
|
self, dbapi: interfaces.DBAPIModule
|
|
206
215
|
) -> util.VersionInfo:
|
|
207
|
-
return util.parse_version_string(dbapi
|
|
216
|
+
return util.parse_version_string(getattr(dbapi, "__version__", None))
|
|
208
217
|
|
|
209
218
|
def _get_server_version_info(self, connection):
|
|
210
219
|
vers = connection.exec_driver_sql("select @@version").scalar()
|
|
@@ -489,15 +489,12 @@ class _ODBCDateTimeBindProcessor:
|
|
|
489
489
|
else:
|
|
490
490
|
# for DATETIMEOFFSET or DateTime(timezone=True)
|
|
491
491
|
#
|
|
492
|
-
# Convert to string format required by T-SQL
|
|
493
|
-
|
|
494
|
-
# offset
|
|
495
|
-
#
|
|
496
|
-
#
|
|
497
|
-
|
|
498
|
-
r"([\+\-]\d{2})([\d\.]+)$", r"\1:\2", dto_string
|
|
499
|
-
)
|
|
500
|
-
return dto_string
|
|
492
|
+
# Convert to string format required by T-SQL. isoformat()
|
|
493
|
+
# zero-pads the year, which strftime("%Y") does not do on
|
|
494
|
+
# all platforms, and renders the offset with a colon,
|
|
495
|
+
# e.g. -07:00. backend currently rejects seconds /
|
|
496
|
+
# fractional seconds in the offset
|
|
497
|
+
return value.isoformat(sep=" ", timespec="microseconds")
|
|
501
498
|
|
|
502
499
|
return process
|
|
503
500
|
|
|
@@ -600,6 +600,7 @@ from ...connectors.asyncio import AsyncAdapt_dbapi_connection
|
|
|
600
600
|
from ...connectors.asyncio import AsyncAdapt_dbapi_cursor
|
|
601
601
|
from ...connectors.asyncio import AsyncAdapt_dbapi_module
|
|
602
602
|
from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor
|
|
603
|
+
from ...connectors.asyncio import AsyncAdapt_terminate
|
|
603
604
|
from ...engine import default
|
|
604
605
|
from ...util import await_
|
|
605
606
|
|
|
@@ -773,7 +774,9 @@ class AsyncAdapt_oracledb_ss_cursor(
|
|
|
773
774
|
self._cursor = None # type: ignore
|
|
774
775
|
|
|
775
776
|
|
|
776
|
-
class AsyncAdapt_oracledb_connection(
|
|
777
|
+
class AsyncAdapt_oracledb_connection(
|
|
778
|
+
AsyncAdapt_terminate, AsyncAdapt_dbapi_connection
|
|
779
|
+
):
|
|
777
780
|
_connection: AsyncConnection
|
|
778
781
|
__slots__ = ()
|
|
779
782
|
|
|
@@ -838,6 +841,12 @@ class AsyncAdapt_oracledb_connection(AsyncAdapt_dbapi_connection):
|
|
|
838
841
|
def tpc_rollback(self, *args: Any, **kwargs: Any) -> Any:
|
|
839
842
|
return await_(self._connection.tpc_rollback(*args, **kwargs))
|
|
840
843
|
|
|
844
|
+
async def _terminate_graceful_close(self) -> None:
|
|
845
|
+
await self._connection.close()
|
|
846
|
+
|
|
847
|
+
def _terminate_force_close(self) -> None:
|
|
848
|
+
self._connection.terminate()
|
|
849
|
+
|
|
841
850
|
|
|
842
851
|
class OracledbAdaptDBAPI(AsyncAdapt_dbapi_module):
|
|
843
852
|
def __init__(self, oracledb) -> None:
|
|
@@ -883,6 +892,14 @@ class OracleDialectAsync_oracledb(OracleDialect_oracledb):
|
|
|
883
892
|
|
|
884
893
|
minimum_dbapi_version = util.VersionInfo((2, 0, 1))
|
|
885
894
|
|
|
895
|
+
def __init__(self, **kwargs):
|
|
896
|
+
super().__init__(**kwargs)
|
|
897
|
+
|
|
898
|
+
self.has_terminate = (
|
|
899
|
+
self._dbapi_version_or_none is not None
|
|
900
|
+
and self._dbapi_version_or_none >= (26, 0, 0)
|
|
901
|
+
)
|
|
902
|
+
|
|
886
903
|
# thick_mode mode is not supported by asyncio, oracledb will raise
|
|
887
904
|
@classmethod
|
|
888
905
|
def import_dbapi(cls):
|
|
@@ -893,6 +910,12 @@ class OracleDialectAsync_oracledb(OracleDialect_oracledb):
|
|
|
893
910
|
def get_driver_connection(self, connection):
|
|
894
911
|
return connection._connection
|
|
895
912
|
|
|
913
|
+
def do_terminate(self, dbapi_connection) -> None:
|
|
914
|
+
if self.has_terminate:
|
|
915
|
+
dbapi_connection.terminate()
|
|
916
|
+
else:
|
|
917
|
+
super().do_terminate(dbapi_connection)
|
|
918
|
+
|
|
896
919
|
|
|
897
920
|
dialect = OracleDialect_oracledb
|
|
898
921
|
dialect_async = OracleDialectAsync_oracledb
|
|
@@ -1179,6 +1179,12 @@ construct, the DBAPI's "autocommit" mode must be used::
|
|
|
1179
1179
|
PostgreSQL Index Reflection
|
|
1180
1180
|
---------------------------
|
|
1181
1181
|
|
|
1182
|
+
Notes on Index reflection
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
Implicit UNIQUE index for UNIQUE CONSTRAINT
|
|
1186
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
1187
|
+
|
|
1182
1188
|
The PostgreSQL database creates a UNIQUE INDEX implicitly whenever the
|
|
1183
1189
|
UNIQUE CONSTRAINT construct is used. When inspecting a table using
|
|
1184
1190
|
:class:`_reflection.Inspector`, the :meth:`_reflection.Inspector.get_indexes`
|
|
@@ -1189,8 +1195,38 @@ two constructs distinctly; in the case of the index, the key
|
|
|
1189
1195
|
detected as mirroring a constraint. When performing reflection using
|
|
1190
1196
|
``Table(..., autoload_with=engine)``, the UNIQUE INDEX is **not** returned
|
|
1191
1197
|
in :attr:`_schema.Table.indexes` when it is detected as mirroring a
|
|
1192
|
-
:class:`.UniqueConstraint` in the :attr:`_schema.Table.constraints` collection
|
|
1193
|
-
|
|
1198
|
+
:class:`.UniqueConstraint` in the :attr:`_schema.Table.constraints` collection.
|
|
1199
|
+
|
|
1200
|
+
.. _postgresql_invalid_index_reflection:
|
|
1201
|
+
|
|
1202
|
+
Invalid indexes
|
|
1203
|
+
^^^^^^^^^^^^^^^
|
|
1204
|
+
|
|
1205
|
+
Indexes marked "invalid" by PostgreSQL (i.e. an index that exists in the
|
|
1206
|
+
catalog, but is ignored by the query planner because it may be incomplete,
|
|
1207
|
+
while still being maintained on writes) remain present in reflection. The
|
|
1208
|
+
:meth:`_reflection.Inspector.get_indexes` and
|
|
1209
|
+
:meth:`_reflection.Inspector.get_multi_indexes` methods include
|
|
1210
|
+
``postgresql_invalid=True`` in the index's ``dialect_options`` dictionary when
|
|
1211
|
+
``pg_index.indisvalid`` is false. Valid indexes omit this flag.
|
|
1212
|
+
|
|
1213
|
+
When reflecting a :class:`_schema.Table`, the flag, if present, is stored
|
|
1214
|
+
separately from DDL options on each reflected :class:`.Index`::
|
|
1215
|
+
|
|
1216
|
+
table = Table("my_table", MetaData(), autoload_with=engine)
|
|
1217
|
+
for index in table.indexes:
|
|
1218
|
+
if index.reflect_only_elements["postgresql"].get("invalid"):
|
|
1219
|
+
print(index.name)
|
|
1220
|
+
|
|
1221
|
+
The :attr:`.Index.reflect_only_elements` mapping is read-only. These values are
|
|
1222
|
+
not included in :attr:`.Index.dialect_kwargs` and do not affect DDL.
|
|
1223
|
+
|
|
1224
|
+
An invalid index may be left by a failed ``CREATE INDEX CONCURRENTLY``, may
|
|
1225
|
+
still be building, or may be a partitioned index awaiting attachment of its
|
|
1226
|
+
partition indexes. This flag reports the state at reflection time; it does not
|
|
1227
|
+
request an index rebuild.
|
|
1228
|
+
|
|
1229
|
+
.. versionadded:: 2.1
|
|
1194
1230
|
|
|
1195
1231
|
Special Reflection Options
|
|
1196
1232
|
--------------------------
|
|
@@ -1946,6 +1982,7 @@ from ...sql import functions
|
|
|
1946
1982
|
from ...sql import roles
|
|
1947
1983
|
from ...sql import sqltypes
|
|
1948
1984
|
from ...sql import util as sql_util
|
|
1985
|
+
from ...sql.base import DialectKWArgConst
|
|
1949
1986
|
from ...sql.compiler import InsertmanyvaluesSentinelOpts
|
|
1950
1987
|
from ...sql.visitors import InternalTraversal
|
|
1951
1988
|
from ...types import BIGINT
|
|
@@ -3666,6 +3703,7 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
3666
3703
|
"with": {},
|
|
3667
3704
|
"tablespace": None,
|
|
3668
3705
|
"nulls_not_distinct": None,
|
|
3706
|
+
"invalid": DialectKWArgConst.REFLECTED_ONLY,
|
|
3669
3707
|
},
|
|
3670
3708
|
),
|
|
3671
3709
|
(
|
|
@@ -4808,8 +4846,12 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
4808
4846
|
)
|
|
4809
4847
|
.outerjoin(
|
|
4810
4848
|
pg_catalog.pg_description,
|
|
4811
|
-
|
|
4812
|
-
|
|
4849
|
+
sql.and_(
|
|
4850
|
+
pg_catalog.pg_description.c.objoid
|
|
4851
|
+
== pg_catalog.pg_constraint.c.oid,
|
|
4852
|
+
pg_catalog.pg_description.c.classoid
|
|
4853
|
+
== sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
|
|
4854
|
+
),
|
|
4813
4855
|
)
|
|
4814
4856
|
.where(
|
|
4815
4857
|
pg_catalog.pg_constraint.c.contype == bindparam("contype"),
|
|
@@ -5001,8 +5043,12 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
5001
5043
|
)
|
|
5002
5044
|
.outerjoin(
|
|
5003
5045
|
pg_catalog.pg_description,
|
|
5004
|
-
|
|
5005
|
-
|
|
5046
|
+
sql.and_(
|
|
5047
|
+
pg_catalog.pg_description.c.objoid
|
|
5048
|
+
== pg_catalog.pg_constraint.c.oid,
|
|
5049
|
+
pg_catalog.pg_description.c.classoid
|
|
5050
|
+
== sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
|
|
5051
|
+
),
|
|
5006
5052
|
)
|
|
5007
5053
|
.order_by(
|
|
5008
5054
|
pg_catalog.pg_class.c.relname,
|
|
@@ -5269,6 +5315,7 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
5269
5315
|
pg_catalog.pg_index.c.indrelid,
|
|
5270
5316
|
pg_catalog.pg_class.c.relname,
|
|
5271
5317
|
pg_catalog.pg_index.c.indisunique,
|
|
5318
|
+
pg_catalog.pg_index.c.indisvalid,
|
|
5272
5319
|
pg_catalog.pg_constraint.c.conrelid.is_not(None).label(
|
|
5273
5320
|
"has_constraint"
|
|
5274
5321
|
),
|
|
@@ -5475,6 +5522,9 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
5475
5522
|
"indnullsnotdistinct"
|
|
5476
5523
|
]
|
|
5477
5524
|
|
|
5525
|
+
if not row["indisvalid"]:
|
|
5526
|
+
dialect_options["postgresql_invalid"] = True
|
|
5527
|
+
|
|
5478
5528
|
if dialect_options:
|
|
5479
5529
|
index["dialect_options"] = dialect_options
|
|
5480
5530
|
|
|
@@ -5590,8 +5640,12 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
5590
5640
|
)
|
|
5591
5641
|
.outerjoin(
|
|
5592
5642
|
pg_catalog.pg_description,
|
|
5593
|
-
|
|
5594
|
-
|
|
5643
|
+
sql.and_(
|
|
5644
|
+
pg_catalog.pg_description.c.objoid
|
|
5645
|
+
== pg_catalog.pg_constraint.c.oid,
|
|
5646
|
+
pg_catalog.pg_description.c.classoid
|
|
5647
|
+
== sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
|
|
5648
|
+
),
|
|
5595
5649
|
)
|
|
5596
5650
|
.order_by(
|
|
5597
5651
|
pg_catalog.pg_class.c.relname,
|
sqlalchemy/engine/_row_cy.py
CHANGED
sqlalchemy/engine/default.py
CHANGED
|
@@ -16,6 +16,7 @@ as the base class for their own corresponding classes.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import datetime
|
|
19
20
|
import functools
|
|
20
21
|
import operator
|
|
21
22
|
import random
|
|
@@ -829,6 +830,17 @@ class DefaultDialect(Dialect):
|
|
|
829
830
|
"""
|
|
830
831
|
return type_api.adapt_type(typeobj, self.colspecs)
|
|
831
832
|
|
|
833
|
+
def min_year_for_type(self, typeobj: TypeEngine[Any]) -> int:
|
|
834
|
+
"""Return the earliest year the database accepts for the given
|
|
835
|
+
date or datetime type.
|
|
836
|
+
|
|
837
|
+
The default implementation returns ``datetime.MINYEAR``.
|
|
838
|
+
|
|
839
|
+
.. versionadded:: 2.0.55
|
|
840
|
+
|
|
841
|
+
"""
|
|
842
|
+
return datetime.MINYEAR
|
|
843
|
+
|
|
832
844
|
def has_index(self, connection, table_name, index_name, schema=None, **kw):
|
|
833
845
|
if not self.has_table(connection, table_name, schema=schema, **kw):
|
|
834
846
|
return False
|
sqlalchemy/engine/interfaces.py
CHANGED
|
@@ -560,7 +560,7 @@ class ReflectedIndex(TypedDict):
|
|
|
560
560
|
"""
|
|
561
561
|
|
|
562
562
|
dialect_options: NotRequired[Dict[str, Any]]
|
|
563
|
-
"""Additional dialect-specific options detected for this index"""
|
|
563
|
+
"""Additional dialect-specific options detected for this index."""
|
|
564
564
|
|
|
565
565
|
|
|
566
566
|
class ReflectedTableComment(TypedDict):
|
|
@@ -1259,6 +1259,17 @@ class Dialect(EventTarget):
|
|
|
1259
1259
|
here is so that third-party dialects that haven't yet implemented this
|
|
1260
1260
|
feature continue to function in the old way.
|
|
1261
1261
|
|
|
1262
|
+
Arguments that represent database state without any corresponding DDL,
|
|
1263
|
+
but may nonetheless appear in a reflected set of arguments that should
|
|
1264
|
+
be ignored by the construct, may make use of the
|
|
1265
|
+
:attr:`.DialectKWArgConst.REFLECTED_ONLY` token as the default value.
|
|
1266
|
+
Arguments passed to the schema object on such a key will be passed along
|
|
1267
|
+
into the separate read-only collection
|
|
1268
|
+
:attr:`.DialectKWArgs.reflect_only_elements` and should be omitted from
|
|
1269
|
+
DDL rendering.
|
|
1270
|
+
|
|
1271
|
+
.. versionadded:: 2.1 Added :class:`.DialectKWArgConst`.
|
|
1272
|
+
|
|
1262
1273
|
.. seealso::
|
|
1263
1274
|
|
|
1264
1275
|
:class:`.DialectKWArgs` - implementing base class which consumes
|
|
@@ -1454,6 +1465,23 @@ class Dialect(EventTarget):
|
|
|
1454
1465
|
|
|
1455
1466
|
raise NotImplementedError()
|
|
1456
1467
|
|
|
1468
|
+
def min_year_for_type(self, typeobj: TypeEngine[Any]) -> int:
|
|
1469
|
+
"""Return the earliest year the database accepts for the given
|
|
1470
|
+
date or datetime type.
|
|
1471
|
+
|
|
1472
|
+
The type may be a generic type such as :class:`.DateTime`, in
|
|
1473
|
+
which case the limit is that of the database type the dialect
|
|
1474
|
+
renders for it. Dialects whose date or datetime types don't
|
|
1475
|
+
accept all years from ``datetime.MINYEAR`` forward override this
|
|
1476
|
+
method; e.g. SQL Server's ``DATETIME`` type, which the dialect
|
|
1477
|
+
renders for :class:`.DateTime`, accepts years from 1753 forward.
|
|
1478
|
+
|
|
1479
|
+
.. versionadded:: 2.0.55
|
|
1480
|
+
|
|
1481
|
+
"""
|
|
1482
|
+
|
|
1483
|
+
raise NotImplementedError()
|
|
1484
|
+
|
|
1457
1485
|
def initialize(self, connection: Connection) -> None:
|
|
1458
1486
|
"""Called during strategized creation of the dialect with a
|
|
1459
1487
|
connection.
|
|
@@ -3426,7 +3454,8 @@ class ExceptionContext:
|
|
|
3426
3454
|
|
|
3427
3455
|
"""
|
|
3428
3456
|
|
|
3429
|
-
|
|
3457
|
+
if not TYPE_CHECKING:
|
|
3458
|
+
__slots__ = ()
|
|
3430
3459
|
|
|
3431
3460
|
dialect: Dialect
|
|
3432
3461
|
"""The :class:`_engine.Dialect` in use.
|
sqlalchemy/ext/asyncio/base.py
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import abc
|
|
11
|
+
import asyncio
|
|
11
12
|
import functools
|
|
12
13
|
from typing import Any
|
|
13
14
|
from typing import AsyncGenerator
|
|
@@ -15,6 +16,7 @@ from typing import AsyncIterator
|
|
|
15
16
|
from typing import Awaitable
|
|
16
17
|
from typing import Callable
|
|
17
18
|
from typing import ClassVar
|
|
19
|
+
from typing import Coroutine
|
|
18
20
|
from typing import Dict
|
|
19
21
|
from typing import Generator
|
|
20
22
|
from typing import Generic
|
|
@@ -37,6 +39,35 @@ _T_co = TypeVar("_T_co", bound=Any, covariant=True)
|
|
|
37
39
|
_PT = TypeVar("_PT", bound=Any)
|
|
38
40
|
|
|
39
41
|
|
|
42
|
+
async def _run_to_completion(coro: Coroutine[Any, Any, _T]) -> _T:
|
|
43
|
+
"""Run ``coro`` in a task of its own and wait for it to complete, even
|
|
44
|
+
if the calling task is cancelled meanwhile.
|
|
45
|
+
|
|
46
|
+
Used for cleanup, such as the ``close()`` performed by an async context
|
|
47
|
+
manager on exit, that must not be abandoned partway through. A
|
|
48
|
+
cancellation received while waiting is re-raised once ``coro`` has
|
|
49
|
+
completed, so that the caller does not return before its cleanup has
|
|
50
|
+
finished; were it to return early, the cleanup would carry on in the
|
|
51
|
+
background, where for example it could return a connection into a pool
|
|
52
|
+
that the application has meanwhile disposed of.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
task = asyncio.create_task(coro)
|
|
56
|
+
cancelled = None
|
|
57
|
+
while True:
|
|
58
|
+
try:
|
|
59
|
+
result = await asyncio.shield(task)
|
|
60
|
+
except asyncio.CancelledError as err:
|
|
61
|
+
if task.done():
|
|
62
|
+
raise
|
|
63
|
+
cancelled = err
|
|
64
|
+
else:
|
|
65
|
+
break
|
|
66
|
+
if cancelled is not None:
|
|
67
|
+
raise cancelled
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
40
71
|
class ReversibleProxy(Generic[_PT]):
|
|
41
72
|
_proxy_objects: ClassVar[
|
|
42
73
|
Dict[weakref.ref[Any], weakref.ref[ReversibleProxy[Any]]]
|
sqlalchemy/ext/asyncio/engine.py
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import asyncio
|
|
10
9
|
import contextlib
|
|
11
10
|
from typing import Any
|
|
12
11
|
from typing import AsyncIterator
|
|
@@ -24,6 +23,7 @@ from typing import TypeVar
|
|
|
24
23
|
from typing import Union
|
|
25
24
|
|
|
26
25
|
from . import exc as async_exc
|
|
26
|
+
from .base import _run_to_completion
|
|
27
27
|
from .base import asyncstartablecontext
|
|
28
28
|
from .base import GeneratorStartableContext
|
|
29
29
|
from .base import ProxyComparable
|
|
@@ -606,8 +606,7 @@ class AsyncConnection( # type: ignore[misc]
|
|
|
606
606
|
except GeneratorExit:
|
|
607
607
|
pass
|
|
608
608
|
else:
|
|
609
|
-
|
|
610
|
-
await asyncio.shield(task)
|
|
609
|
+
await _run_to_completion(ar.close())
|
|
611
610
|
|
|
612
611
|
@overload
|
|
613
612
|
async def execute(
|
|
@@ -913,8 +912,7 @@ class AsyncConnection( # type: ignore[misc]
|
|
|
913
912
|
return self.start().__await__()
|
|
914
913
|
|
|
915
914
|
async def __aexit__(self, type_: Any, value: Any, traceback: Any) -> None:
|
|
916
|
-
|
|
917
|
-
await asyncio.shield(task)
|
|
915
|
+
await _run_to_completion(self.close())
|
|
918
916
|
|
|
919
917
|
# START PROXY METHODS AsyncConnection
|
|
920
918
|
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import asyncio
|
|
10
9
|
from typing import Any
|
|
11
10
|
from typing import Awaitable
|
|
12
11
|
from typing import Callable
|
|
@@ -30,6 +29,7 @@ from typing import Union
|
|
|
30
29
|
|
|
31
30
|
from . import engine
|
|
32
31
|
from . import exc as async_exc
|
|
32
|
+
from .base import _run_to_completion
|
|
33
33
|
from .base import ReversibleProxy
|
|
34
34
|
from .base import StartableContext
|
|
35
35
|
from .result import _ensure_sync_result
|
|
@@ -1375,8 +1375,7 @@ class AsyncSession(ReversibleProxy[Session]):
|
|
|
1375
1375
|
return self
|
|
1376
1376
|
|
|
1377
1377
|
async def __aexit__(self, type_: Any, value: Any, traceback: Any) -> None:
|
|
1378
|
-
|
|
1379
|
-
await asyncio.shield(task)
|
|
1378
|
+
await _run_to_completion(self.close())
|
|
1380
1379
|
|
|
1381
1380
|
def _maker_context_manager(self: _AS) -> _AsyncSessionContextManager[_AS]:
|
|
1382
1381
|
return _AsyncSessionContextManager(self)
|
|
@@ -2121,8 +2120,7 @@ class _AsyncSessionContextManager(Generic[_AS]):
|
|
|
2121
2120
|
await self.trans.__aexit__(type_, value, traceback)
|
|
2122
2121
|
await self.async_session.__aexit__(type_, value, traceback)
|
|
2123
2122
|
|
|
2124
|
-
|
|
2125
|
-
await asyncio.shield(task)
|
|
2123
|
+
await _run_to_completion(go())
|
|
2126
2124
|
|
|
2127
2125
|
|
|
2128
2126
|
class AsyncSessionTransaction(
|
sqlalchemy/ext/automap.py
CHANGED
|
@@ -1661,11 +1661,11 @@ def _m2m_relationship(
|
|
|
1661
1661
|
overlaps=overlaps,
|
|
1662
1662
|
secondary=table,
|
|
1663
1663
|
primaryjoin=and_(
|
|
1664
|
-
fk.column == fk.parent for fk in m2m_const[0].elements
|
|
1665
|
-
),
|
|
1664
|
+
*(fk.column == fk.parent for fk in m2m_const[0].elements)
|
|
1665
|
+
),
|
|
1666
1666
|
secondaryjoin=and_(
|
|
1667
|
-
fk.column == fk.parent for fk in m2m_const[1].elements
|
|
1668
|
-
),
|
|
1667
|
+
*(fk.column == fk.parent for fk in m2m_const[1].elements)
|
|
1668
|
+
),
|
|
1669
1669
|
backref=backref_obj,
|
|
1670
1670
|
collection_class=collection_class,
|
|
1671
1671
|
)
|
|
@@ -1687,11 +1687,11 @@ def _m2m_relationship(
|
|
|
1687
1687
|
overlaps=overlaps,
|
|
1688
1688
|
secondary=table,
|
|
1689
1689
|
primaryjoin=and_(
|
|
1690
|
-
fk.column == fk.parent for fk in m2m_const[1].elements
|
|
1691
|
-
),
|
|
1690
|
+
*(fk.column == fk.parent for fk in m2m_const[1].elements)
|
|
1691
|
+
),
|
|
1692
1692
|
secondaryjoin=and_(
|
|
1693
|
-
fk.column == fk.parent for fk in m2m_const[0].elements
|
|
1694
|
-
),
|
|
1693
|
+
*(fk.column == fk.parent for fk in m2m_const[0].elements)
|
|
1694
|
+
),
|
|
1695
1695
|
back_populates=relationship_name,
|
|
1696
1696
|
collection_class=collection_class,
|
|
1697
1697
|
)
|
|
@@ -642,6 +642,7 @@ def composite(
|
|
|
642
642
|
return_none_on: Union[_NoArg, None, Callable[..., bool]] = _NoArg.NO_ARG,
|
|
643
643
|
comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
|
|
644
644
|
active_history: bool = False,
|
|
645
|
+
column_template: Optional[str] = None,
|
|
645
646
|
init: Union[_NoArg, bool] = _NoArg.NO_ARG,
|
|
646
647
|
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
|
|
647
648
|
default: Optional[Any] = _NoArg.NO_ARG,
|
|
@@ -667,6 +668,7 @@ def composite(
|
|
|
667
668
|
return_none_on: Union[_NoArg, None, Callable[..., bool]] = _NoArg.NO_ARG,
|
|
668
669
|
comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
|
|
669
670
|
active_history: bool = False,
|
|
671
|
+
column_template: Optional[str] = None,
|
|
670
672
|
init: Union[_NoArg, bool] = _NoArg.NO_ARG,
|
|
671
673
|
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
|
|
672
674
|
default: Optional[Any] = _NoArg.NO_ARG,
|
|
@@ -691,6 +693,7 @@ def composite(
|
|
|
691
693
|
return_none_on: Union[_NoArg, None, Callable[..., bool]] = _NoArg.NO_ARG,
|
|
692
694
|
comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
|
|
693
695
|
active_history: bool = False,
|
|
696
|
+
column_template: Optional[str] = None,
|
|
694
697
|
init: Union[_NoArg, bool] = _NoArg.NO_ARG,
|
|
695
698
|
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
|
|
696
699
|
default: Optional[Any] = _NoArg.NO_ARG,
|
|
@@ -716,6 +719,7 @@ def composite(
|
|
|
716
719
|
return_none_on: Union[_NoArg, None, Callable[..., bool]] = _NoArg.NO_ARG,
|
|
717
720
|
comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
|
|
718
721
|
active_history: bool = False,
|
|
722
|
+
column_template: Optional[str] = None,
|
|
719
723
|
init: Union[_NoArg, bool] = _NoArg.NO_ARG,
|
|
720
724
|
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
|
|
721
725
|
default: Optional[Any] = _NoArg.NO_ARG,
|
|
@@ -785,6 +789,18 @@ def composite(
|
|
|
785
789
|
:class:`.Composite.Comparator` which provides custom SQL
|
|
786
790
|
clause generation for comparison operations.
|
|
787
791
|
|
|
792
|
+
:param column_template: A string template such as ``"person_%s"``,
|
|
793
|
+
containing exactly one ``%s`` placeholder, that's used to generate
|
|
794
|
+
column names for fields of a dataclass :paramref:`.composite.class_`
|
|
795
|
+
that don't otherwise have an explicit name. Only supported for
|
|
796
|
+
dataclass-based composite classes.
|
|
797
|
+
|
|
798
|
+
.. seealso::
|
|
799
|
+
|
|
800
|
+
:ref:`composite_column_template`
|
|
801
|
+
|
|
802
|
+
.. versionadded:: 2.1
|
|
803
|
+
|
|
788
804
|
:param doc:
|
|
789
805
|
optional string that will be applied as the doc on the
|
|
790
806
|
class-bound descriptor.
|
|
@@ -852,6 +868,7 @@ def composite(
|
|
|
852
868
|
raiseload=raiseload,
|
|
853
869
|
comparator_factory=comparator_factory,
|
|
854
870
|
active_history=active_history,
|
|
871
|
+
column_template=column_template,
|
|
855
872
|
info=info,
|
|
856
873
|
doc=doc,
|
|
857
874
|
)
|
|
@@ -208,6 +208,7 @@ class CompositeProperty(
|
|
|
208
208
|
|
|
209
209
|
composite_class: Union[Type[_CC], Callable[..., _CC]]
|
|
210
210
|
attrs: Tuple[_CompositeAttrType[Any], ...]
|
|
211
|
+
column_template: Optional[str]
|
|
211
212
|
|
|
212
213
|
_generated_composite_accessor: CallableReference[
|
|
213
214
|
Optional[Callable[[_CC], Tuple[Any, ...]]]
|
|
@@ -229,6 +230,7 @@ class CompositeProperty(
|
|
|
229
230
|
deferred: bool = False,
|
|
230
231
|
group: Optional[str] = None,
|
|
231
232
|
comparator_factory: Optional[Type[Comparator[_CC]]] = None,
|
|
233
|
+
column_template: Optional[str] = None,
|
|
232
234
|
info: Optional[_InfoType] = None,
|
|
233
235
|
**kwargs: Any,
|
|
234
236
|
):
|
|
@@ -242,6 +244,17 @@ class CompositeProperty(
|
|
|
242
244
|
self.composite_class = _class_or_attr # type: ignore[assignment]
|
|
243
245
|
self.attrs = attrs
|
|
244
246
|
|
|
247
|
+
if column_template is not None:
|
|
248
|
+
try:
|
|
249
|
+
column_template % "x"
|
|
250
|
+
except (TypeError, ValueError) as te:
|
|
251
|
+
raise sa_exc.ArgumentError(
|
|
252
|
+
f"column_template {column_template!r} is not a valid "
|
|
253
|
+
"template; expected a string containing exactly one "
|
|
254
|
+
"'%s' placeholder"
|
|
255
|
+
) from te
|
|
256
|
+
self.column_template = column_template
|
|
257
|
+
|
|
245
258
|
self.return_none_on = return_none_on
|
|
246
259
|
self.active_history = active_history
|
|
247
260
|
self.deferred = deferred
|
|
@@ -428,6 +441,11 @@ class CompositeProperty(
|
|
|
428
441
|
decl_scan, registry, cls, originating_module, key
|
|
429
442
|
)
|
|
430
443
|
else:
|
|
444
|
+
if self.column_template is not None:
|
|
445
|
+
raise sa_exc.ArgumentError(
|
|
446
|
+
"column_template is only supported when composite_class "
|
|
447
|
+
"is a dataclass"
|
|
448
|
+
)
|
|
431
449
|
for attr in self.attrs:
|
|
432
450
|
if (
|
|
433
451
|
isinstance(attr, (MappedColumn, schema.Column))
|
|
@@ -492,8 +510,12 @@ class CompositeProperty(
|
|
|
492
510
|
f"{self.composite_class.__name__} {len(insp.parameters)}"
|
|
493
511
|
)
|
|
494
512
|
if attr is None:
|
|
495
|
-
# fill in missing attr spots with empty MappedColumn
|
|
496
|
-
|
|
513
|
+
# fill in missing attr spots with empty MappedColumn,
|
|
514
|
+
# or one named from column_template if present
|
|
515
|
+
if self.column_template is not None:
|
|
516
|
+
attr = MappedColumn(self.column_template % param.name)
|
|
517
|
+
else:
|
|
518
|
+
attr = MappedColumn()
|
|
497
519
|
self.attrs += (attr,)
|
|
498
520
|
|
|
499
521
|
if isinstance(attr, MappedColumn):
|