SQLAlchemy 2.1.4__py3-none-any.whl → 2.1.6__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/mysql/base.py +6 -1
- sqlalchemy/dialects/oracle/base.py +4 -1
- sqlalchemy/dialects/postgresql/base.py +12 -2
- sqlalchemy/engine/base.py +31 -19
- sqlalchemy/engine/create.py +20 -10
- sqlalchemy/engine/result.py +3 -1
- sqlalchemy/engine/url.py +39 -6
- sqlalchemy/ext/asyncio/result.py +23 -11
- sqlalchemy/orm/__init__.py +2 -0
- sqlalchemy/orm/_orm_constructors.py +134 -0
- sqlalchemy/orm/base.py +54 -0
- sqlalchemy/orm/bulk_persistence.py +5 -3
- sqlalchemy/orm/context.py +89 -3
- sqlalchemy/orm/decl_api.py +22 -5
- sqlalchemy/orm/decl_base.py +106 -22
- sqlalchemy/orm/mapper.py +56 -2
- sqlalchemy/orm/properties.py +75 -1
- sqlalchemy/orm/session.py +8 -1
- sqlalchemy/orm/util.py +9 -2
- sqlalchemy/pool/base.py +55 -30
- sqlalchemy/sql/compiler.py +117 -44
- sqlalchemy/sql/ddl.py +4 -4
- sqlalchemy/sql/dml.py +19 -17
- sqlalchemy/sql/schema.py +2 -0
- sqlalchemy/sql/selectable.py +42 -19
- sqlalchemy/testing/cancellation.py +77 -6
- sqlalchemy/util/langhelpers.py +7 -1
- sqlalchemy/util/queue.py +11 -0
- {sqlalchemy-2.1.4.dist-info → sqlalchemy-2.1.6.dist-info}/METADATA +2 -3
- {sqlalchemy-2.1.4.dist-info → sqlalchemy-2.1.6.dist-info}/RECORD +35 -35
- {sqlalchemy-2.1.4.dist-info → sqlalchemy-2.1.6.dist-info}/WHEEL +1 -1
- {sqlalchemy-2.1.4.dist-info → sqlalchemy-2.1.6.dist-info}/licenses/AUTHORS +0 -0
- {sqlalchemy-2.1.4.dist-info → sqlalchemy-2.1.6.dist-info}/licenses/LICENSE +0 -0
- {sqlalchemy-2.1.4.dist-info → sqlalchemy-2.1.6.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py
CHANGED
|
@@ -2386,6 +2386,8 @@ class MySQLDDLCompiler(
|
|
|
2386
2386
|
class MySQLTypeCompiler(
|
|
2387
2387
|
_mariadb_shim.MariaDBTypeCompilerShim, compiler.GenericTypeCompiler
|
|
2388
2388
|
):
|
|
2389
|
+
dialect: MySQLDialect
|
|
2390
|
+
|
|
2389
2391
|
def _extend_numeric(self, type_: _NumericCommonType, spec: str) -> str:
|
|
2390
2392
|
"Extend a numeric-type declaration with MySQL specific extensions."
|
|
2391
2393
|
|
|
@@ -2680,9 +2682,12 @@ class MySQLTypeCompiler(
|
|
|
2680
2682
|
) -> str:
|
|
2681
2683
|
quoted_enums = []
|
|
2682
2684
|
for e in enumerated_values:
|
|
2685
|
+
e = e.replace("'", "''")
|
|
2686
|
+
if self.dialect._backslash_escapes:
|
|
2687
|
+
e = e.replace("\\", "\\\\")
|
|
2683
2688
|
if self.dialect.identifier_preparer._double_percents:
|
|
2684
2689
|
e = e.replace("%", "%%")
|
|
2685
|
-
quoted_enums.append("'%s'" % e
|
|
2690
|
+
quoted_enums.append("'%s'" % e)
|
|
2686
2691
|
return self._extend_string(
|
|
2687
2692
|
type_, {}, "%s(%s)" % (name, ",".join(quoted_enums))
|
|
2688
2693
|
)
|
|
@@ -2279,7 +2279,10 @@ class OracleDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
2279
2279
|
return self.get_isolation_level(dbapi_conn)
|
|
2280
2280
|
except NotImplementedError:
|
|
2281
2281
|
raise
|
|
2282
|
-
except:
|
|
2282
|
+
except Exception:
|
|
2283
|
+
# Exception only; a bare except here also caught
|
|
2284
|
+
# asyncio.CancelledError and KeyboardInterrupt, returning the
|
|
2285
|
+
# default in place of propagating them
|
|
2283
2286
|
return "READ COMMITTED"
|
|
2284
2287
|
|
|
2285
2288
|
def _execute_reflection(
|
|
@@ -5070,8 +5070,9 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
5070
5070
|
|
|
5071
5071
|
# https://www.postgresql.org/docs/current/static/sql-createtable.html
|
|
5072
5072
|
return re.compile(
|
|
5073
|
-
|
|
5074
|
-
rf"REFERENCES (?:({qtoken})\.)?({qtoken})
|
|
5073
|
+
rf"FOREIGN KEY \((.*?)(?:, PERIOD ({qtoken}))?\) "
|
|
5074
|
+
rf"REFERENCES (?:({qtoken})\.)?({qtoken})"
|
|
5075
|
+
rf"\(((?:{qtoken}(?: *, *)?)+)(?:, PERIOD ({qtoken}))?\)"
|
|
5075
5076
|
r"[\s]?(MATCH (FULL|PARTIAL|SIMPLE)+)?"
|
|
5076
5077
|
r"[\s]?(?:ON (UPDATE|DELETE) "
|
|
5077
5078
|
r"(CASCADE|RESTRICT|NO ACTION|"
|
|
@@ -5089,9 +5090,11 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
5089
5090
|
|
|
5090
5091
|
(
|
|
5091
5092
|
constrained_columns,
|
|
5093
|
+
constrained_period_column,
|
|
5092
5094
|
referred_schema,
|
|
5093
5095
|
referred_table,
|
|
5094
5096
|
referred_columns,
|
|
5097
|
+
referred_period_column,
|
|
5095
5098
|
_,
|
|
5096
5099
|
match,
|
|
5097
5100
|
upddelkey1,
|
|
@@ -5103,6 +5106,13 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
5103
5106
|
initially,
|
|
5104
5107
|
) = m
|
|
5105
5108
|
|
|
5109
|
+
# TODO: the PERIOD designation of a temporal foreign key
|
|
5110
|
+
# (PostgreSQL 18+) is not yet reflected
|
|
5111
|
+
if constrained_period_column:
|
|
5112
|
+
constrained_columns += f", {constrained_period_column}"
|
|
5113
|
+
if referred_period_column:
|
|
5114
|
+
referred_columns += f", {referred_period_column}"
|
|
5115
|
+
|
|
5106
5116
|
onupdate = (
|
|
5107
5117
|
upddelval1
|
|
5108
5118
|
if upddelkey1 == "UPDATE"
|
sqlalchemy/engine/base.py
CHANGED
|
@@ -758,12 +758,19 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
|
|
|
758
758
|
if self.closed:
|
|
759
759
|
raise exc.ResourceClosedError("This Connection is closed")
|
|
760
760
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
761
|
+
try:
|
|
762
|
+
if self._still_open_and_dbapi_connection_is_valid:
|
|
763
|
+
pool_proxied_connection = self._dbapi_connection
|
|
764
|
+
assert pool_proxied_connection is not None
|
|
765
|
+
pool_proxied_connection.invalidate(exception)
|
|
766
|
+
finally:
|
|
767
|
+
# this is always a hard invalidation, which closes the DBAPI
|
|
768
|
+
# connection; for an asyncio driver that close is an awaited
|
|
769
|
+
# terminate() which a cancellation can interrupt. this
|
|
770
|
+
# Connection is invalidated regardless, so that nothing
|
|
771
|
+
# further, such as a rollback, is emitted on the DBAPI
|
|
772
|
+
# connection
|
|
773
|
+
self._dbapi_connection = None
|
|
767
774
|
|
|
768
775
|
def detach(self) -> None:
|
|
769
776
|
"""Detach the underlying DB-API connection from its connection pool.
|
|
@@ -1266,24 +1273,29 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
|
|
|
1266
1273
|
self._transaction.close()
|
|
1267
1274
|
skip_reset = True
|
|
1268
1275
|
finally:
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1276
|
+
try:
|
|
1277
|
+
if self._dbapi_connection is not None:
|
|
1278
|
+
conn = self._dbapi_connection
|
|
1279
|
+
|
|
1280
|
+
# as we just closed the transaction, close the connection
|
|
1281
|
+
# pool connection without doing an additional reset
|
|
1282
|
+
if skip_reset:
|
|
1283
|
+
cast("_ConnectionFairy", conn)._close_special(
|
|
1284
|
+
transaction_reset=True
|
|
1285
|
+
)
|
|
1286
|
+
else:
|
|
1287
|
+
conn.close()
|
|
1288
|
+
finally:
|
|
1281
1289
|
# There is a slight chance that conn.close() may have
|
|
1282
1290
|
# triggered an invalidation here in which case
|
|
1283
1291
|
# _dbapi_connection would already be None, however usually
|
|
1284
1292
|
# it will be non-None here and in a "closed" state.
|
|
1293
|
+
#
|
|
1294
|
+
# there is also a chance that the conn.close() here could
|
|
1295
|
+
# itself be interrupted, such as by a cancellation; hence a
|
|
1296
|
+
# second try/finally is needed as well (see #13699)
|
|
1285
1297
|
self._dbapi_connection = None
|
|
1286
|
-
|
|
1298
|
+
self.__can_reconnect = False
|
|
1287
1299
|
|
|
1288
1300
|
# special case to handle mypy issue:
|
|
1289
1301
|
# https://github.com/python/mypy/issues/20651
|
sqlalchemy/engine/create.py
CHANGED
|
@@ -747,16 +747,26 @@ def create_engine(url: Union[str, _url.URL], **kwargs: Any) -> Engine:
|
|
|
747
747
|
)
|
|
748
748
|
c._execution_options = util.EMPTY_DICT
|
|
749
749
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
750
|
+
dialect.initialize(c)
|
|
751
|
+
|
|
752
|
+
# roll back only once initialize() has succeeded, rather than
|
|
753
|
+
# in a finally: block. when initialize() raises, the DBAPI
|
|
754
|
+
# connection may be partway through an operation that c
|
|
755
|
+
# doesn't know about, such as psycopg's TypeInfo.fetch()
|
|
756
|
+
# interrupted by a cancellation; the rollback then fails
|
|
757
|
+
# and its error replaces the original one. the pool closes
|
|
758
|
+
# the connection in that case, so there's nothing to roll
|
|
759
|
+
# back.
|
|
760
|
+
#
|
|
761
|
+
# note that "invalidated" and "closed" are mutually
|
|
762
|
+
# exclusive in 1.4 Connection. c may be invalidated here if
|
|
763
|
+
# initialize() caught an error on a statement that was a
|
|
764
|
+
# disconnect.
|
|
765
|
+
if not c.invalidated and not c.closed:
|
|
766
|
+
# transaction is rolled back otherwise, tested by
|
|
767
|
+
# test/dialect/postgresql/test_dialect.py
|
|
768
|
+
# ::MiscBackendTest::test_initial_transaction_state
|
|
769
|
+
dialect.do_rollback(c.connection)
|
|
760
770
|
|
|
761
771
|
# previously, the "first_connect" event was used here, which was then
|
|
762
772
|
# scaled back if the "on_connect" handler were present. now,
|
sqlalchemy/engine/result.py
CHANGED
|
@@ -1774,10 +1774,12 @@ class FrozenResult(Generic[Unpack[_Ts]]):
|
|
|
1774
1774
|
self._source_supports_scalars = result._source_supports_scalars
|
|
1775
1775
|
self._attributes = result._attributes
|
|
1776
1776
|
|
|
1777
|
+
# use _allrows() rather than fetchall() so that AsyncResult, whose
|
|
1778
|
+
# fetchall() is a coroutine, may be frozen within greenlet_spawn()
|
|
1777
1779
|
if self._source_supports_scalars:
|
|
1778
1780
|
self.data = list(result._raw_row_iterator())
|
|
1779
1781
|
else:
|
|
1780
|
-
self.data = result.
|
|
1782
|
+
self.data = result._allrows()
|
|
1781
1783
|
|
|
1782
1784
|
def _rewrite_rows(self) -> Sequence[Sequence[Any]]:
|
|
1783
1785
|
# used only by the orm fn merge_frozen_result
|
sqlalchemy/engine/url.py
CHANGED
|
@@ -364,7 +364,11 @@ class URL(NamedTuple):
|
|
|
364
364
|
return self._replace(**kw)
|
|
365
365
|
|
|
366
366
|
def update_query_string(
|
|
367
|
-
self,
|
|
367
|
+
self,
|
|
368
|
+
query_string: str,
|
|
369
|
+
append: bool = False,
|
|
370
|
+
*,
|
|
371
|
+
keep_blank_values: bool = False,
|
|
368
372
|
) -> URL:
|
|
369
373
|
"""Return a new :class:`_engine.URL` object with the :attr:`_engine.URL.query`
|
|
370
374
|
parameter dictionary updated by the given query string.
|
|
@@ -387,6 +391,13 @@ class URL(NamedTuple):
|
|
|
387
391
|
If left at its default of False, keys present in the given query
|
|
388
392
|
parameters will replace those of the existing query string.
|
|
389
393
|
|
|
394
|
+
:param keep_blank_values: if True, parameters with blank values
|
|
395
|
+
such as ``key=`` are retained in the query dictionary as empty
|
|
396
|
+
strings. If left at its default of False, parameters with blank
|
|
397
|
+
values are omitted.
|
|
398
|
+
|
|
399
|
+
.. versionadded:: 2.1.6
|
|
400
|
+
|
|
390
401
|
.. versionadded:: 1.4
|
|
391
402
|
|
|
392
403
|
.. seealso::
|
|
@@ -396,7 +407,10 @@ class URL(NamedTuple):
|
|
|
396
407
|
:meth:`_engine.URL.update_query_dict`
|
|
397
408
|
|
|
398
409
|
""" # noqa: E501
|
|
399
|
-
return self.update_query_pairs(
|
|
410
|
+
return self.update_query_pairs(
|
|
411
|
+
parse_qsl(query_string, keep_blank_values=keep_blank_values),
|
|
412
|
+
append=append,
|
|
413
|
+
)
|
|
400
414
|
|
|
401
415
|
def update_query_pairs(
|
|
402
416
|
self,
|
|
@@ -836,7 +850,9 @@ class URL(NamedTuple):
|
|
|
836
850
|
return translated
|
|
837
851
|
|
|
838
852
|
|
|
839
|
-
def make_url(
|
|
853
|
+
def make_url(
|
|
854
|
+
name_or_url: Union[str, URL], *, keep_blank_values: bool = False
|
|
855
|
+
) -> URL:
|
|
840
856
|
"""Given a string, produce a new URL instance.
|
|
841
857
|
|
|
842
858
|
The format of the URL generally follows `RFC-1738
|
|
@@ -846,6 +862,21 @@ def make_url(name_or_url: Union[str, URL]) -> URL:
|
|
|
846
862
|
|
|
847
863
|
If a :class:`.URL` object is passed, it is returned as is.
|
|
848
864
|
|
|
865
|
+
:param name_or_url: a string URL or :class:`.URL` object.
|
|
866
|
+
|
|
867
|
+
:param keep_blank_values: if True, query string parameters with blank
|
|
868
|
+
values such as ``key=`` are retained in the :attr:`.URL.query`
|
|
869
|
+
dictionary as empty strings. If left at its default of False,
|
|
870
|
+
parameters with blank values are omitted. Has no effect when a
|
|
871
|
+
:class:`.URL` object is passed.
|
|
872
|
+
|
|
873
|
+
As :func:`_sa.create_engine` parses string URLs using the default
|
|
874
|
+
setting, a string URL with blank values to be retained should be
|
|
875
|
+
parsed with :func:`.make_url` first, and the resulting :class:`.URL`
|
|
876
|
+
passed to :func:`_sa.create_engine`.
|
|
877
|
+
|
|
878
|
+
.. versionadded:: 2.1.6
|
|
879
|
+
|
|
849
880
|
.. seealso::
|
|
850
881
|
|
|
851
882
|
:ref:`database_urls`
|
|
@@ -853,7 +884,7 @@ def make_url(name_or_url: Union[str, URL]) -> URL:
|
|
|
853
884
|
"""
|
|
854
885
|
|
|
855
886
|
if isinstance(name_or_url, str):
|
|
856
|
-
return _parse_url(name_or_url)
|
|
887
|
+
return _parse_url(name_or_url, keep_blank_values=keep_blank_values)
|
|
857
888
|
elif not isinstance(name_or_url, URL) and not hasattr(
|
|
858
889
|
name_or_url, "_sqla_is_testing_if_this_is_a_mock_object"
|
|
859
890
|
):
|
|
@@ -864,7 +895,7 @@ def make_url(name_or_url: Union[str, URL]) -> URL:
|
|
|
864
895
|
return name_or_url
|
|
865
896
|
|
|
866
897
|
|
|
867
|
-
def _parse_url(name: str) -> URL:
|
|
898
|
+
def _parse_url(name: str, keep_blank_values: bool = False) -> URL:
|
|
868
899
|
pattern = re.compile(
|
|
869
900
|
r"""
|
|
870
901
|
(?P<name>[\w\+]+)://
|
|
@@ -892,7 +923,9 @@ def _parse_url(name: str) -> URL:
|
|
|
892
923
|
if components["query"] is not None:
|
|
893
924
|
query = {}
|
|
894
925
|
|
|
895
|
-
for key, value in parse_qsl(
|
|
926
|
+
for key, value in parse_qsl(
|
|
927
|
+
components["query"], keep_blank_values=keep_blank_values
|
|
928
|
+
):
|
|
896
929
|
if key in query:
|
|
897
930
|
query[key] = util.to_list(query[key])
|
|
898
931
|
cast("List[str]", query[key]).append(value)
|
sqlalchemy/ext/asyncio/result.py
CHANGED
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import operator
|
|
10
10
|
from typing import Any
|
|
11
11
|
from typing import AsyncIterator
|
|
12
|
+
from typing import Iterator
|
|
12
13
|
from typing import Literal
|
|
13
14
|
from typing import Optional
|
|
14
15
|
from typing import overload
|
|
@@ -20,6 +21,7 @@ from typing import TypeVar
|
|
|
20
21
|
from . import exc as async_exc
|
|
21
22
|
from ... import util
|
|
22
23
|
from ...engine import Result
|
|
24
|
+
from ...engine._result_cy import _RowData
|
|
23
25
|
from ...engine.result import _NO_ROW
|
|
24
26
|
from ...engine.result import _R
|
|
25
27
|
from ...engine.result import _WithKeys
|
|
@@ -467,6 +469,11 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[Unpack[_Ts]]]):
|
|
|
467
469
|
|
|
468
470
|
return await greenlet_spawn(FrozenResult, self)
|
|
469
471
|
|
|
472
|
+
def _raw_row_iterator(self) -> Iterator[_RowData]:
|
|
473
|
+
# used by FrozenResult when the source result supports scalars;
|
|
474
|
+
# must be invoked within greenlet_spawn()
|
|
475
|
+
return self._real_result._raw_row_iterator()
|
|
476
|
+
|
|
470
477
|
@overload
|
|
471
478
|
def scalars(
|
|
472
479
|
self: AsyncResult[_T, Unpack[TupleAny]], index: Literal[0]
|
|
@@ -494,7 +501,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[Unpack[_Ts]]]):
|
|
|
494
501
|
referring to this :class:`_asyncio.AsyncResult` object.
|
|
495
502
|
|
|
496
503
|
"""
|
|
497
|
-
return AsyncScalarResult(self
|
|
504
|
+
return AsyncScalarResult(self, index)
|
|
498
505
|
|
|
499
506
|
def mappings(self) -> AsyncMappingResult:
|
|
500
507
|
"""Apply a mappings filter to returned rows, returning an instance of
|
|
@@ -505,11 +512,11 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[Unpack[_Ts]]]):
|
|
|
505
512
|
objects.
|
|
506
513
|
|
|
507
514
|
:return: a new :class:`_asyncio.AsyncMappingResult` filtering object
|
|
508
|
-
referring to
|
|
515
|
+
referring to this :class:`_asyncio.AsyncResult` object.
|
|
509
516
|
|
|
510
517
|
"""
|
|
511
518
|
|
|
512
|
-
return AsyncMappingResult(self
|
|
519
|
+
return AsyncMappingResult(self)
|
|
513
520
|
|
|
514
521
|
|
|
515
522
|
class AsyncScalarResult(AsyncCommon[_R]):
|
|
@@ -532,19 +539,21 @@ class AsyncScalarResult(AsyncCommon[_R]):
|
|
|
532
539
|
|
|
533
540
|
def __init__(
|
|
534
541
|
self,
|
|
535
|
-
|
|
542
|
+
result: AsyncResult[Unpack[TupleAny]],
|
|
536
543
|
index: _KeyIndexType,
|
|
537
544
|
):
|
|
538
|
-
|
|
545
|
+
# rows are fetched from the sync Result; metadata and unique state
|
|
546
|
+
# come from the AsyncResult, where unique() and columns() apply them
|
|
547
|
+
self._real_result = result._real_result
|
|
539
548
|
|
|
540
|
-
if
|
|
541
|
-
self._metadata =
|
|
549
|
+
if result._source_supports_scalars:
|
|
550
|
+
self._metadata = result._metadata
|
|
542
551
|
self._post_creational_filter = None
|
|
543
552
|
else:
|
|
544
|
-
self._metadata =
|
|
553
|
+
self._metadata = result._metadata._reduce([index])
|
|
545
554
|
self._post_creational_filter = operator.itemgetter(0)
|
|
546
555
|
|
|
547
|
-
self._unique_filter_state =
|
|
556
|
+
self._unique_filter_state = result._unique_filter_state
|
|
548
557
|
|
|
549
558
|
def unique(
|
|
550
559
|
self,
|
|
@@ -665,8 +674,11 @@ class AsyncMappingResult(_WithKeys, AsyncCommon[RowMapping]):
|
|
|
665
674
|
|
|
666
675
|
_post_creational_filter = operator.attrgetter("_mapping")
|
|
667
676
|
|
|
668
|
-
def __init__(self, result:
|
|
669
|
-
|
|
677
|
+
def __init__(self, result: AsyncResult[Unpack[TupleAny]]):
|
|
678
|
+
# rows are fetched from the sync Result; metadata and unique state
|
|
679
|
+
# come from the AsyncResult, where unique() and columns() apply them
|
|
680
|
+
self._real_result = result._real_result
|
|
681
|
+
|
|
670
682
|
self._unique_filter_state = result._unique_filter_state
|
|
671
683
|
self._metadata = result._metadata
|
|
672
684
|
if result._source_supports_scalars:
|
sqlalchemy/orm/__init__.py
CHANGED
|
@@ -37,6 +37,7 @@ from ._orm_constructors import outerjoin as outerjoin
|
|
|
37
37
|
from ._orm_constructors import query_expression as query_expression
|
|
38
38
|
from ._orm_constructors import relationship as relationship
|
|
39
39
|
from ._orm_constructors import synonym as synonym
|
|
40
|
+
from ._orm_constructors import unmapped_column as unmapped_column
|
|
40
41
|
from ._orm_constructors import with_loader_criteria as with_loader_criteria
|
|
41
42
|
from ._orm_constructors import with_polymorphic as with_polymorphic
|
|
42
43
|
from .attributes import AttributeEventToken as AttributeEventToken
|
|
@@ -51,6 +52,7 @@ from .base import NotExtension as NotExtension
|
|
|
51
52
|
from .base import ORMDescriptor as ORMDescriptor
|
|
52
53
|
from .base import PassiveFlag as PassiveFlag
|
|
53
54
|
from .base import SQLORMExpression as SQLORMExpression
|
|
55
|
+
from .base import Unmapped as Unmapped
|
|
54
56
|
from .base import WriteOnlyMapped as WriteOnlyMapped
|
|
55
57
|
from .context import FromStatement as FromStatement
|
|
56
58
|
from .context import QueryContext as QueryContext
|
|
@@ -29,6 +29,7 @@ from .descriptor_props import Synonym
|
|
|
29
29
|
from .interfaces import _AttributeOptions
|
|
30
30
|
from .properties import MappedColumn
|
|
31
31
|
from .properties import MappedSQLExpression
|
|
32
|
+
from .properties import UnmappedColumn
|
|
32
33
|
from .query import AliasOption
|
|
33
34
|
from .relationships import _RelationshipArgumentType
|
|
34
35
|
from .relationships import _RelationshipBackPopulatesArgument
|
|
@@ -395,6 +396,139 @@ def mapped_column(
|
|
|
395
396
|
)
|
|
396
397
|
|
|
397
398
|
|
|
399
|
+
def unmapped_column(
|
|
400
|
+
__name_pos: Optional[
|
|
401
|
+
Union[str, _TypeEngineArgument[Any], SchemaEventTarget]
|
|
402
|
+
] = None,
|
|
403
|
+
__type_pos: Optional[
|
|
404
|
+
Union[_TypeEngineArgument[Any], SchemaEventTarget]
|
|
405
|
+
] = None,
|
|
406
|
+
/,
|
|
407
|
+
*args: SchemaEventTarget,
|
|
408
|
+
init: Literal[False] = False,
|
|
409
|
+
nullable: Optional[
|
|
410
|
+
Union[bool, Literal[SchemaConst.NULL_UNSPECIFIED]]
|
|
411
|
+
] = SchemaConst.NULL_UNSPECIFIED,
|
|
412
|
+
primary_key: Optional[bool] = False,
|
|
413
|
+
autoincrement: _AutoIncrementType = "auto",
|
|
414
|
+
use_existing_column: bool = False,
|
|
415
|
+
name: Optional[str] = None,
|
|
416
|
+
type_: Optional[_TypeEngineArgument[Any]] = None,
|
|
417
|
+
index: Optional[bool] = None,
|
|
418
|
+
unique: Optional[bool] = None,
|
|
419
|
+
info: Optional[_InfoType] = None,
|
|
420
|
+
default: Optional[Any] = _NoArg.NO_ARG,
|
|
421
|
+
insert_default: Optional[Any] = _NoArg.NO_ARG,
|
|
422
|
+
onupdate: Optional[Any] = None,
|
|
423
|
+
server_default: Optional[_ServerDefaultArgument] = None,
|
|
424
|
+
server_onupdate: Optional[_ServerOnUpdateArgument] = None,
|
|
425
|
+
quote: Optional[bool] = None,
|
|
426
|
+
system: bool = False,
|
|
427
|
+
comment: Optional[str] = None,
|
|
428
|
+
sort_order: Union[_NoArg, int] = _NoArg.NO_ARG,
|
|
429
|
+
**kw: Any,
|
|
430
|
+
) -> UnmappedColumn[Any]:
|
|
431
|
+
r"""declare a :class:`_schema.Column` that is part of the
|
|
432
|
+
:class:`_schema.Table` for a :ref:`Declarative Table
|
|
433
|
+
<orm_declarative_table>` mapping, but which is not mapped by the ORM.
|
|
434
|
+
|
|
435
|
+
:func:`_orm.unmapped_column` is used with the :class:`_orm.Unmapped`
|
|
436
|
+
annotation::
|
|
437
|
+
|
|
438
|
+
class User(Base):
|
|
439
|
+
__tablename__ = "user"
|
|
440
|
+
|
|
441
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
442
|
+
name: Mapped[str]
|
|
443
|
+
|
|
444
|
+
legacy_name: Unmapped[str | None] = unmapped_column()
|
|
445
|
+
|
|
446
|
+
The resulting :class:`_schema.Column` is added to the
|
|
447
|
+
:class:`_schema.Table` in the same way as one produced by
|
|
448
|
+
:func:`_orm.mapped_column`, deriving its type and nullability from the
|
|
449
|
+
annotation, so that the column is present in DDL as well as for schema
|
|
450
|
+
comparison tools such as Alembic autogenerate. However, the column is
|
|
451
|
+
excluded from the mapping, the attribute is removed from the class, and
|
|
452
|
+
when using :ref:`orm_declarative_native_dataclasses` it is not part of
|
|
453
|
+
the dataclass.
|
|
454
|
+
|
|
455
|
+
A bare ``Unmapped[]`` annotation with no right-hand side is equivalent
|
|
456
|
+
to ``unmapped_column()`` with no arguments; however, see the note at
|
|
457
|
+
:class:`_orm.Unmapped` regarding typing tools and dataclasses.
|
|
458
|
+
|
|
459
|
+
As the attribute is not part of the Python model, ORM and dataclass
|
|
460
|
+
oriented arguments of :func:`_orm.mapped_column`, such as
|
|
461
|
+
``default_factory`` and ``deferred``, are not part of
|
|
462
|
+
:func:`_orm.unmapped_column`.
|
|
463
|
+
|
|
464
|
+
.. versionadded:: 2.1.5
|
|
465
|
+
|
|
466
|
+
.. seealso::
|
|
467
|
+
|
|
468
|
+
:ref:`orm_declarative_unmapped_column`
|
|
469
|
+
|
|
470
|
+
:param __name: String name to give to the :class:`_schema.Column`;
|
|
471
|
+
defaults to the attribute name.
|
|
472
|
+
:param __type: :class:`_types.TypeEngine` type or instance for the
|
|
473
|
+
:class:`_schema.Column`; if omitted, it is derived from the
|
|
474
|
+
annotation, or from a :class:`_schema.ForeignKey` if present.
|
|
475
|
+
:param \*args: Additional positional arguments such as
|
|
476
|
+
:class:`_schema.ForeignKey` and :class:`_schema.CheckConstraint`,
|
|
477
|
+
passed through to the :class:`_schema.Column`.
|
|
478
|
+
:param init: Always ``False``; this parameter is present so that
|
|
479
|
+
:pep:`681` typing tools omit the attribute from the ``__init__()``
|
|
480
|
+
method of :ref:`ORM mapped dataclasses
|
|
481
|
+
<orm_declarative_native_dataclasses>`.
|
|
482
|
+
:param nullable: whether the column is "NULL" or "NOT NULL"; if omitted,
|
|
483
|
+
derived from the annotation as with :func:`_orm.mapped_column`.
|
|
484
|
+
:param primary_key: indicates the :class:`_schema.Column` is part of the
|
|
485
|
+
table's primary key. As the mapper requires that its primary key
|
|
486
|
+
columns be mapped, the columns that the mapper should use for the
|
|
487
|
+
identity of the class must then be indicated using the
|
|
488
|
+
:paramref:`_orm.Mapper.primary_key` parameter; see
|
|
489
|
+
:ref:`orm_declarative_unmapped_column` for an example.
|
|
490
|
+
:param autoincrement: Passed directly to the
|
|
491
|
+
:paramref:`_schema.Column.autoincrement` parameter.
|
|
492
|
+
:param use_existing_column: see
|
|
493
|
+
:paramref:`_orm.mapped_column.use_existing_column`.
|
|
494
|
+
:param default: Passed directly to the :paramref:`_schema.Column.default`
|
|
495
|
+
parameter. As the attribute is not part of a dataclass, this is
|
|
496
|
+
strictly a Core column default.
|
|
497
|
+
:param insert_default: synonym for
|
|
498
|
+
:paramref:`_orm.unmapped_column.default`.
|
|
499
|
+
:param sort_order: see :paramref:`_orm.mapped_column.sort_order`.
|
|
500
|
+
:param \**kw: All remaining keyword arguments are passed through to the
|
|
501
|
+
constructor for the :class:`_schema.Column`, including dialect-specific
|
|
502
|
+
arguments.
|
|
503
|
+
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
return UnmappedColumn(
|
|
507
|
+
__name_pos,
|
|
508
|
+
__type_pos,
|
|
509
|
+
*args,
|
|
510
|
+
name=name,
|
|
511
|
+
type_=type_,
|
|
512
|
+
default=default,
|
|
513
|
+
insert_default=insert_default,
|
|
514
|
+
index=index,
|
|
515
|
+
unique=unique,
|
|
516
|
+
info=info,
|
|
517
|
+
nullable=nullable,
|
|
518
|
+
primary_key=primary_key,
|
|
519
|
+
autoincrement=autoincrement,
|
|
520
|
+
onupdate=onupdate,
|
|
521
|
+
server_default=server_default,
|
|
522
|
+
server_onupdate=server_onupdate,
|
|
523
|
+
use_existing_column=use_existing_column,
|
|
524
|
+
quote=quote,
|
|
525
|
+
comment=comment,
|
|
526
|
+
system=system,
|
|
527
|
+
sort_order=sort_order,
|
|
528
|
+
**kw,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
398
532
|
def orm_insert_sentinel(
|
|
399
533
|
name: Optional[str] = None,
|
|
400
534
|
type_: Optional[_TypeEngineArgument[Any]] = None,
|
sqlalchemy/orm/base.py
CHANGED
|
@@ -881,6 +881,60 @@ class _DeclarativeMapped(Mapped[_T_co], _MappedAttribute[_T_co]):
|
|
|
881
881
|
return NotImplemented
|
|
882
882
|
|
|
883
883
|
|
|
884
|
+
class Unmapped(_MappedAnnotationBase[_T_co]):
|
|
885
|
+
"""Represent a database column that is part of the mapped
|
|
886
|
+
:class:`_schema.Table`, but is not mapped by the ORM.
|
|
887
|
+
|
|
888
|
+
The :class:`_orm.Unmapped` type annotation is used in an
|
|
889
|
+
:ref:`Annotated Declarative Table <orm_declarative_mapped_column>`
|
|
890
|
+
mapping, in conjunction with the :func:`_orm.unmapped_column`
|
|
891
|
+
construct, to indicate a column that should be present in the
|
|
892
|
+
:class:`_schema.Table` generated for the class, while not being part
|
|
893
|
+
of the Python model at all. The type and nullability of the
|
|
894
|
+
:class:`_schema.Column` are derived from the annotation in the same
|
|
895
|
+
way as for :class:`_orm.Mapped`.
|
|
896
|
+
|
|
897
|
+
E.g.::
|
|
898
|
+
|
|
899
|
+
class User(Base):
|
|
900
|
+
__tablename__ = "user"
|
|
901
|
+
|
|
902
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
903
|
+
name: Mapped[str]
|
|
904
|
+
|
|
905
|
+
legacy_name: Unmapped[str | None] = unmapped_column()
|
|
906
|
+
|
|
907
|
+
The ``legacy_name`` column is part of ``User.__table__``, but there is
|
|
908
|
+
no ``User.legacy_name`` attribute, and when using
|
|
909
|
+
:ref:`orm_declarative_native_dataclasses`, it is not part of the
|
|
910
|
+
dataclass.
|
|
911
|
+
|
|
912
|
+
A bare ``Unmapped[]`` annotation with no right-hand side is equivalent to
|
|
913
|
+
``unmapped_column()`` with no arguments. When using
|
|
914
|
+
:ref:`orm_declarative_native_dataclasses`, the attribute is omitted from
|
|
915
|
+
the dataclass and its ``__init__()`` method in both cases; however,
|
|
916
|
+
:pep:`681` typing tools are only able to determine this when
|
|
917
|
+
:func:`_orm.unmapped_column` is present, and otherwise see the attribute
|
|
918
|
+
as a required ``__init__()`` argument, so it should always be included
|
|
919
|
+
in that case.
|
|
920
|
+
|
|
921
|
+
.. versionadded:: 2.1.5
|
|
922
|
+
|
|
923
|
+
.. seealso::
|
|
924
|
+
|
|
925
|
+
:ref:`orm_declarative_unmapped_column`
|
|
926
|
+
|
|
927
|
+
:func:`_orm.unmapped_column`
|
|
928
|
+
|
|
929
|
+
"""
|
|
930
|
+
|
|
931
|
+
__slots__ = ()
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _is_unmapped_container(mapped_container: Optional[type]) -> bool:
|
|
935
|
+
return mapped_container is Unmapped
|
|
936
|
+
|
|
937
|
+
|
|
884
938
|
class DynamicMapped(_MappedAnnotationBase[_T_co]):
|
|
885
939
|
"""Represent the ORM mapped attribute type for a "dynamic" relationship.
|
|
886
940
|
|
|
@@ -1933,9 +1933,11 @@ class _BulkORMUpdate(_BulkUDCompileState, UpdateDMLState):
|
|
|
1933
1933
|
# attributes that were formerly modified instead get expired.
|
|
1934
1934
|
# this only gets hit if the session had pending changes
|
|
1935
1935
|
# and autoflush were set to False.
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1936
|
+
formerly_modified = attrib.intersection(dict_).difference(
|
|
1937
|
+
to_evaluate
|
|
1938
|
+
)
|
|
1939
|
+
if formerly_modified:
|
|
1940
|
+
state._expire_attributes(dict_, formerly_modified)
|
|
1939
1941
|
|
|
1940
1942
|
states.add(state)
|
|
1941
1943
|
session._register_altered(states)
|