SQLAlchemy 2.1.6__py3-none-any.whl → 2.1.8__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 +10 -18
- sqlalchemy/dialects/oracle/base.py +1 -1
- sqlalchemy/dialects/postgresql/array.py +3 -4
- sqlalchemy/dialects/postgresql/base.py +6 -3
- sqlalchemy/engine/base.py +40 -36
- sqlalchemy/engine/url.py +2 -1
- sqlalchemy/ext/asyncio/session.py +71 -0
- sqlalchemy/inspection.py +2 -2
- sqlalchemy/orm/context.py +24 -0
- sqlalchemy/orm/mapper.py +33 -0
- sqlalchemy/orm/persistence.py +35 -10
- sqlalchemy/orm/query.py +3 -3
- sqlalchemy/orm/relationships.py +22 -6
- sqlalchemy/orm/session.py +101 -18
- sqlalchemy/orm/strategies.py +25 -13
- sqlalchemy/orm/strategy_options.py +10 -4
- sqlalchemy/orm/util.py +7 -9
- sqlalchemy/pool/base.py +18 -2
- sqlalchemy/sql/_annotated_cols.py +3 -3
- sqlalchemy/sql/_elements_constructors.py +7 -6
- sqlalchemy/sql/_typing.py +21 -0
- sqlalchemy/sql/compiler.py +17 -13
- sqlalchemy/sql/elements.py +65 -16
- sqlalchemy/sql/functions.py +4 -3
- sqlalchemy/sql/operators.py +2 -2
- sqlalchemy/sql/schema.py +70 -4
- sqlalchemy/sql/sqltypes.py +3 -4
- sqlalchemy/sql/type_api.py +16 -3
- sqlalchemy/util/compat.py +36 -0
- sqlalchemy/util/queue.py +4 -8
- {sqlalchemy-2.1.6.dist-info → sqlalchemy-2.1.8.dist-info}/METADATA +1 -1
- {sqlalchemy-2.1.6.dist-info → sqlalchemy-2.1.8.dist-info}/RECORD +37 -37
- {sqlalchemy-2.1.6.dist-info → sqlalchemy-2.1.8.dist-info}/WHEEL +0 -0
- {sqlalchemy-2.1.6.dist-info → sqlalchemy-2.1.8.dist-info}/licenses/AUTHORS +0 -0
- {sqlalchemy-2.1.6.dist-info → sqlalchemy-2.1.8.dist-info}/licenses/LICENSE +0 -0
- {sqlalchemy-2.1.6.dist-info → sqlalchemy-2.1.8.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py
CHANGED
|
@@ -3553,35 +3553,27 @@ class MSDialect(default._BackendsMultiReflection, default.DefaultDialect):
|
|
|
3553
3553
|
else:
|
|
3554
3554
|
raise exc.NoSuchTableError(f"{owner}.{viewname}")
|
|
3555
3555
|
|
|
3556
|
-
def _temp_table_name_like_pattern(self, tablename):
|
|
3557
|
-
# LIKE uses '%' to match zero or more characters and '_' to match any
|
|
3558
|
-
# single character. We want to match literal underscores, so T-SQL
|
|
3559
|
-
# requires that we enclose them in square brackets.
|
|
3560
|
-
return tablename + (
|
|
3561
|
-
("[_][_][_]%") if not tablename.startswith("##") else ""
|
|
3562
|
-
)
|
|
3563
|
-
|
|
3564
3556
|
def _get_internal_temp_table_name(self, connection, tablename):
|
|
3565
3557
|
# it's likely that schema is always "dbo", but since we can
|
|
3566
3558
|
# get it here, let's get it.
|
|
3567
3559
|
# see https://stackoverflow.com/questions/8311959/
|
|
3568
3560
|
# specifying-schema-for-temporary-tables
|
|
3569
3561
|
|
|
3562
|
+
# object_id() resolves a local temp table name only against the
|
|
3563
|
+
# current session's tables (plus any ##global one), so the lookup
|
|
3564
|
+
# neither sees nor locks other sessions' same-named temp tables in
|
|
3565
|
+
# tempdb, as a scan of tempdb's catalog by name would. See #13754.
|
|
3570
3566
|
try:
|
|
3571
3567
|
return connection.execute(
|
|
3572
3568
|
sql.text(
|
|
3573
|
-
"select
|
|
3574
|
-
"from tempdb.
|
|
3575
|
-
"
|
|
3569
|
+
"select s.name, o.name "
|
|
3570
|
+
"from tempdb.sys.objects as o "
|
|
3571
|
+
"join tempdb.sys.schemas as s "
|
|
3572
|
+
"on s.schema_id = o.schema_id "
|
|
3573
|
+
"where o.object_id = object_id(:qualified, 'U')"
|
|
3576
3574
|
),
|
|
3577
|
-
{"
|
|
3575
|
+
{"qualified": f"tempdb..[{tablename}]"},
|
|
3578
3576
|
).one()
|
|
3579
|
-
except exc.MultipleResultsFound as me:
|
|
3580
|
-
raise exc.UnreflectableTableError(
|
|
3581
|
-
"Found more than one temporary table named '%s' in tempdb "
|
|
3582
|
-
"at this time. Cannot reliably resolve that name to its "
|
|
3583
|
-
"internal table name." % tablename
|
|
3584
|
-
) from me
|
|
3585
3577
|
except exc.NoResultFound as ne:
|
|
3586
3578
|
raise exc.NoSuchTableError(
|
|
3587
3579
|
"Unable to find a temporary table named '%s' in tempdb."
|
|
@@ -1932,7 +1932,7 @@ class OracleDDLCompiler(compiler.DDLCompiler):
|
|
|
1932
1932
|
def define_constraint_cascades(self, constraint):
|
|
1933
1933
|
text = ""
|
|
1934
1934
|
if constraint.ondelete is not None:
|
|
1935
|
-
text +=
|
|
1935
|
+
text += self.define_constraint_ondelete_cascade(constraint)
|
|
1936
1936
|
|
|
1937
1937
|
# oracle has no ON UPDATE CASCADE -
|
|
1938
1938
|
# its only available via triggers
|
|
@@ -29,6 +29,7 @@ from ...sql.visitors import InternalTraversal
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from ...engine.interfaces import Dialect
|
|
31
31
|
from ...sql._typing import _ColumnExpressionArgument
|
|
32
|
+
from ...sql._typing import _CovariantTypeEngineArgument
|
|
32
33
|
from ...sql._typing import _TypeEngineArgument
|
|
33
34
|
from ...sql.elements import ColumnElement
|
|
34
35
|
from ...sql.elements import Grouping
|
|
@@ -324,7 +325,7 @@ class ARRAY(sqltypes.ARRAY[_T]):
|
|
|
324
325
|
|
|
325
326
|
def __init__(
|
|
326
327
|
self,
|
|
327
|
-
item_type:
|
|
328
|
+
item_type: _CovariantTypeEngineArgument[_T],
|
|
328
329
|
as_tuple: bool = False,
|
|
329
330
|
dimensions: Optional[int] = None,
|
|
330
331
|
zero_indexes: bool = False,
|
|
@@ -366,9 +367,7 @@ class ARRAY(sqltypes.ARRAY[_T]):
|
|
|
366
367
|
"Do not nest ARRAY types; ARRAY(basetype) "
|
|
367
368
|
"handles multi-dimensional arrays of basetype"
|
|
368
369
|
)
|
|
369
|
-
|
|
370
|
-
item_type = item_type()
|
|
371
|
-
self.item_type = item_type
|
|
370
|
+
self.item_type = sqltypes.to_instance(item_type)
|
|
372
371
|
self.as_tuple = as_tuple
|
|
373
372
|
self.dimensions = dimensions
|
|
374
373
|
self.zero_indexes = zero_indexes
|
|
@@ -2783,10 +2783,13 @@ class PGDDLCompiler(compiler.DDLCompiler):
|
|
|
2783
2783
|
|
|
2784
2784
|
@util.memoized_property
|
|
2785
2785
|
def _fk_ondelete_pattern(self):
|
|
2786
|
+
# a keyword phrase, optionally followed by the column list accepted
|
|
2787
|
+
# by SET NULL / SET DEFAULT. column names may be double-quoted, as
|
|
2788
|
+
# rendered by pg_get_constraintdef() when reflected
|
|
2789
|
+
ident = r'(?:[\w$]+|"(?:[^"]|"")+")'
|
|
2786
2790
|
return re.compile(
|
|
2787
|
-
r"
|
|
2788
|
-
|
|
2789
|
-
re.I,
|
|
2791
|
+
r"^\w+(?:\s+\w+)*"
|
|
2792
|
+
rf"(?:\s*\(\s*{ident}(?:\s*,\s*{ident})*\s*\))?$"
|
|
2790
2793
|
)
|
|
2791
2794
|
|
|
2792
2795
|
def define_constraint_ondelete_cascade(self, constraint):
|
sqlalchemy/engine/base.py
CHANGED
|
@@ -129,6 +129,11 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
|
|
|
129
129
|
|
|
130
130
|
_execution_options: _ExecuteOptions
|
|
131
131
|
|
|
132
|
+
# set to True at the class level by ConnectionEvents when listeners
|
|
133
|
+
# are associated with the Connection class; set at the instance level
|
|
134
|
+
# when listeners are associated with a specific Connection
|
|
135
|
+
_has_events: bool = False
|
|
136
|
+
|
|
132
137
|
_transaction: Optional[RootTransaction]
|
|
133
138
|
_nested_transaction: Optional[NestedTransaction]
|
|
134
139
|
|
|
@@ -167,10 +172,13 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
|
|
|
167
172
|
# if _has_events is sent explicitly as False,
|
|
168
173
|
# then don't join the dispatch of the engine; we don't
|
|
169
174
|
# want to handle any of the engine's events in that case.
|
|
175
|
+
# the engine's _has_events is checked alongside our own at
|
|
176
|
+
# each event point; leave our own _has_events unset so that
|
|
177
|
+
# the class-level flag, set by listeners on the Connection
|
|
178
|
+
# class, takes effect
|
|
170
179
|
self.dispatch = self.dispatch._join(engine.dispatch)
|
|
171
|
-
|
|
172
|
-
_has_events
|
|
173
|
-
)
|
|
180
|
+
else:
|
|
181
|
+
self._has_events = _has_events
|
|
174
182
|
|
|
175
183
|
self._execution_options = engine._execution_options
|
|
176
184
|
|
|
@@ -181,6 +189,29 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
|
|
|
181
189
|
# characteristics.LoggingTokenCharacteristic
|
|
182
190
|
_message_formatter: Any = None
|
|
183
191
|
|
|
192
|
+
def _log_parameters(
|
|
193
|
+
self,
|
|
194
|
+
parameters: _AnyExecuteParams | None,
|
|
195
|
+
stats: str,
|
|
196
|
+
*,
|
|
197
|
+
ismulti: bool,
|
|
198
|
+
) -> None:
|
|
199
|
+
if not self.engine.hide_parameters:
|
|
200
|
+
self._log_info(
|
|
201
|
+
"[%s] %r",
|
|
202
|
+
stats,
|
|
203
|
+
sql_util._repr_params(
|
|
204
|
+
parameters,
|
|
205
|
+
batches=10,
|
|
206
|
+
ismulti=ismulti,
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
self._log_info(
|
|
211
|
+
"[%s] [SQL parameters hidden due to hide_parameters=True]",
|
|
212
|
+
stats,
|
|
213
|
+
)
|
|
214
|
+
|
|
184
215
|
def _log_info(self, message: str, *arg: Any, **kw: Any) -> None:
|
|
185
216
|
fmt = self._message_formatter
|
|
186
217
|
|
|
@@ -1893,22 +1924,10 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
|
|
|
1893
1924
|
|
|
1894
1925
|
stats = context._get_cache_stats()
|
|
1895
1926
|
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
sql_util._repr_params(
|
|
1901
|
-
effective_parameters,
|
|
1902
|
-
batches=10,
|
|
1903
|
-
ismulti=context.executemany,
|
|
1904
|
-
),
|
|
1905
|
-
)
|
|
1906
|
-
else:
|
|
1907
|
-
self._log_info(
|
|
1908
|
-
"[%s] [SQL parameters hidden due to "
|
|
1909
|
-
"hide_parameters=True]",
|
|
1910
|
-
stats,
|
|
1911
|
-
)
|
|
1927
|
+
self._log_parameters(
|
|
1928
|
+
effective_parameters, stats, ismulti=context.executemany
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1912
1931
|
if context.execute_style is ExecuteStyle.EXECUTEMANY:
|
|
1913
1932
|
effective_parameters = cast(
|
|
1914
1933
|
"_CoreMultiExecuteParams", effective_parameters
|
|
@@ -2080,22 +2099,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
|
|
|
2080
2099
|
else:
|
|
2081
2100
|
stats = f"insertmanyvalues{imv_stats}"
|
|
2082
2101
|
|
|
2083
|
-
|
|
2084
|
-
self._log_info(
|
|
2085
|
-
"[%s] %r",
|
|
2086
|
-
stats,
|
|
2087
|
-
sql_util._repr_params(
|
|
2088
|
-
sub_params,
|
|
2089
|
-
batches=10,
|
|
2090
|
-
ismulti=False,
|
|
2091
|
-
),
|
|
2092
|
-
)
|
|
2093
|
-
else:
|
|
2094
|
-
self._log_info(
|
|
2095
|
-
"[%s] [SQL parameters hidden due to "
|
|
2096
|
-
"hide_parameters=True]",
|
|
2097
|
-
stats,
|
|
2098
|
-
)
|
|
2102
|
+
self._log_parameters(sub_params, stats, ismulti=False)
|
|
2099
2103
|
|
|
2100
2104
|
for fn in do_execute_dispatch:
|
|
2101
2105
|
if fn(
|
|
@@ -2176,7 +2180,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
|
|
|
2176
2180
|
|
|
2177
2181
|
if self._echo:
|
|
2178
2182
|
self._log_info(statement)
|
|
2179
|
-
self.
|
|
2183
|
+
self._log_parameters(parameters, "raw sql", ismulti=False)
|
|
2180
2184
|
|
|
2181
2185
|
for fn in (
|
|
2182
2186
|
()
|
sqlalchemy/engine/url.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import Callable
|
|
|
12
12
|
from typing import cast
|
|
13
13
|
from typing import Concatenate
|
|
14
14
|
from typing import Dict
|
|
15
|
+
from typing import FrozenSet
|
|
15
16
|
from typing import Generic
|
|
16
17
|
from typing import Iterable
|
|
17
18
|
from typing import Iterator
|
|
@@ -2164,6 +2165,76 @@ class AsyncSessionTransaction(
|
|
|
2164
2165
|
self._raise_for_not_started()
|
|
2165
2166
|
return self.sync_transaction
|
|
2166
2167
|
|
|
2168
|
+
@property
|
|
2169
|
+
def connections(self) -> FrozenSet[AsyncConnection]:
|
|
2170
|
+
"""The :class:`_asyncio.AsyncConnection` objects this transaction
|
|
2171
|
+
has already acquired.
|
|
2172
|
+
|
|
2173
|
+
This is the asyncio-facing counterpart to
|
|
2174
|
+
:attr:`_orm.SessionTransaction.connections`.
|
|
2175
|
+
|
|
2176
|
+
.. note::
|
|
2177
|
+
|
|
2178
|
+
A savepoint holds only the connections it has used itself.
|
|
2179
|
+
To get all connections in the outer transaction, even while a
|
|
2180
|
+
savepoint is active, use :meth:`.AsyncSession.get_transaction`
|
|
2181
|
+
rather than :meth:`.AsyncSession.get_nested_transaction`::
|
|
2182
|
+
|
|
2183
|
+
root = async_session.get_transaction()
|
|
2184
|
+
connections = root.connections
|
|
2185
|
+
|
|
2186
|
+
.. versionadded:: 2.1.7
|
|
2187
|
+
|
|
2188
|
+
.. seealso::
|
|
2189
|
+
|
|
2190
|
+
:attr:`_orm.SessionTransaction.connections` - main documentation
|
|
2191
|
+
for "connections"
|
|
2192
|
+
|
|
2193
|
+
"""
|
|
2194
|
+
return frozenset(
|
|
2195
|
+
engine.AsyncConnection._retrieve_proxy_for_target(conn)
|
|
2196
|
+
for conn in self._sync_transaction().connections
|
|
2197
|
+
)
|
|
2198
|
+
|
|
2199
|
+
def bound_connection(
|
|
2200
|
+
self,
|
|
2201
|
+
bindkey: Optional[_EntityBindKey[Any]] = None,
|
|
2202
|
+
*,
|
|
2203
|
+
bind: Optional[_AsyncSessionBind] = None,
|
|
2204
|
+
**kw: Any,
|
|
2205
|
+
) -> AsyncConnection:
|
|
2206
|
+
"""Return the :class:`_asyncio.AsyncConnection` this transaction has
|
|
2207
|
+
already acquired for the given bind key.
|
|
2208
|
+
|
|
2209
|
+
This is the asyncio-facing counterpart to
|
|
2210
|
+
:meth:`_orm.SessionTransaction.bound_connection`.
|
|
2211
|
+
:paramref:`.AsyncSessionTransaction.bound_connection.bind` is given
|
|
2212
|
+
as an asyncio object, and is translated on the way in.
|
|
2213
|
+
|
|
2214
|
+
.. note::
|
|
2215
|
+
|
|
2216
|
+
A savepoint can return only a connection it has used itself.
|
|
2217
|
+
To look up a connection in the outer transaction, even while a
|
|
2218
|
+
savepoint is active, use :meth:`.AsyncSession.get_transaction`
|
|
2219
|
+
rather than :meth:`.AsyncSession.get_nested_transaction`::
|
|
2220
|
+
|
|
2221
|
+
root = async_session.get_transaction()
|
|
2222
|
+
connection = root.bound_connection(bindkey)
|
|
2223
|
+
|
|
2224
|
+
.. versionadded:: 2.1.7
|
|
2225
|
+
|
|
2226
|
+
.. seealso::
|
|
2227
|
+
|
|
2228
|
+
:meth:`_orm.SessionTransaction.bound_connection` - main
|
|
2229
|
+
documentation for "bound_connection"
|
|
2230
|
+
|
|
2231
|
+
"""
|
|
2232
|
+
if bind is not None:
|
|
2233
|
+
kw["bind"] = engine._get_sync_engine_or_connection(bind)
|
|
2234
|
+
return engine.AsyncConnection._retrieve_proxy_for_target(
|
|
2235
|
+
self._sync_transaction().bound_connection(bindkey, **kw)
|
|
2236
|
+
)
|
|
2237
|
+
|
|
2167
2238
|
async def rollback(self) -> None:
|
|
2168
2239
|
"""Roll back this :class:`_asyncio.AsyncTransaction`."""
|
|
2169
2240
|
await greenlet_spawn(self._sync_transaction().rollback)
|
sqlalchemy/inspection.py
CHANGED
|
@@ -104,11 +104,11 @@ def inspect(subject: Inspectable[_IN], raiseerr: bool = True) -> _IN: ...
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
@overload
|
|
107
|
-
def inspect(subject: Any, raiseerr: Literal[
|
|
107
|
+
def inspect(subject: Any, raiseerr: Literal[True] = ...) -> Any: ...
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
@overload
|
|
111
|
-
def inspect(subject: Any, raiseerr: bool
|
|
111
|
+
def inspect(subject: Any, raiseerr: bool) -> Optional[Any]: ...
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
def inspect(subject: Any, raiseerr: bool = True) -> Any:
|
sqlalchemy/orm/context.py
CHANGED
|
@@ -14,6 +14,7 @@ from typing import AbstractSet
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
from typing import cast
|
|
16
16
|
from typing import Dict
|
|
17
|
+
from typing import FrozenSet
|
|
17
18
|
from typing import Iterable
|
|
18
19
|
from typing import List
|
|
19
20
|
from typing import Optional
|
|
@@ -84,6 +85,7 @@ if TYPE_CHECKING:
|
|
|
84
85
|
from .query import Query
|
|
85
86
|
from .session import _BindArguments
|
|
86
87
|
from .session import Session
|
|
88
|
+
from .util import LoaderCriteriaOption
|
|
87
89
|
from ..engine import Result
|
|
88
90
|
from ..engine.interfaces import _CoreSingleExecuteParams
|
|
89
91
|
from ..sql._typing import _ColumnsClauseArgument
|
|
@@ -1115,6 +1117,13 @@ class _ORMSelectCompileState(_ORMCompileState, SelectState):
|
|
|
1115
1117
|
_explicit_correlate_froms: AbstractSet[FromClause] = util.EMPTY_SET
|
|
1116
1118
|
_implicit_correlate_froms: AbstractSet[FromClause] = util.EMPTY_SET
|
|
1117
1119
|
|
|
1120
|
+
# with_loader_criteria() options whose criteria contain this SELECT,
|
|
1121
|
+
# either directly or within an enclosing SELECT; see
|
|
1122
|
+
# LoaderCriteriaOption._should_include()
|
|
1123
|
+
_containing_loader_criteria: FrozenSet[LoaderCriteriaOption] = (
|
|
1124
|
+
util.EMPTY_SET
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1118
1127
|
@classmethod
|
|
1119
1128
|
def _create_orm_context(
|
|
1120
1129
|
cls,
|
|
@@ -1138,6 +1147,21 @@ class _ORMSelectCompileState(_ORMCompileState, SelectState):
|
|
|
1138
1147
|
kw.get("lateral", False),
|
|
1139
1148
|
)
|
|
1140
1149
|
|
|
1150
|
+
# the nearest enclosing ORM SELECT has already collected the
|
|
1151
|
+
# options of all the SELECTs enclosing it
|
|
1152
|
+
for entry in reversed(compiler.stack):
|
|
1153
|
+
enclosing_state = entry.get("compile_state")
|
|
1154
|
+
if isinstance(enclosing_state, _ORMSelectCompileState):
|
|
1155
|
+
self._containing_loader_criteria = (
|
|
1156
|
+
enclosing_state._containing_loader_criteria
|
|
1157
|
+
)
|
|
1158
|
+
break
|
|
1159
|
+
|
|
1160
|
+
if "for_loader_criteria" in statement._annotations:
|
|
1161
|
+
self._containing_loader_criteria |= {
|
|
1162
|
+
statement._annotations["for_loader_criteria"]
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1141
1165
|
select_statement = statement
|
|
1142
1166
|
|
|
1143
1167
|
# if we are a select() that was never a legacy Query, we won't
|
sqlalchemy/orm/mapper.py
CHANGED
|
@@ -2754,6 +2754,39 @@ class Mapper(
|
|
|
2754
2754
|
for table, columns in self._cols_by_table.items()
|
|
2755
2755
|
}
|
|
2756
2756
|
|
|
2757
|
+
@HasMemoized.memoized_attribute
|
|
2758
|
+
def _insert_orm_defaults(
|
|
2759
|
+
self,
|
|
2760
|
+
) -> Mapping[FromClause, Sequence[Tuple[str, ColumnElement[Any], Any]]]:
|
|
2761
|
+
"""per table, (propkey, column, value) for ORM-level defaults on
|
|
2762
|
+
column properties, i.e. dataclass defaults, which the column's own
|
|
2763
|
+
default would not deliver to an INSERT.
|
|
2764
|
+
|
|
2765
|
+
Normally the ORM-level default is the same as the column's default.
|
|
2766
|
+
They differ when the column is shared with another mapper, as is the
|
|
2767
|
+
case for use_existing_column on single inheritance subclasses, #13748
|
|
2768
|
+
|
|
2769
|
+
"""
|
|
2770
|
+
return {
|
|
2771
|
+
table: [
|
|
2772
|
+
(prop.key, col, prop._default_scalar_value)
|
|
2773
|
+
for col in columns
|
|
2774
|
+
# None indicates no ORM-level default; a None value would
|
|
2775
|
+
# be omitted from the INSERT in any case
|
|
2776
|
+
if (prop := self._columntoproperty[col])._default_scalar_value
|
|
2777
|
+
is not None
|
|
2778
|
+
# skip if the column's own default already delivers the
|
|
2779
|
+
# same value
|
|
2780
|
+
and not (
|
|
2781
|
+
col.default is not None
|
|
2782
|
+
and col.default.compare_argument(
|
|
2783
|
+
prop._default_scalar_value
|
|
2784
|
+
)
|
|
2785
|
+
)
|
|
2786
|
+
]
|
|
2787
|
+
for table, columns in self._cols_by_table.items()
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2757
2790
|
@HasMemoized.memoized_attribute
|
|
2758
2791
|
def _propkey_to_col(self):
|
|
2759
2792
|
return {
|
sqlalchemy/orm/persistence.py
CHANGED
|
@@ -322,6 +322,19 @@ def _organize_states_for_delete(base_mapper, states, uowtransaction):
|
|
|
322
322
|
yield (state, dict_, mapper, connection, update_version_id)
|
|
323
323
|
|
|
324
324
|
|
|
325
|
+
def _set_insert_value(col, value, params, value_params):
|
|
326
|
+
"""Place a value for an INSERT into either ``value_params``, if it is a
|
|
327
|
+
SQL expression, or ``params``, if it is a plain value.
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
if hasattr(value, "__clause_element__"):
|
|
331
|
+
value_params[col] = value.__clause_element__()
|
|
332
|
+
elif isinstance(value, sql.ClauseElement):
|
|
333
|
+
value_params[col] = value
|
|
334
|
+
else:
|
|
335
|
+
params[col.key] = value
|
|
336
|
+
|
|
337
|
+
|
|
325
338
|
def _collect_insert_commands(
|
|
326
339
|
table,
|
|
327
340
|
states_to_insert,
|
|
@@ -351,19 +364,19 @@ def _collect_insert_commands(
|
|
|
351
364
|
col = propkey_to_col[propkey]
|
|
352
365
|
if value is None and col not in eval_none and not render_nulls:
|
|
353
366
|
continue
|
|
354
|
-
elif
|
|
355
|
-
hasattr(value, "__clause_element__")
|
|
356
|
-
or isinstance(value, sql.ClauseElement)
|
|
357
|
-
):
|
|
358
|
-
value_params[col] = (
|
|
359
|
-
value.__clause_element__()
|
|
360
|
-
if hasattr(value, "__clause_element__")
|
|
361
|
-
else value
|
|
362
|
-
)
|
|
363
|
-
else:
|
|
367
|
+
elif bulk:
|
|
364
368
|
params[col.key] = value
|
|
369
|
+
else:
|
|
370
|
+
_set_insert_value(col, value, params, value_params)
|
|
365
371
|
|
|
366
372
|
if not bulk:
|
|
373
|
+
# ORM-level defaults, i.e. dataclass defaults, for columns that
|
|
374
|
+
# have no value, where the column's own default does not
|
|
375
|
+
# deliver the value, #13748
|
|
376
|
+
for propkey, col, value in mapper._insert_orm_defaults[table]:
|
|
377
|
+
if col.key not in params and col not in value_params:
|
|
378
|
+
_set_insert_value(col, value, params, value_params)
|
|
379
|
+
|
|
367
380
|
# for all the columns that have no default and we don't have
|
|
368
381
|
# a value and where "None" is not a special value, add
|
|
369
382
|
# explicit None to the INSERT. This is a legacy behavior
|
|
@@ -1659,6 +1672,18 @@ def _postfetch(
|
|
|
1659
1672
|
):
|
|
1660
1673
|
prefetch_cols = list(prefetch_cols) + [mapper.version_id_col]
|
|
1661
1674
|
|
|
1675
|
+
if not isupdate:
|
|
1676
|
+
# ORM-level defaults that _collect_insert_commands delivered to the
|
|
1677
|
+
# INSERT for attributes with no value are handled in the same way as
|
|
1678
|
+
# prefetched column defaults, #13748
|
|
1679
|
+
orm_default_cols = [
|
|
1680
|
+
col
|
|
1681
|
+
for pkey, col, _ in mapper._insert_orm_defaults[table]
|
|
1682
|
+
if dict_.get(pkey) is None and params.get(col.key) is not None
|
|
1683
|
+
]
|
|
1684
|
+
if orm_default_cols:
|
|
1685
|
+
prefetch_cols = list(prefetch_cols) + orm_default_cols
|
|
1686
|
+
|
|
1662
1687
|
refresh_flush = bool(mapper.class_manager.dispatch.refresh_flush)
|
|
1663
1688
|
if refresh_flush:
|
|
1664
1689
|
load_evt_attrs = []
|
sqlalchemy/orm/query.py
CHANGED
|
@@ -1307,7 +1307,7 @@ class Query(
|
|
|
1307
1307
|
for prop in mapper.iterate_properties:
|
|
1308
1308
|
if (
|
|
1309
1309
|
isinstance(prop, relationships.RelationshipProperty)
|
|
1310
|
-
and prop.mapper is entity_zero.mapper
|
|
1310
|
+
and prop.mapper is entity_zero.mapper
|
|
1311
1311
|
):
|
|
1312
1312
|
property = prop # type: ignore[assignment] # noqa: A001
|
|
1313
1313
|
break
|
|
@@ -1316,7 +1316,7 @@ class Query(
|
|
|
1316
1316
|
"Could not locate a property which relates instances "
|
|
1317
1317
|
"of class '%s' to instances of class '%s'"
|
|
1318
1318
|
% (
|
|
1319
|
-
entity_zero.mapper.class_.__name__,
|
|
1319
|
+
entity_zero.mapper.class_.__name__,
|
|
1320
1320
|
instance.__class__.__name__,
|
|
1321
1321
|
)
|
|
1322
1322
|
)
|
|
@@ -1325,7 +1325,7 @@ class Query(
|
|
|
1325
1325
|
with_parent(
|
|
1326
1326
|
instance,
|
|
1327
1327
|
property, # type: ignore[arg-type]
|
|
1328
|
-
entity_zero.entity,
|
|
1328
|
+
entity_zero.entity,
|
|
1329
1329
|
)
|
|
1330
1330
|
)
|
|
1331
1331
|
|
sqlalchemy/orm/relationships.py
CHANGED
|
@@ -667,7 +667,7 @@ class RelationshipProperty(
|
|
|
667
667
|
|
|
668
668
|
def _memoized_attr_entity(self) -> _InternalEntityType[_PT]:
|
|
669
669
|
if self._of_type:
|
|
670
|
-
return inspect(self._of_type) # type: ignore[return
|
|
670
|
+
return inspect(self._of_type) # type: ignore[no-any-return]
|
|
671
671
|
else:
|
|
672
672
|
return self.prop.entity
|
|
673
673
|
|
|
@@ -1914,22 +1914,38 @@ class RelationshipProperty(
|
|
|
1914
1914
|
self._attribute_options.dataclasses_default_factory
|
|
1915
1915
|
is not _NoArg.NO_ARG
|
|
1916
1916
|
):
|
|
1917
|
+
default_factory = (
|
|
1918
|
+
self._attribute_options.dataclasses_default_factory
|
|
1919
|
+
)
|
|
1920
|
+
|
|
1917
1921
|
# write only / dynamic relationships have no collection_class,
|
|
1918
1922
|
# however they accept a list of objects when assigned, so
|
|
1919
1923
|
# ``list`` is the expected default_factory for these
|
|
1920
|
-
|
|
1924
|
+
exact_default_factory = (
|
|
1921
1925
|
list if is_write_only or is_dynamic else self.collection_class
|
|
1922
1926
|
)
|
|
1923
1927
|
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1928
|
+
# the plain list / set / dict that a custom collection class
|
|
1929
|
+
# acts as is also accepted, such as ``dict`` for
|
|
1930
|
+
# attribute_keyed_dict()
|
|
1931
|
+
plain_collection_type = util.duck_type_collection(
|
|
1932
|
+
exact_default_factory
|
|
1933
|
+
)
|
|
1934
|
+
if plain_collection_type is exact_default_factory:
|
|
1935
|
+
plain_collection_type = None
|
|
1936
|
+
|
|
1937
|
+
if default_factory is not exact_default_factory and (
|
|
1938
|
+
plain_collection_type is None
|
|
1939
|
+
or default_factory is not plain_collection_type
|
|
1927
1940
|
):
|
|
1941
|
+
expected = str(exact_default_factory)
|
|
1942
|
+
if plain_collection_type is not None:
|
|
1943
|
+
expected += f" or {plain_collection_type}"
|
|
1928
1944
|
raise sa_exc.ArgumentError(
|
|
1929
1945
|
"For relationship "
|
|
1930
1946
|
f"{self._format_as_string(cls, key)} using "
|
|
1931
1947
|
"dataclass options, default_factory must be exactly "
|
|
1932
|
-
f"{
|
|
1948
|
+
f"{expected}"
|
|
1933
1949
|
)
|
|
1934
1950
|
|
|
1935
1951
|
@util.preload_module("sqlalchemy.orm.mapper")
|