SQLAlchemy 2.1.0rc1__cp315-cp315-win_amd64.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 (276) hide show
  1. sqlalchemy/__init__.py +299 -0
  2. sqlalchemy/connectors/__init__.py +18 -0
  3. sqlalchemy/connectors/aioodbc.py +171 -0
  4. sqlalchemy/connectors/asyncio.py +476 -0
  5. sqlalchemy/connectors/pyodbc.py +248 -0
  6. sqlalchemy/dialects/__init__.py +62 -0
  7. sqlalchemy/dialects/_typing.py +29 -0
  8. sqlalchemy/dialects/mssql/__init__.py +88 -0
  9. sqlalchemy/dialects/mssql/aioodbc.py +63 -0
  10. sqlalchemy/dialects/mssql/base.py +4833 -0
  11. sqlalchemy/dialects/mssql/information_schema.py +345 -0
  12. sqlalchemy/dialects/mssql/json.py +140 -0
  13. sqlalchemy/dialects/mssql/mssqlpython.py +242 -0
  14. sqlalchemy/dialects/mssql/provision.py +196 -0
  15. sqlalchemy/dialects/mssql/pymssql.py +130 -0
  16. sqlalchemy/dialects/mssql/pyodbc.py +697 -0
  17. sqlalchemy/dialects/mysql/__init__.py +106 -0
  18. sqlalchemy/dialects/mysql/_mariadb_shim.py +312 -0
  19. sqlalchemy/dialects/mysql/aiomysql.py +260 -0
  20. sqlalchemy/dialects/mysql/asyncmy.py +241 -0
  21. sqlalchemy/dialects/mysql/base.py +3896 -0
  22. sqlalchemy/dialects/mysql/cymysql.py +107 -0
  23. sqlalchemy/dialects/mysql/dml.py +279 -0
  24. sqlalchemy/dialects/mysql/enumerated.py +277 -0
  25. sqlalchemy/dialects/mysql/expression.py +146 -0
  26. sqlalchemy/dialects/mysql/json.py +92 -0
  27. sqlalchemy/dialects/mysql/mariadb.py +67 -0
  28. sqlalchemy/dialects/mysql/mariadbconnector.py +314 -0
  29. sqlalchemy/dialects/mysql/mysqlconnector.py +291 -0
  30. sqlalchemy/dialects/mysql/mysqldb.py +318 -0
  31. sqlalchemy/dialects/mysql/provision.py +153 -0
  32. sqlalchemy/dialects/mysql/pymysql.py +188 -0
  33. sqlalchemy/dialects/mysql/pyodbc.py +157 -0
  34. sqlalchemy/dialects/mysql/reflection.py +724 -0
  35. sqlalchemy/dialects/mysql/reserved_words.py +570 -0
  36. sqlalchemy/dialects/mysql/types.py +845 -0
  37. sqlalchemy/dialects/oracle/__init__.py +85 -0
  38. sqlalchemy/dialects/oracle/base.py +3847 -0
  39. sqlalchemy/dialects/oracle/cx_oracle.py +1736 -0
  40. sqlalchemy/dialects/oracle/dictionary.py +507 -0
  41. sqlalchemy/dialects/oracle/json.py +157 -0
  42. sqlalchemy/dialects/oracle/oracledb.py +898 -0
  43. sqlalchemy/dialects/oracle/provision.py +288 -0
  44. sqlalchemy/dialects/oracle/types.py +367 -0
  45. sqlalchemy/dialects/oracle/vector.py +366 -0
  46. sqlalchemy/dialects/postgresql/__init__.py +170 -0
  47. sqlalchemy/dialects/postgresql/_psycopg_common.py +232 -0
  48. sqlalchemy/dialects/postgresql/array.py +534 -0
  49. sqlalchemy/dialects/postgresql/asyncpg.py +1318 -0
  50. sqlalchemy/dialects/postgresql/base.py +5935 -0
  51. sqlalchemy/dialects/postgresql/bitstring.py +327 -0
  52. sqlalchemy/dialects/postgresql/dml.py +360 -0
  53. sqlalchemy/dialects/postgresql/ext.py +599 -0
  54. sqlalchemy/dialects/postgresql/hstore.py +422 -0
  55. sqlalchemy/dialects/postgresql/json.py +411 -0
  56. sqlalchemy/dialects/postgresql/named_types.py +535 -0
  57. sqlalchemy/dialects/postgresql/operators.py +129 -0
  58. sqlalchemy/dialects/postgresql/pg8000.py +655 -0
  59. sqlalchemy/dialects/postgresql/pg_catalog.py +345 -0
  60. sqlalchemy/dialects/postgresql/provision.py +202 -0
  61. sqlalchemy/dialects/postgresql/psycopg.py +800 -0
  62. sqlalchemy/dialects/postgresql/psycopg2.py +860 -0
  63. sqlalchemy/dialects/postgresql/psycopg2cffi.py +62 -0
  64. sqlalchemy/dialects/postgresql/ranges.py +1002 -0
  65. sqlalchemy/dialects/postgresql/types.py +388 -0
  66. sqlalchemy/dialects/sqlite/__init__.py +59 -0
  67. sqlalchemy/dialects/sqlite/aiosqlite.py +375 -0
  68. sqlalchemy/dialects/sqlite/base.py +3103 -0
  69. sqlalchemy/dialects/sqlite/dml.py +314 -0
  70. sqlalchemy/dialects/sqlite/json.py +134 -0
  71. sqlalchemy/dialects/sqlite/provision.py +237 -0
  72. sqlalchemy/dialects/sqlite/pysqlcipher.py +166 -0
  73. sqlalchemy/dialects/sqlite/pysqlite.py +959 -0
  74. sqlalchemy/dialects/type_migration_guidelines.txt +145 -0
  75. sqlalchemy/engine/__init__.py +62 -0
  76. sqlalchemy/engine/_processors_cy.cp315-win_amd64.pyd +0 -0
  77. sqlalchemy/engine/_processors_cy.py +92 -0
  78. sqlalchemy/engine/_result_cy.cp315-win_amd64.pyd +0 -0
  79. sqlalchemy/engine/_result_cy.py +711 -0
  80. sqlalchemy/engine/_row_cy.cp315-win_amd64.pyd +0 -0
  81. sqlalchemy/engine/_row_cy.py +232 -0
  82. sqlalchemy/engine/_util_cy.cp315-win_amd64.pyd +0 -0
  83. sqlalchemy/engine/_util_cy.py +136 -0
  84. sqlalchemy/engine/base.py +3357 -0
  85. sqlalchemy/engine/characteristics.py +155 -0
  86. sqlalchemy/engine/create.py +877 -0
  87. sqlalchemy/engine/cursor.py +2425 -0
  88. sqlalchemy/engine/default.py +2627 -0
  89. sqlalchemy/engine/events.py +965 -0
  90. sqlalchemy/engine/interfaces.py +3636 -0
  91. sqlalchemy/engine/mock.py +133 -0
  92. sqlalchemy/engine/processors.py +83 -0
  93. sqlalchemy/engine/reflection.py +2141 -0
  94. sqlalchemy/engine/result.py +2012 -0
  95. sqlalchemy/engine/row.py +397 -0
  96. sqlalchemy/engine/strategies.py +16 -0
  97. sqlalchemy/engine/url.py +922 -0
  98. sqlalchemy/engine/util.py +164 -0
  99. sqlalchemy/event/__init__.py +26 -0
  100. sqlalchemy/event/api.py +220 -0
  101. sqlalchemy/event/attr.py +675 -0
  102. sqlalchemy/event/base.py +473 -0
  103. sqlalchemy/event/legacy.py +259 -0
  104. sqlalchemy/event/registry.py +391 -0
  105. sqlalchemy/events.py +17 -0
  106. sqlalchemy/exc.py +939 -0
  107. sqlalchemy/ext/__init__.py +10 -0
  108. sqlalchemy/ext/associationproxy.py +2073 -0
  109. sqlalchemy/ext/asyncio/__init__.py +29 -0
  110. sqlalchemy/ext/asyncio/base.py +281 -0
  111. sqlalchemy/ext/asyncio/engine.py +1487 -0
  112. sqlalchemy/ext/asyncio/exc.py +21 -0
  113. sqlalchemy/ext/asyncio/result.py +994 -0
  114. sqlalchemy/ext/asyncio/scoping.py +1679 -0
  115. sqlalchemy/ext/asyncio/session.py +2006 -0
  116. sqlalchemy/ext/automap.py +1702 -0
  117. sqlalchemy/ext/baked.py +558 -0
  118. sqlalchemy/ext/compiler.py +601 -0
  119. sqlalchemy/ext/declarative/__init__.py +65 -0
  120. sqlalchemy/ext/declarative/extensions.py +561 -0
  121. sqlalchemy/ext/horizontal_shard.py +481 -0
  122. sqlalchemy/ext/hybrid.py +1877 -0
  123. sqlalchemy/ext/indexable.py +364 -0
  124. sqlalchemy/ext/instrumentation.py +450 -0
  125. sqlalchemy/ext/mutable.py +1081 -0
  126. sqlalchemy/ext/orderinglist.py +440 -0
  127. sqlalchemy/ext/serializer.py +184 -0
  128. sqlalchemy/future/__init__.py +17 -0
  129. sqlalchemy/future/engine.py +15 -0
  130. sqlalchemy/inspection.py +188 -0
  131. sqlalchemy/log.py +279 -0
  132. sqlalchemy/orm/__init__.py +176 -0
  133. sqlalchemy/orm/_orm_constructors.py +2694 -0
  134. sqlalchemy/orm/_typing.py +180 -0
  135. sqlalchemy/orm/attributes.py +2868 -0
  136. sqlalchemy/orm/base.py +991 -0
  137. sqlalchemy/orm/bulk_persistence.py +2168 -0
  138. sqlalchemy/orm/clsregistry.py +630 -0
  139. sqlalchemy/orm/collections.py +1569 -0
  140. sqlalchemy/orm/context.py +3475 -0
  141. sqlalchemy/orm/decl_api.py +2283 -0
  142. sqlalchemy/orm/decl_base.py +2320 -0
  143. sqlalchemy/orm/dependency.py +1306 -0
  144. sqlalchemy/orm/descriptor_props.py +1183 -0
  145. sqlalchemy/orm/dynamic.py +306 -0
  146. sqlalchemy/orm/evaluator.py +378 -0
  147. sqlalchemy/orm/events.py +3387 -0
  148. sqlalchemy/orm/exc.py +237 -0
  149. sqlalchemy/orm/identity.py +302 -0
  150. sqlalchemy/orm/instrumentation.py +749 -0
  151. sqlalchemy/orm/interfaces.py +1595 -0
  152. sqlalchemy/orm/loading.py +1712 -0
  153. sqlalchemy/orm/mapped_collection.py +557 -0
  154. sqlalchemy/orm/mapper.py +4465 -0
  155. sqlalchemy/orm/path_registry.py +907 -0
  156. sqlalchemy/orm/persistence.py +1790 -0
  157. sqlalchemy/orm/properties.py +972 -0
  158. sqlalchemy/orm/query.py +3528 -0
  159. sqlalchemy/orm/relationships.py +3608 -0
  160. sqlalchemy/orm/scoping.py +2233 -0
  161. sqlalchemy/orm/session.py +5468 -0
  162. sqlalchemy/orm/state.py +1175 -0
  163. sqlalchemy/orm/state_changes.py +196 -0
  164. sqlalchemy/orm/strategies.py +3552 -0
  165. sqlalchemy/orm/strategy_options.py +2648 -0
  166. sqlalchemy/orm/sync.py +164 -0
  167. sqlalchemy/orm/unitofwork.py +797 -0
  168. sqlalchemy/orm/util.py +2461 -0
  169. sqlalchemy/orm/writeonly.py +701 -0
  170. sqlalchemy/pool/__init__.py +41 -0
  171. sqlalchemy/pool/base.py +1540 -0
  172. sqlalchemy/pool/events.py +375 -0
  173. sqlalchemy/pool/impl.py +583 -0
  174. sqlalchemy/py.typed +0 -0
  175. sqlalchemy/schema.py +75 -0
  176. sqlalchemy/sql/__init__.py +156 -0
  177. sqlalchemy/sql/_annotated_cols.py +402 -0
  178. sqlalchemy/sql/_cache_key_cy.cp315-win_amd64.pyd +0 -0
  179. sqlalchemy/sql/_cache_key_cy.py +363 -0
  180. sqlalchemy/sql/_dml_constructors.py +132 -0
  181. sqlalchemy/sql/_elements_constructors.py +2190 -0
  182. sqlalchemy/sql/_orm_types.py +19 -0
  183. sqlalchemy/sql/_selectable_constructors.py +840 -0
  184. sqlalchemy/sql/_typing.py +500 -0
  185. sqlalchemy/sql/_util_cy.cp315-win_amd64.pyd +0 -0
  186. sqlalchemy/sql/_util_cy.pxd +11 -0
  187. sqlalchemy/sql/_util_cy.py +127 -0
  188. sqlalchemy/sql/annotation.py +590 -0
  189. sqlalchemy/sql/base.py +2702 -0
  190. sqlalchemy/sql/cache_key.py +915 -0
  191. sqlalchemy/sql/coercions.py +1373 -0
  192. sqlalchemy/sql/compiler.py +8453 -0
  193. sqlalchemy/sql/crud.py +1816 -0
  194. sqlalchemy/sql/ddl.py +1962 -0
  195. sqlalchemy/sql/default_comparator.py +660 -0
  196. sqlalchemy/sql/dml.py +2018 -0
  197. sqlalchemy/sql/elements.py +6057 -0
  198. sqlalchemy/sql/events.py +458 -0
  199. sqlalchemy/sql/expression.py +171 -0
  200. sqlalchemy/sql/functions.py +2380 -0
  201. sqlalchemy/sql/lambdas.py +1442 -0
  202. sqlalchemy/sql/naming.py +204 -0
  203. sqlalchemy/sql/operators.py +2909 -0
  204. sqlalchemy/sql/roles.py +332 -0
  205. sqlalchemy/sql/schema.py +7075 -0
  206. sqlalchemy/sql/selectable.py +7634 -0
  207. sqlalchemy/sql/sqltypes.py +4130 -0
  208. sqlalchemy/sql/traversals.py +1041 -0
  209. sqlalchemy/sql/type_api.py +2450 -0
  210. sqlalchemy/sql/util.py +1496 -0
  211. sqlalchemy/sql/visitors.py +1153 -0
  212. sqlalchemy/testing/__init__.py +97 -0
  213. sqlalchemy/testing/assertions.py +1007 -0
  214. sqlalchemy/testing/assertsql.py +519 -0
  215. sqlalchemy/testing/asyncio.py +128 -0
  216. sqlalchemy/testing/cancellation.py +237 -0
  217. sqlalchemy/testing/config.py +440 -0
  218. sqlalchemy/testing/engines.py +482 -0
  219. sqlalchemy/testing/entities.py +117 -0
  220. sqlalchemy/testing/exclusions.py +501 -0
  221. sqlalchemy/testing/fixtures/__init__.py +30 -0
  222. sqlalchemy/testing/fixtures/base.py +426 -0
  223. sqlalchemy/testing/fixtures/mypy.py +247 -0
  224. sqlalchemy/testing/fixtures/orm.py +227 -0
  225. sqlalchemy/testing/fixtures/sql.py +538 -0
  226. sqlalchemy/testing/pickleable.py +155 -0
  227. sqlalchemy/testing/plugin/__init__.py +6 -0
  228. sqlalchemy/testing/plugin/bootstrap.py +50 -0
  229. sqlalchemy/testing/plugin/plugin_base.py +828 -0
  230. sqlalchemy/testing/plugin/pytestplugin.py +896 -0
  231. sqlalchemy/testing/profiles_file.py +350 -0
  232. sqlalchemy/testing/profiling.py +294 -0
  233. sqlalchemy/testing/provision.py +633 -0
  234. sqlalchemy/testing/requirements.py +1971 -0
  235. sqlalchemy/testing/schema.py +198 -0
  236. sqlalchemy/testing/suite/__init__.py +19 -0
  237. sqlalchemy/testing/suite/test_cte.py +237 -0
  238. sqlalchemy/testing/suite/test_ddl.py +420 -0
  239. sqlalchemy/testing/suite/test_dialect.py +776 -0
  240. sqlalchemy/testing/suite/test_insert.py +630 -0
  241. sqlalchemy/testing/suite/test_reflection.py +3815 -0
  242. sqlalchemy/testing/suite/test_results.py +660 -0
  243. sqlalchemy/testing/suite/test_rowcount.py +258 -0
  244. sqlalchemy/testing/suite/test_select.py +2112 -0
  245. sqlalchemy/testing/suite/test_sequence.py +317 -0
  246. sqlalchemy/testing/suite/test_table_via_select.py +686 -0
  247. sqlalchemy/testing/suite/test_types.py +2271 -0
  248. sqlalchemy/testing/suite/test_unicode_ddl.py +189 -0
  249. sqlalchemy/testing/suite/test_update_delete.py +139 -0
  250. sqlalchemy/testing/util.py +575 -0
  251. sqlalchemy/testing/warnings.py +52 -0
  252. sqlalchemy/types.py +75 -0
  253. sqlalchemy/util/__init__.py +165 -0
  254. sqlalchemy/util/_collections.py +688 -0
  255. sqlalchemy/util/_collections_cy.cp315-win_amd64.pyd +0 -0
  256. sqlalchemy/util/_collections_cy.pxd +8 -0
  257. sqlalchemy/util/_collections_cy.py +516 -0
  258. sqlalchemy/util/_has_cython.py +48 -0
  259. sqlalchemy/util/_immutabledict_cy.cp315-win_amd64.pyd +0 -0
  260. sqlalchemy/util/_immutabledict_cy.py +240 -0
  261. sqlalchemy/util/compat.py +298 -0
  262. sqlalchemy/util/concurrency.py +272 -0
  263. sqlalchemy/util/cython.py +95 -0
  264. sqlalchemy/util/deprecations.py +401 -0
  265. sqlalchemy/util/langhelpers.py +2797 -0
  266. sqlalchemy/util/preloaded.py +153 -0
  267. sqlalchemy/util/queue.py +304 -0
  268. sqlalchemy/util/tool_support.py +202 -0
  269. sqlalchemy/util/topological.py +120 -0
  270. sqlalchemy/util/typing.py +709 -0
  271. sqlalchemy-2.1.0rc1.dist-info/METADATA +270 -0
  272. sqlalchemy-2.1.0rc1.dist-info/RECORD +276 -0
  273. sqlalchemy-2.1.0rc1.dist-info/WHEEL +5 -0
  274. sqlalchemy-2.1.0rc1.dist-info/licenses/AUTHORS +30 -0
  275. sqlalchemy-2.1.0rc1.dist-info/licenses/LICENSE +19 -0
  276. sqlalchemy-2.1.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2168 @@
1
+ # orm/bulk_persistence.py
2
+ # Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3
+ # <see AUTHORS file>
4
+ #
5
+ # This module is part of SQLAlchemy and is released under
6
+ # the MIT License: https://www.opensource.org/licenses/mit-license.php
7
+ # mypy: ignore-errors
8
+
9
+
10
+ """additional ORM persistence classes related to "bulk" operations,
11
+ specifically outside of the flush() process.
12
+
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any
18
+ from typing import cast
19
+ from typing import Dict
20
+ from typing import Iterable
21
+ from typing import Literal
22
+ from typing import Optional
23
+ from typing import overload
24
+ from typing import TYPE_CHECKING
25
+ from typing import TypeVar
26
+ from typing import Union
27
+
28
+ from . import attributes
29
+ from . import context
30
+ from . import evaluator
31
+ from . import exc as orm_exc
32
+ from . import loading
33
+ from . import persistence
34
+ from .base import NO_VALUE
35
+ from .context import _AbstractORMCompileState
36
+ from .context import _ORMFromStatementCompileState
37
+ from .context import FromStatement
38
+ from .context import QueryContext
39
+ from .interfaces import PropComparator
40
+ from .. import exc as sa_exc
41
+ from .. import util
42
+ from ..engine import Dialect
43
+ from ..engine import result as _result
44
+ from ..sql import coercions
45
+ from ..sql import dml
46
+ from ..sql import expression
47
+ from ..sql import roles
48
+ from ..sql import select
49
+ from ..sql import sqltypes
50
+ from ..sql.base import _entity_namespace_key
51
+ from ..sql.base import CompileState
52
+ from ..sql.base import Options
53
+ from ..sql.dml import DeleteDMLState
54
+ from ..sql.dml import InsertDMLState
55
+ from ..sql.dml import UpdateDMLState
56
+ from ..util import EMPTY_DICT
57
+ from ..util.typing import TupleAny
58
+ from ..util.typing import Unpack
59
+
60
+ if TYPE_CHECKING:
61
+ from ._typing import DMLStrategyArgument
62
+ from ._typing import OrmExecuteOptionsParameter
63
+ from ._typing import SynchronizeSessionArgument
64
+ from .mapper import Mapper
65
+ from .session import _BindArguments
66
+ from .session import ORMExecuteState
67
+ from .session import Session
68
+ from .session import SessionTransaction
69
+ from .state import InstanceState
70
+ from ..engine import Connection
71
+ from ..engine import cursor
72
+ from ..engine.interfaces import _CoreAnyExecuteParams
73
+
74
+ _O = TypeVar("_O", bound=object)
75
+
76
+
77
+ @overload
78
+ def _bulk_insert(
79
+ mapper: Mapper[_O],
80
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
81
+ session_transaction: SessionTransaction,
82
+ *,
83
+ isstates: bool,
84
+ return_defaults: bool,
85
+ render_nulls: bool,
86
+ use_orm_insert_stmt: Literal[None] = ...,
87
+ execution_options: Optional[OrmExecuteOptionsParameter] = ...,
88
+ ) -> None: ...
89
+
90
+
91
+ @overload
92
+ def _bulk_insert(
93
+ mapper: Mapper[_O],
94
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
95
+ session_transaction: SessionTransaction,
96
+ *,
97
+ isstates: bool,
98
+ return_defaults: bool,
99
+ render_nulls: bool,
100
+ use_orm_insert_stmt: Optional[dml.Insert] = ...,
101
+ execution_options: Optional[OrmExecuteOptionsParameter] = ...,
102
+ ) -> cursor.CursorResult[Any]: ...
103
+
104
+
105
+ def _bulk_insert(
106
+ mapper: Mapper[_O],
107
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
108
+ session_transaction: SessionTransaction,
109
+ *,
110
+ isstates: bool,
111
+ return_defaults: bool,
112
+ render_nulls: bool,
113
+ use_orm_insert_stmt: Optional[dml.Insert] = None,
114
+ execution_options: Optional[OrmExecuteOptionsParameter] = None,
115
+ ) -> Optional[cursor.CursorResult[Any]]:
116
+ base_mapper = mapper.base_mapper
117
+
118
+ if session_transaction.session.connection_callable:
119
+ raise NotImplementedError(
120
+ "connection_callable / per-instance sharding "
121
+ "not supported in bulk_insert()"
122
+ )
123
+
124
+ if isstates:
125
+ if TYPE_CHECKING:
126
+ mappings = cast(Iterable[InstanceState[_O]], mappings)
127
+
128
+ if return_defaults:
129
+ # list of states allows us to attach .key for return_defaults case
130
+ states = [(state, state.dict) for state in mappings]
131
+ mappings = [dict_ for (state, dict_) in states]
132
+ else:
133
+ mappings = [state.dict for state in mappings]
134
+ else:
135
+ if TYPE_CHECKING:
136
+ mappings = cast(Iterable[Dict[str, Any]], mappings)
137
+
138
+ if return_defaults:
139
+ # use dictionaries given, so that newly populated defaults
140
+ # can be delivered back to the caller (see #11661). This is **not**
141
+ # compatible with other use cases such as a session-executed
142
+ # insert() construct, as this will confuse the case of
143
+ # insert-per-subclass for joined inheritance cases (see
144
+ # test_bulk_statements.py::BulkDMLReturningJoinedInhTest).
145
+ #
146
+ # So in this conditional, we have **only** called
147
+ # session.bulk_insert_mappings() which does not have this
148
+ # requirement
149
+ mappings = list(mappings)
150
+ else:
151
+ # for all other cases we need to establish a local dictionary
152
+ # so that the incoming dictionaries aren't mutated
153
+ mappings = [dict(m) for m in mappings]
154
+ _expand_other_attrs(mapper, mappings)
155
+
156
+ connection = session_transaction.connection(base_mapper)
157
+
158
+ return_result: Optional[cursor.CursorResult[Any]] = None
159
+
160
+ mappers_to_run = [
161
+ (table, mp)
162
+ for table, mp in base_mapper._sorted_tables.items()
163
+ if table in mapper._pks_by_table
164
+ ]
165
+
166
+ if return_defaults:
167
+ # not used by new-style bulk inserts, only used for legacy
168
+ bookkeeping = True
169
+ elif len(mappers_to_run) > 1:
170
+ # if we have more than one table, mapper to run where we will be
171
+ # either horizontally splicing, or copying values between tables,
172
+ # we need the "bookkeeping" / deterministic returning order
173
+ bookkeeping = True
174
+ else:
175
+ bookkeeping = False
176
+
177
+ for table, super_mapper in mappers_to_run:
178
+ # find bindparams in the statement. For bulk, we don't really know if
179
+ # a key in the params applies to a different table since we are
180
+ # potentially inserting for multiple tables here; looking at the
181
+ # bindparam() is a lot more direct. in most cases this will
182
+ # use _generate_cache_key() which is memoized, although in practice
183
+ # the ultimate statement that's executed is probably not the same
184
+ # object so that memoization might not matter much.
185
+ extra_bp_names = (
186
+ [
187
+ b.key
188
+ for b in use_orm_insert_stmt._get_embedded_bindparams()
189
+ if b.key in mappings[0]
190
+ ]
191
+ if use_orm_insert_stmt is not None
192
+ else ()
193
+ )
194
+
195
+ records = (
196
+ (
197
+ None,
198
+ state_dict,
199
+ params,
200
+ mapper,
201
+ connection,
202
+ value_params,
203
+ has_all_pks,
204
+ has_all_defaults,
205
+ )
206
+ for (
207
+ state,
208
+ state_dict,
209
+ params,
210
+ mp,
211
+ conn,
212
+ value_params,
213
+ has_all_pks,
214
+ has_all_defaults,
215
+ ) in persistence._collect_insert_commands(
216
+ table,
217
+ ((None, mapping, mapper, connection) for mapping in mappings),
218
+ bulk=True,
219
+ return_defaults=bookkeeping,
220
+ render_nulls=render_nulls,
221
+ include_bulk_keys=extra_bp_names,
222
+ )
223
+ )
224
+
225
+ result = persistence._emit_insert_statements(
226
+ base_mapper,
227
+ None,
228
+ super_mapper,
229
+ table,
230
+ records,
231
+ bookkeeping=bookkeeping,
232
+ use_orm_insert_stmt=use_orm_insert_stmt,
233
+ execution_options=execution_options,
234
+ )
235
+ if use_orm_insert_stmt is not None:
236
+ if not use_orm_insert_stmt._returning or return_result is None:
237
+ return_result = result
238
+ elif result.returns_rows:
239
+ assert bookkeeping
240
+ return_result = return_result.splice_horizontally(result)
241
+
242
+ if return_defaults and isstates:
243
+ identity_cls = mapper._identity_class
244
+ identity_props = [p.key for p in mapper._identity_key_props]
245
+ for state, dict_ in states:
246
+ state.key = (
247
+ identity_cls,
248
+ tuple([dict_[key] for key in identity_props]),
249
+ None,
250
+ )
251
+
252
+ if use_orm_insert_stmt is not None:
253
+ assert return_result is not None
254
+ return return_result
255
+
256
+
257
+ @overload
258
+ def _bulk_update(
259
+ mapper: Mapper[Any],
260
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
261
+ session_transaction: SessionTransaction,
262
+ *,
263
+ isstates: bool,
264
+ update_changed_only: bool,
265
+ use_orm_update_stmt: Literal[None] = ...,
266
+ enable_check_rowcount: bool = True,
267
+ ) -> None: ...
268
+
269
+
270
+ @overload
271
+ def _bulk_update(
272
+ mapper: Mapper[Any],
273
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
274
+ session_transaction: SessionTransaction,
275
+ *,
276
+ isstates: bool,
277
+ update_changed_only: bool,
278
+ use_orm_update_stmt: Optional[dml.Update] = ...,
279
+ enable_check_rowcount: bool = True,
280
+ ) -> _result.Result[Unpack[TupleAny]]: ...
281
+
282
+
283
+ def _bulk_update(
284
+ mapper: Mapper[Any],
285
+ mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
286
+ session_transaction: SessionTransaction,
287
+ *,
288
+ isstates: bool,
289
+ update_changed_only: bool,
290
+ use_orm_update_stmt: Optional[dml.Update] = None,
291
+ enable_check_rowcount: bool = True,
292
+ ) -> Optional[_result.Result[Unpack[TupleAny]]]:
293
+ base_mapper = mapper.base_mapper
294
+
295
+ search_keys = mapper._primary_key_propkeys
296
+ if mapper._version_id_prop:
297
+ search_keys = {mapper._version_id_prop.key}.union(search_keys)
298
+
299
+ def _changed_dict(mapper, state):
300
+ return {
301
+ k: v
302
+ for k, v in state.dict.items()
303
+ if k in state.committed_state or k in search_keys
304
+ }
305
+
306
+ if isstates:
307
+ if update_changed_only:
308
+ mappings = [_changed_dict(mapper, state) for state in mappings]
309
+ else:
310
+ mappings = [state.dict for state in mappings]
311
+ else:
312
+ mappings = [dict(m) for m in mappings]
313
+ _expand_other_attrs(mapper, mappings)
314
+
315
+ if session_transaction.session.connection_callable:
316
+ raise NotImplementedError(
317
+ "connection_callable / per-instance sharding "
318
+ "not supported in bulk_update()"
319
+ )
320
+
321
+ connection = session_transaction.connection(base_mapper)
322
+
323
+ # find bindparams in the statement. see _bulk_insert for similar
324
+ # notes for the insert case
325
+ extra_bp_names = (
326
+ [
327
+ b.key
328
+ for b in use_orm_update_stmt._get_embedded_bindparams()
329
+ if b.key in mappings[0]
330
+ ]
331
+ if use_orm_update_stmt is not None
332
+ else ()
333
+ )
334
+
335
+ for table, super_mapper in base_mapper._sorted_tables.items():
336
+ if not mapper.isa(super_mapper) or table not in mapper._pks_by_table:
337
+ continue
338
+
339
+ records = persistence._collect_update_commands(
340
+ None,
341
+ table,
342
+ (
343
+ (
344
+ None,
345
+ mapping,
346
+ mapper,
347
+ connection,
348
+ (
349
+ mapping[mapper._version_id_prop.key]
350
+ if mapper._version_id_prop
351
+ else None
352
+ ),
353
+ )
354
+ for mapping in mappings
355
+ ),
356
+ bulk=True,
357
+ use_orm_update_stmt=use_orm_update_stmt,
358
+ include_bulk_keys=extra_bp_names,
359
+ )
360
+ persistence._emit_update_statements(
361
+ base_mapper,
362
+ None,
363
+ super_mapper,
364
+ table,
365
+ records,
366
+ bookkeeping=False,
367
+ use_orm_update_stmt=use_orm_update_stmt,
368
+ enable_check_rowcount=enable_check_rowcount,
369
+ )
370
+
371
+ if use_orm_update_stmt is not None:
372
+ return _result.null_result()
373
+
374
+
375
+ def _expand_other_attrs(
376
+ mapper: Mapper[Any], mappings: Iterable[Dict[str, Any]]
377
+ ) -> None:
378
+ all_attrs = mapper.all_orm_descriptors
379
+
380
+ attr_keys = set(all_attrs.keys())
381
+
382
+ bulk_dml_setters = {
383
+ key: setter
384
+ for key, setter in (
385
+ (key, attr._bulk_dml_setter(key))
386
+ for key, attr in (
387
+ (key, _entity_namespace_key(mapper, key, default=NO_VALUE))
388
+ for key in attr_keys
389
+ )
390
+ if attr is not NO_VALUE and isinstance(attr, PropComparator)
391
+ )
392
+ if setter is not None
393
+ }
394
+ setters_todo = set(bulk_dml_setters)
395
+ if not setters_todo:
396
+ return
397
+
398
+ for mapping in mappings:
399
+ for key in setters_todo.intersection(mapping):
400
+ bulk_dml_setters[key](mapping)
401
+
402
+
403
+ class _ORMDMLState(_AbstractORMCompileState):
404
+ is_dml_returning = True
405
+ from_statement_ctx: Optional[_ORMFromStatementCompileState] = None
406
+
407
+ @classmethod
408
+ def _get_orm_crud_kv_pairs(
409
+ cls, mapper, statement, kv_iterator, needs_to_be_cacheable
410
+ ):
411
+ core_get_crud_kv_pairs = UpdateDMLState._get_crud_kv_pairs
412
+
413
+ for k, v in kv_iterator:
414
+ k = coercions.expect(roles.DMLColumnRole, k)
415
+
416
+ if isinstance(k, str):
417
+ desc = _entity_namespace_key(mapper, k, default=NO_VALUE)
418
+ if not isinstance(desc, PropComparator):
419
+ yield (
420
+ coercions.expect(roles.DMLColumnRole, k),
421
+ (
422
+ coercions.expect(
423
+ roles.ExpressionElementRole,
424
+ v,
425
+ type_=sqltypes.NullType(),
426
+ is_crud=True,
427
+ )
428
+ if needs_to_be_cacheable
429
+ else v
430
+ ),
431
+ )
432
+ else:
433
+ yield from core_get_crud_kv_pairs(
434
+ statement,
435
+ desc._bulk_update_tuples(v),
436
+ needs_to_be_cacheable,
437
+ )
438
+ elif "entity_namespace" in k._annotations:
439
+ k_anno = k._annotations
440
+ attr = _entity_namespace_key(
441
+ k_anno["entity_namespace"], k_anno["proxy_key"]
442
+ )
443
+ assert isinstance(attr, PropComparator)
444
+ yield from core_get_crud_kv_pairs(
445
+ statement,
446
+ attr._bulk_update_tuples(v),
447
+ needs_to_be_cacheable,
448
+ )
449
+ else:
450
+ yield (
451
+ k,
452
+ (
453
+ v
454
+ if not needs_to_be_cacheable
455
+ else coercions.expect(
456
+ roles.ExpressionElementRole,
457
+ v,
458
+ type_=sqltypes.NullType(),
459
+ is_crud=True,
460
+ )
461
+ ),
462
+ )
463
+
464
+ @classmethod
465
+ def _get_dml_plugin_subject(cls, statement):
466
+ plugin_subject = statement.table._propagate_attrs.get("plugin_subject")
467
+
468
+ if (
469
+ not plugin_subject
470
+ or not plugin_subject.mapper
471
+ or plugin_subject
472
+ is not statement._propagate_attrs["plugin_subject"]
473
+ ):
474
+ return None
475
+ return plugin_subject
476
+
477
+ @classmethod
478
+ def _get_multi_crud_kv_pairs(cls, statement, kv_iterator):
479
+ plugin_subject = cls._get_dml_plugin_subject(statement)
480
+
481
+ if not plugin_subject:
482
+ return UpdateDMLState._get_multi_crud_kv_pairs(
483
+ statement, kv_iterator
484
+ )
485
+
486
+ return [
487
+ dict(
488
+ cls._get_orm_crud_kv_pairs(
489
+ plugin_subject.mapper, statement, value_dict.items(), False
490
+ )
491
+ )
492
+ for value_dict in kv_iterator
493
+ ]
494
+
495
+ @classmethod
496
+ def _get_crud_kv_pairs(cls, statement, kv_iterator, needs_to_be_cacheable):
497
+ assert (
498
+ needs_to_be_cacheable
499
+ ), "no test coverage for needs_to_be_cacheable=False"
500
+
501
+ plugin_subject = cls._get_dml_plugin_subject(statement)
502
+
503
+ if not plugin_subject:
504
+ return UpdateDMLState._get_crud_kv_pairs(
505
+ statement, kv_iterator, needs_to_be_cacheable
506
+ )
507
+ return list(
508
+ cls._get_orm_crud_kv_pairs(
509
+ plugin_subject.mapper,
510
+ statement,
511
+ kv_iterator,
512
+ needs_to_be_cacheable,
513
+ )
514
+ )
515
+
516
+ @classmethod
517
+ def get_entity_description(cls, statement):
518
+ ext_info = statement.table._annotations["parententity"]
519
+ mapper = ext_info.mapper
520
+ if ext_info.is_aliased_class:
521
+ _label_name = ext_info.name
522
+ else:
523
+ _label_name = mapper.class_.__name__
524
+
525
+ return {
526
+ "name": _label_name,
527
+ "type": mapper.class_,
528
+ "expr": ext_info.entity,
529
+ "entity": ext_info.entity,
530
+ "table": mapper.local_table,
531
+ }
532
+
533
+ @classmethod
534
+ def get_returning_column_descriptions(cls, statement):
535
+ def _ent_for_col(c):
536
+ return c._annotations.get("parententity", None)
537
+
538
+ def _attr_for_col(c, ent):
539
+ if ent is None:
540
+ return c
541
+ proxy_key = c._annotations.get("proxy_key", None)
542
+ if not proxy_key:
543
+ return c
544
+ else:
545
+ return getattr(ent.entity, proxy_key, c)
546
+
547
+ return [
548
+ {
549
+ "name": c.key,
550
+ "type": c.type,
551
+ "expr": _attr_for_col(c, ent),
552
+ "aliased": ent.is_aliased_class,
553
+ "entity": ent.entity,
554
+ }
555
+ for c, ent in [
556
+ (c, _ent_for_col(c)) for c in statement._all_selected_columns
557
+ ]
558
+ ]
559
+
560
+ def _setup_orm_returning(
561
+ self,
562
+ compiler,
563
+ orm_level_statement,
564
+ dml_level_statement,
565
+ dml_mapper,
566
+ *,
567
+ use_supplemental_cols=True,
568
+ ):
569
+ """establish ORM column handlers for an INSERT, UPDATE, or DELETE
570
+ which uses explicit returning().
571
+
572
+ called within compilation level create_for_statement.
573
+
574
+ The _return_orm_returning() method then receives the Result
575
+ after the statement was executed, and applies ORM loading to the
576
+ state that we first established here.
577
+
578
+ """
579
+
580
+ if orm_level_statement._returning:
581
+ fs = FromStatement(
582
+ orm_level_statement._returning,
583
+ dml_level_statement,
584
+ _adapt_on_names=False,
585
+ )
586
+ fs = fs.execution_options(**orm_level_statement._execution_options)
587
+ fs = fs.options(*orm_level_statement._with_options)
588
+ self.select_statement = fs
589
+ self.from_statement_ctx = fsc = (
590
+ _ORMFromStatementCompileState.create_for_statement(
591
+ fs, compiler
592
+ )
593
+ )
594
+ fsc.setup_dml_returning_compile_state(dml_mapper)
595
+
596
+ dml_level_statement = dml_level_statement._generate()
597
+ dml_level_statement._returning = ()
598
+
599
+ cols_to_return = [c for c in fsc.primary_columns if c is not None]
600
+
601
+ # since we are splicing result sets together, make sure there
602
+ # are columns of some kind returned in each result set
603
+ if not cols_to_return:
604
+ cols_to_return.extend(dml_mapper.primary_key)
605
+
606
+ if use_supplemental_cols:
607
+ dml_level_statement = dml_level_statement.return_defaults(
608
+ # this is a little weird looking, but by passing
609
+ # primary key as the main list of cols, this tells
610
+ # return_defaults to omit server-default cols (and
611
+ # actually all cols, due to some weird thing we should
612
+ # clean up in crud.py).
613
+ # Since we have cols_to_return, just return what we asked
614
+ # for (plus primary key, which ORM persistence needs since
615
+ # we likely set bookkeeping=True here, which is another
616
+ # whole thing...). We dont want to clutter the
617
+ # statement up with lots of other cols the user didn't
618
+ # ask for. see #9685
619
+ *dml_mapper.primary_key,
620
+ supplemental_cols=cols_to_return,
621
+ )
622
+ else:
623
+ dml_level_statement = dml_level_statement.returning(
624
+ *cols_to_return
625
+ )
626
+
627
+ return dml_level_statement
628
+
629
+ @classmethod
630
+ def _return_orm_returning(
631
+ cls,
632
+ session,
633
+ statement,
634
+ params,
635
+ execution_options,
636
+ bind_arguments,
637
+ result,
638
+ ):
639
+ execution_context = result.context
640
+ compile_state = execution_context.compiled.compile_state
641
+
642
+ if (
643
+ compile_state.from_statement_ctx
644
+ and not compile_state.from_statement_ctx.compile_options._is_star
645
+ ):
646
+ load_options = execution_options.get(
647
+ "_sa_orm_load_options", QueryContext.default_load_options
648
+ )
649
+
650
+ querycontext = QueryContext(
651
+ compile_state.from_statement_ctx,
652
+ compile_state.select_statement,
653
+ statement,
654
+ params,
655
+ session,
656
+ load_options,
657
+ execution_options,
658
+ bind_arguments,
659
+ )
660
+ return loading.instances(result, querycontext)
661
+ else:
662
+ return result
663
+
664
+
665
+ class _BulkUDCompileState(_ORMDMLState):
666
+ class default_update_options(Options):
667
+ _dml_strategy: DMLStrategyArgument = "auto"
668
+ _synchronize_session: SynchronizeSessionArgument = "auto"
669
+ _can_use_returning: bool = False
670
+ _is_delete_using: bool = False
671
+ _is_update_from: bool = False
672
+ _autoflush: bool = True
673
+ _subject_mapper: Optional[Mapper[Any]] = None
674
+ _resolved_values = EMPTY_DICT
675
+ _eval_condition = None
676
+ _matched_rows = None
677
+ _identity_token = None
678
+ _populate_existing: bool = False
679
+
680
+ @classmethod
681
+ def can_use_returning(
682
+ cls,
683
+ dialect: Dialect,
684
+ mapper: Mapper[Any],
685
+ *,
686
+ is_multitable: bool = False,
687
+ is_update_from: bool = False,
688
+ is_delete_using: bool = False,
689
+ is_executemany: bool = False,
690
+ ) -> bool:
691
+ raise NotImplementedError()
692
+
693
+ @classmethod
694
+ def orm_pre_session_exec(
695
+ cls,
696
+ session,
697
+ statement,
698
+ params,
699
+ execution_options,
700
+ bind_arguments,
701
+ is_pre_event,
702
+ ):
703
+ (
704
+ update_options,
705
+ execution_options,
706
+ ) = _BulkUDCompileState.default_update_options.from_execution_options(
707
+ "_sa_orm_update_options",
708
+ {
709
+ "synchronize_session",
710
+ "autoflush",
711
+ "populate_existing",
712
+ "identity_token",
713
+ "is_delete_using",
714
+ "is_update_from",
715
+ "dml_strategy",
716
+ },
717
+ execution_options,
718
+ statement._execution_options,
719
+ )
720
+ bind_arguments["clause"] = statement
721
+ try:
722
+ plugin_subject = statement._propagate_attrs["plugin_subject"]
723
+ except KeyError:
724
+ assert False, "statement had 'orm' plugin but no plugin_subject"
725
+ else:
726
+ if plugin_subject:
727
+ bind_arguments["mapper"] = plugin_subject.mapper
728
+ update_options += {"_subject_mapper": plugin_subject.mapper}
729
+
730
+ if "parententity" not in statement.table._annotations:
731
+ update_options += {"_dml_strategy": "core_only"}
732
+ elif not isinstance(params, list):
733
+ if update_options._dml_strategy == "auto":
734
+ update_options += {"_dml_strategy": "orm"}
735
+ elif update_options._dml_strategy == "bulk":
736
+ raise sa_exc.InvalidRequestError(
737
+ 'Can\'t use "bulk" ORM insert strategy without '
738
+ "passing separate parameters"
739
+ )
740
+ else:
741
+ if update_options._dml_strategy == "auto":
742
+ update_options += {"_dml_strategy": "bulk"}
743
+
744
+ sync = update_options._synchronize_session
745
+ if sync is not None:
746
+ if sync not in ("auto", "evaluate", "fetch", False):
747
+ raise sa_exc.ArgumentError(
748
+ "Valid strategies for session synchronization "
749
+ "are 'auto', 'evaluate', 'fetch', False"
750
+ )
751
+ if update_options._dml_strategy == "bulk" and sync == "fetch":
752
+ raise sa_exc.InvalidRequestError(
753
+ "The 'fetch' synchronization strategy is not available "
754
+ "for 'bulk' ORM updates (i.e. multiple parameter sets)"
755
+ )
756
+
757
+ if not is_pre_event:
758
+ if update_options._autoflush:
759
+ session._autoflush()
760
+
761
+ if update_options._dml_strategy == "orm":
762
+ if update_options._synchronize_session == "auto":
763
+ update_options = cls._do_pre_synchronize_auto(
764
+ session,
765
+ statement,
766
+ params,
767
+ execution_options,
768
+ bind_arguments,
769
+ update_options,
770
+ )
771
+ elif update_options._synchronize_session == "evaluate":
772
+ update_options = cls._do_pre_synchronize_evaluate(
773
+ session,
774
+ statement,
775
+ params,
776
+ execution_options,
777
+ bind_arguments,
778
+ update_options,
779
+ )
780
+ elif update_options._synchronize_session == "fetch":
781
+ update_options = cls._do_pre_synchronize_fetch(
782
+ session,
783
+ statement,
784
+ params,
785
+ execution_options,
786
+ bind_arguments,
787
+ update_options,
788
+ )
789
+ elif update_options._dml_strategy == "bulk":
790
+ if update_options._synchronize_session == "auto":
791
+ update_options += {"_synchronize_session": "evaluate"}
792
+
793
+ # indicators from the "pre exec" step that are then
794
+ # added to the DML statement, which will also be part of the cache
795
+ # key. The compile level create_for_statement() method will then
796
+ # consume these at compiler time.
797
+ statement = statement._annotate(
798
+ {
799
+ "synchronize_session": update_options._synchronize_session,
800
+ "is_delete_using": update_options._is_delete_using,
801
+ "is_update_from": update_options._is_update_from,
802
+ "dml_strategy": update_options._dml_strategy,
803
+ "can_use_returning": update_options._can_use_returning,
804
+ }
805
+ )
806
+
807
+ # disable result-level adapt_to_context. ORM UPDATE/DELETE with
808
+ # RETURNING uses a "two level" statement: the invoked ORM statement
809
+ # holds the user's returning() columns while the cached Core
810
+ # statement returns primary key + supplemental columns, so the two
811
+ # are not positionally aligned. adapt_to_context() would remap
812
+ # result keys positionally between them and mislabel columns on a
813
+ # cache hit; the ORM instead interprets rows via its own
814
+ # from_statement context, so the Core adaptation would interfere
815
+ # here.
816
+ if not execution_options:
817
+ execution_options = context._orm_load_exec_options
818
+ else:
819
+ execution_options = execution_options.union(
820
+ context._orm_load_exec_options
821
+ )
822
+
823
+ return (
824
+ statement,
825
+ util.immutabledict(execution_options).union(
826
+ {"_sa_orm_update_options": update_options}
827
+ ),
828
+ params,
829
+ )
830
+
831
+ @classmethod
832
+ def orm_setup_cursor_result(
833
+ cls,
834
+ session,
835
+ statement,
836
+ params,
837
+ execution_options,
838
+ bind_arguments,
839
+ result,
840
+ ):
841
+ # this stage of the execution is called after the
842
+ # do_orm_execute event hook. meaning for an extension like
843
+ # horizontal sharding, this step happens *within* the horizontal
844
+ # sharding event handler which calls session.execute() re-entrantly
845
+ # and will occur for each backend individually.
846
+ # the sharding extension then returns its own merged result from the
847
+ # individual ones we return here.
848
+
849
+ update_options = execution_options["_sa_orm_update_options"]
850
+ if update_options._dml_strategy == "orm":
851
+ if update_options._synchronize_session == "evaluate":
852
+ cls._do_post_synchronize_evaluate(
853
+ session, statement, result, update_options
854
+ )
855
+ elif update_options._synchronize_session == "fetch":
856
+ cls._do_post_synchronize_fetch(
857
+ session, statement, result, update_options
858
+ )
859
+ elif update_options._dml_strategy == "bulk":
860
+ if update_options._synchronize_session == "evaluate":
861
+ cls._do_post_synchronize_bulk_evaluate(
862
+ session, params, result, update_options
863
+ )
864
+ return result
865
+
866
+ return cls._return_orm_returning(
867
+ session,
868
+ statement,
869
+ params,
870
+ execution_options,
871
+ bind_arguments,
872
+ result,
873
+ )
874
+
875
+ @classmethod
876
+ def _adjust_for_extra_criteria(cls, global_attributes, ext_info):
877
+ """Apply extra criteria filtering.
878
+
879
+ For all distinct single-table-inheritance mappers represented in the
880
+ table being updated or deleted, produce additional WHERE criteria such
881
+ that only the appropriate subtypes are selected from the total results.
882
+
883
+ Additionally, add WHERE criteria originating from LoaderCriteriaOptions
884
+ collected from the statement.
885
+
886
+ """
887
+
888
+ return_crit = ()
889
+
890
+ adapter = ext_info._adapter if ext_info.is_aliased_class else None
891
+
892
+ if (
893
+ "additional_entity_criteria",
894
+ ext_info.mapper,
895
+ ) in global_attributes:
896
+ return_crit += tuple(
897
+ ae._resolve_where_criteria(ext_info)
898
+ for ae in global_attributes[
899
+ ("additional_entity_criteria", ext_info.mapper)
900
+ ]
901
+ if ae.include_aliases or ae.entity is ext_info
902
+ )
903
+
904
+ if ext_info.mapper._single_table_criterion is not None:
905
+ return_crit += (ext_info.mapper._single_table_criterion,)
906
+
907
+ if adapter:
908
+ return_crit = tuple(adapter.traverse(crit) for crit in return_crit)
909
+
910
+ return return_crit
911
+
912
+ @classmethod
913
+ def _interpret_returning_rows(cls, result, mapper, rows):
914
+ """return rows that indicate PK cols in mapper.primary_key position
915
+ for RETURNING rows.
916
+
917
+ Prior to 2.0.36, this method seemed to be written for some kind of
918
+ inheritance scenario but the scenario was unused for actual joined
919
+ inheritance, and the function instead seemed to perform some kind of
920
+ partial translation that would remove non-PK cols if the PK cols
921
+ happened to be first in the row, but not otherwise. The joined
922
+ inheritance walk feature here seems to have never been used as it was
923
+ always skipped by the "local_table" check.
924
+
925
+ As of 2.0.36 the function strips away non-PK cols and provides the
926
+ PK cols for the table in mapper PK order.
927
+
928
+ """
929
+
930
+ try:
931
+ if mapper.local_table is not mapper.base_mapper.local_table:
932
+ # TODO: dive more into how a local table PK is used for fetch
933
+ # sync, not clear if this is correct as it depends on the
934
+ # downstream routine to fetch rows using
935
+ # local_table.primary_key order
936
+ pk_keys = result._tuple_getter(mapper.local_table.primary_key)
937
+ else:
938
+ pk_keys = result._tuple_getter(mapper.primary_key)
939
+ except KeyError:
940
+ # can't use these rows, they don't have PK cols in them
941
+ # this is an unusual case where the user would have used
942
+ # .return_defaults()
943
+ return []
944
+
945
+ return [pk_keys(row) for row in rows]
946
+
947
+ @classmethod
948
+ def _get_matched_objects_on_criteria(cls, update_options, states):
949
+ mapper = update_options._subject_mapper
950
+ eval_condition = update_options._eval_condition
951
+
952
+ raw_data = [
953
+ (state.obj(), state, state.dict)
954
+ for state in states
955
+ if state.mapper.isa(mapper) and not state.expired
956
+ ]
957
+
958
+ identity_token = update_options._identity_token
959
+ if identity_token is not None:
960
+ raw_data = [
961
+ (obj, state, dict_)
962
+ for obj, state, dict_ in raw_data
963
+ if state.identity_token == identity_token
964
+ ]
965
+
966
+ result = []
967
+ for obj, state, dict_ in raw_data:
968
+ evaled_condition = eval_condition(obj)
969
+
970
+ # caution: don't use "in ()" or == here, _EXPIRE_OBJECT
971
+ # evaluates as True for all comparisons
972
+ if (
973
+ evaled_condition is True
974
+ or evaled_condition is evaluator._EXPIRED_OBJECT
975
+ ):
976
+ result.append(
977
+ (
978
+ obj,
979
+ state,
980
+ dict_,
981
+ evaled_condition is evaluator._EXPIRED_OBJECT,
982
+ )
983
+ )
984
+ return result
985
+
986
+ @classmethod
987
+ def _eval_condition_from_statement(cls, update_options, statement):
988
+ mapper = update_options._subject_mapper
989
+ target_cls = mapper.class_
990
+
991
+ evaluator_compiler = evaluator._EvaluatorCompiler(target_cls)
992
+ crit = ()
993
+ if statement._where_criteria:
994
+ crit += statement._where_criteria
995
+
996
+ global_attributes = {}
997
+ for opt in statement._with_options:
998
+ if opt._is_criteria_option:
999
+ opt.get_global_criteria(global_attributes)
1000
+
1001
+ if global_attributes:
1002
+ crit += cls._adjust_for_extra_criteria(global_attributes, mapper)
1003
+
1004
+ if crit:
1005
+ eval_condition = evaluator_compiler.process(*crit)
1006
+ else:
1007
+ # workaround for mypy https://github.com/python/mypy/issues/14027
1008
+ def _eval_condition(obj):
1009
+ return True
1010
+
1011
+ eval_condition = _eval_condition
1012
+
1013
+ return eval_condition
1014
+
1015
+ @classmethod
1016
+ def _do_pre_synchronize_auto(
1017
+ cls,
1018
+ session,
1019
+ statement,
1020
+ params,
1021
+ execution_options,
1022
+ bind_arguments,
1023
+ update_options,
1024
+ ):
1025
+ """setup auto sync strategy
1026
+
1027
+
1028
+ "auto" checks if we can use "evaluate" first, then falls back
1029
+ to "fetch"
1030
+
1031
+ evaluate is vastly more efficient for the common case
1032
+ where session is empty, only has a few objects, and the UPDATE
1033
+ statement can potentially match thousands/millions of rows.
1034
+
1035
+ OTOH more complex criteria that fails to work with "evaluate"
1036
+ we would hope usually correlates with fewer net rows.
1037
+
1038
+ """
1039
+
1040
+ try:
1041
+ eval_condition = cls._eval_condition_from_statement(
1042
+ update_options, statement
1043
+ )
1044
+
1045
+ except evaluator.UnevaluatableError:
1046
+ pass
1047
+ else:
1048
+ return update_options + {
1049
+ "_eval_condition": eval_condition,
1050
+ "_synchronize_session": "evaluate",
1051
+ }
1052
+
1053
+ update_options += {"_synchronize_session": "fetch"}
1054
+ return cls._do_pre_synchronize_fetch(
1055
+ session,
1056
+ statement,
1057
+ params,
1058
+ execution_options,
1059
+ bind_arguments,
1060
+ update_options,
1061
+ )
1062
+
1063
+ @classmethod
1064
+ def _do_pre_synchronize_evaluate(
1065
+ cls,
1066
+ session,
1067
+ statement,
1068
+ params,
1069
+ execution_options,
1070
+ bind_arguments,
1071
+ update_options,
1072
+ ):
1073
+ try:
1074
+ eval_condition = cls._eval_condition_from_statement(
1075
+ update_options, statement
1076
+ )
1077
+
1078
+ except evaluator.UnevaluatableError as err:
1079
+ raise sa_exc.InvalidRequestError(
1080
+ 'Could not evaluate current criteria in Python: "%s". '
1081
+ "Specify 'fetch' or False for the "
1082
+ "synchronize_session execution option." % err
1083
+ ) from err
1084
+
1085
+ return update_options + {
1086
+ "_eval_condition": eval_condition,
1087
+ }
1088
+
1089
+ @classmethod
1090
+ def _get_resolved_values(cls, mapper, statement):
1091
+ if statement._multi_values:
1092
+ return []
1093
+ elif statement._values:
1094
+ return list(statement._values.items())
1095
+ else:
1096
+ return []
1097
+
1098
+ @classmethod
1099
+ def _resolved_keys_as_propnames(cls, mapper, resolved_values):
1100
+ values = []
1101
+ for k, v in resolved_values:
1102
+ if mapper and isinstance(k, expression.ColumnElement):
1103
+ try:
1104
+ attr = mapper._columntoproperty[k]
1105
+ except orm_exc.UnmappedColumnError:
1106
+ pass
1107
+ else:
1108
+ values.append((attr.key, v))
1109
+ else:
1110
+ raise sa_exc.InvalidRequestError(
1111
+ "Attribute name not found, can't be "
1112
+ "synchronized back to objects: %r" % k
1113
+ )
1114
+ return values
1115
+
1116
+ @classmethod
1117
+ def _do_pre_synchronize_fetch(
1118
+ cls,
1119
+ session,
1120
+ statement,
1121
+ params,
1122
+ execution_options,
1123
+ bind_arguments,
1124
+ update_options,
1125
+ ):
1126
+ mapper = update_options._subject_mapper
1127
+
1128
+ select_stmt = (
1129
+ select(*(mapper.primary_key + (mapper.select_identity_token,)))
1130
+ .select_from(mapper)
1131
+ .options(*statement._with_options)
1132
+ )
1133
+ select_stmt._where_criteria = statement._where_criteria
1134
+
1135
+ # conditionally run the SELECT statement for pre-fetch, testing the
1136
+ # "bind" for if we can use RETURNING or not using the do_orm_execute
1137
+ # event. If RETURNING is available, the do_orm_execute event
1138
+ # will cancel the SELECT from being actually run.
1139
+ #
1140
+ # The way this is organized seems strange, why don't we just
1141
+ # call can_use_returning() before invoking the statement and get
1142
+ # answer?, why does this go through the whole execute phase using an
1143
+ # event? Answer: because we are integrating with extensions such
1144
+ # as the horizontal sharding extension that "multiplexes" an individual
1145
+ # statement run through multiple engines, and it uses
1146
+ # do_orm_execute() to do that.
1147
+
1148
+ can_use_returning = None
1149
+
1150
+ def skip_for_returning(orm_context: ORMExecuteState) -> Any:
1151
+ bind = orm_context.session.get_bind(**orm_context.bind_arguments)
1152
+ nonlocal can_use_returning
1153
+
1154
+ per_bind_result = cls.can_use_returning(
1155
+ bind.dialect,
1156
+ mapper,
1157
+ is_update_from=update_options._is_update_from,
1158
+ is_delete_using=update_options._is_delete_using,
1159
+ is_executemany=orm_context.is_executemany,
1160
+ )
1161
+
1162
+ if can_use_returning is not None:
1163
+ if can_use_returning != per_bind_result:
1164
+ raise sa_exc.InvalidRequestError(
1165
+ "For synchronize_session='fetch', can't mix multiple "
1166
+ "backends where some support RETURNING and others "
1167
+ "don't"
1168
+ )
1169
+ elif orm_context.is_executemany and not per_bind_result:
1170
+ raise sa_exc.InvalidRequestError(
1171
+ "For synchronize_session='fetch', can't use multiple "
1172
+ "parameter sets in ORM mode, which this backend does not "
1173
+ "support with RETURNING"
1174
+ )
1175
+ else:
1176
+ can_use_returning = per_bind_result
1177
+
1178
+ if per_bind_result:
1179
+ return _result.null_result()
1180
+ else:
1181
+ return None
1182
+
1183
+ result = session.execute(
1184
+ select_stmt,
1185
+ params,
1186
+ execution_options=execution_options,
1187
+ bind_arguments=bind_arguments,
1188
+ _add_event=skip_for_returning,
1189
+ )
1190
+ matched_rows = result.fetchall()
1191
+
1192
+ return update_options + {
1193
+ "_matched_rows": matched_rows,
1194
+ "_can_use_returning": can_use_returning,
1195
+ }
1196
+
1197
+
1198
+ @CompileState.plugin_for("orm", "insert")
1199
+ class _BulkORMInsert(_ORMDMLState, InsertDMLState):
1200
+ class default_insert_options(Options):
1201
+ _dml_strategy: DMLStrategyArgument = "auto"
1202
+ _render_nulls: bool = False
1203
+ _return_defaults: bool = False
1204
+ _subject_mapper: Optional[Mapper[Any]] = None
1205
+ _autoflush: bool = True
1206
+ _populate_existing: bool = False
1207
+
1208
+ select_statement: Optional[FromStatement] = None
1209
+
1210
+ @classmethod
1211
+ def orm_pre_session_exec(
1212
+ cls,
1213
+ session,
1214
+ statement,
1215
+ params,
1216
+ execution_options,
1217
+ bind_arguments,
1218
+ is_pre_event,
1219
+ ):
1220
+ (
1221
+ insert_options,
1222
+ execution_options,
1223
+ ) = _BulkORMInsert.default_insert_options.from_execution_options(
1224
+ "_sa_orm_insert_options",
1225
+ {"dml_strategy", "autoflush", "populate_existing", "render_nulls"},
1226
+ execution_options,
1227
+ statement._execution_options,
1228
+ )
1229
+ bind_arguments["clause"] = statement
1230
+ try:
1231
+ plugin_subject = statement._propagate_attrs["plugin_subject"]
1232
+ except KeyError:
1233
+ assert False, "statement had 'orm' plugin but no plugin_subject"
1234
+ else:
1235
+ if plugin_subject:
1236
+ bind_arguments["mapper"] = plugin_subject.mapper
1237
+ insert_options += {"_subject_mapper": plugin_subject.mapper}
1238
+
1239
+ if not params:
1240
+ if insert_options._dml_strategy == "auto":
1241
+ insert_options += {"_dml_strategy": "orm"}
1242
+ elif insert_options._dml_strategy == "bulk":
1243
+ raise sa_exc.InvalidRequestError(
1244
+ 'Can\'t use "bulk" ORM insert strategy without '
1245
+ "passing separate parameters"
1246
+ )
1247
+ else:
1248
+ if insert_options._dml_strategy == "auto":
1249
+ insert_options += {"_dml_strategy": "bulk"}
1250
+
1251
+ if insert_options._dml_strategy != "raw":
1252
+ # for ORM object loading, like ORMContext, we have to disable
1253
+ # result set adapt_to_context, because we will be generating a
1254
+ # new statement with specific columns that's cached inside of
1255
+ # an ORMFromStatementCompileState, which we will reuse for
1256
+ # each result.
1257
+ if not execution_options:
1258
+ execution_options = context._orm_load_exec_options
1259
+ else:
1260
+ execution_options = execution_options.union(
1261
+ context._orm_load_exec_options
1262
+ )
1263
+
1264
+ if not is_pre_event and insert_options._autoflush:
1265
+ session._autoflush()
1266
+
1267
+ statement = statement._annotate(
1268
+ {"dml_strategy": insert_options._dml_strategy}
1269
+ )
1270
+
1271
+ return (
1272
+ statement,
1273
+ util.immutabledict(execution_options).union(
1274
+ {"_sa_orm_insert_options": insert_options}
1275
+ ),
1276
+ params,
1277
+ )
1278
+
1279
+ @classmethod
1280
+ def orm_execute_statement(
1281
+ cls,
1282
+ session: Session,
1283
+ statement: dml.Insert,
1284
+ params: _CoreAnyExecuteParams,
1285
+ execution_options: OrmExecuteOptionsParameter,
1286
+ bind_arguments: _BindArguments,
1287
+ conn: Connection,
1288
+ ) -> _result.Result:
1289
+ insert_options = execution_options.get(
1290
+ "_sa_orm_insert_options", cls.default_insert_options
1291
+ )
1292
+
1293
+ if insert_options._dml_strategy not in (
1294
+ "raw",
1295
+ "bulk",
1296
+ "orm",
1297
+ "auto",
1298
+ ):
1299
+ raise sa_exc.ArgumentError(
1300
+ "Valid strategies for ORM insert strategy "
1301
+ "are 'raw', 'orm', 'bulk', 'auto"
1302
+ )
1303
+
1304
+ result: _result.Result[Unpack[TupleAny]]
1305
+
1306
+ if insert_options._dml_strategy == "raw":
1307
+ result = conn.execute(
1308
+ statement, params or {}, execution_options=execution_options
1309
+ )
1310
+ return result
1311
+
1312
+ if insert_options._dml_strategy == "bulk":
1313
+ mapper = insert_options._subject_mapper
1314
+
1315
+ if (
1316
+ statement._post_values_clause is not None
1317
+ and mapper._multiple_persistence_tables
1318
+ ):
1319
+ raise sa_exc.InvalidRequestError(
1320
+ "bulk INSERT with a 'post values' clause "
1321
+ "(typically upsert) not supported for multi-table "
1322
+ f"mapper {mapper}"
1323
+ )
1324
+
1325
+ assert mapper is not None
1326
+ assert session._transaction is not None
1327
+ result = _bulk_insert(
1328
+ mapper,
1329
+ cast(
1330
+ "Iterable[Dict[str, Any]]",
1331
+ [params] if isinstance(params, dict) else params,
1332
+ ),
1333
+ session._transaction,
1334
+ isstates=False,
1335
+ return_defaults=insert_options._return_defaults,
1336
+ render_nulls=insert_options._render_nulls,
1337
+ use_orm_insert_stmt=statement,
1338
+ execution_options=execution_options,
1339
+ )
1340
+ elif insert_options._dml_strategy == "orm":
1341
+ result = conn.execute(
1342
+ statement, params or {}, execution_options=execution_options
1343
+ )
1344
+ else:
1345
+ raise AssertionError()
1346
+
1347
+ if not bool(statement._returning):
1348
+ return result
1349
+
1350
+ if insert_options._populate_existing:
1351
+ load_options = execution_options.get(
1352
+ "_sa_orm_load_options", QueryContext.default_load_options
1353
+ )
1354
+ load_options += {"_populate_existing": True}
1355
+ execution_options = execution_options.union(
1356
+ {"_sa_orm_load_options": load_options}
1357
+ )
1358
+
1359
+ return cls._return_orm_returning(
1360
+ session,
1361
+ statement,
1362
+ params,
1363
+ execution_options,
1364
+ bind_arguments,
1365
+ result,
1366
+ )
1367
+
1368
+ @classmethod
1369
+ def create_for_statement(cls, statement, compiler, **kw) -> _BulkORMInsert:
1370
+ self = cast(
1371
+ _BulkORMInsert,
1372
+ super().create_for_statement(statement, compiler, **kw),
1373
+ )
1374
+
1375
+ if compiler is not None:
1376
+ toplevel = not compiler.stack
1377
+ else:
1378
+ toplevel = True
1379
+ if not toplevel:
1380
+ return self
1381
+
1382
+ mapper = statement._propagate_attrs["plugin_subject"]
1383
+ dml_strategy = statement._annotations.get("dml_strategy", "raw")
1384
+ if dml_strategy == "bulk":
1385
+ self._setup_for_bulk_insert(compiler)
1386
+ elif dml_strategy == "orm":
1387
+ self._setup_for_orm_insert(compiler, mapper)
1388
+
1389
+ return self
1390
+
1391
+ @classmethod
1392
+ def _resolved_keys_as_col_keys(cls, mapper, resolved_value_dict):
1393
+ return {
1394
+ col.key if col is not None else k: v
1395
+ for col, k, v in (
1396
+ (mapper.c.get(k), k, v) for k, v in resolved_value_dict.items()
1397
+ )
1398
+ }
1399
+
1400
+ def _setup_for_orm_insert(self, compiler, mapper):
1401
+ statement = orm_level_statement = cast(dml.Insert, self.statement)
1402
+
1403
+ statement = self._setup_orm_returning(
1404
+ compiler,
1405
+ orm_level_statement,
1406
+ statement,
1407
+ dml_mapper=mapper,
1408
+ use_supplemental_cols=False,
1409
+ )
1410
+ self.statement = statement
1411
+
1412
+ def _setup_for_bulk_insert(self, compiler):
1413
+ """establish an INSERT statement within the context of
1414
+ bulk insert.
1415
+
1416
+ This method will be within the "conn.execute()" call that is invoked
1417
+ by persistence._emit_insert_statement().
1418
+
1419
+ """
1420
+ statement = orm_level_statement = cast(dml.Insert, self.statement)
1421
+ an = statement._annotations
1422
+
1423
+ emit_insert_table, emit_insert_mapper = (
1424
+ an["_emit_insert_table"],
1425
+ an["_emit_insert_mapper"],
1426
+ )
1427
+
1428
+ statement = statement._clone()
1429
+
1430
+ statement.table = emit_insert_table
1431
+ if self._dict_parameters:
1432
+ self._dict_parameters = {
1433
+ col: val
1434
+ for col, val in self._dict_parameters.items()
1435
+ if col.table is emit_insert_table
1436
+ }
1437
+
1438
+ statement = self._setup_orm_returning(
1439
+ compiler,
1440
+ orm_level_statement,
1441
+ statement,
1442
+ dml_mapper=emit_insert_mapper,
1443
+ use_supplemental_cols=True,
1444
+ )
1445
+
1446
+ if (
1447
+ self.from_statement_ctx is not None
1448
+ and self.from_statement_ctx.compile_options._is_star
1449
+ ):
1450
+ raise sa_exc.CompileError(
1451
+ "Can't use RETURNING * with bulk ORM INSERT. "
1452
+ "Please use a different INSERT form, such as INSERT..VALUES "
1453
+ "or INSERT with a Core Connection"
1454
+ )
1455
+
1456
+ self.statement = statement
1457
+
1458
+
1459
+ @CompileState.plugin_for("orm", "update")
1460
+ class _BulkORMUpdate(_BulkUDCompileState, UpdateDMLState):
1461
+ @classmethod
1462
+ def create_for_statement(cls, statement, compiler, **kw):
1463
+ self = cls.__new__(cls)
1464
+
1465
+ dml_strategy = statement._annotations.get(
1466
+ "dml_strategy", "unspecified"
1467
+ )
1468
+
1469
+ toplevel = not compiler.stack
1470
+
1471
+ if toplevel and dml_strategy == "bulk":
1472
+ self._setup_for_bulk_update(statement, compiler)
1473
+ elif (
1474
+ dml_strategy == "core_only"
1475
+ or dml_strategy == "unspecified"
1476
+ and "parententity" not in statement.table._annotations
1477
+ ):
1478
+ UpdateDMLState.__init__(self, statement, compiler, **kw)
1479
+ elif not toplevel or dml_strategy in ("orm", "unspecified"):
1480
+ self._setup_for_orm_update(statement, compiler)
1481
+
1482
+ return self
1483
+
1484
+ def _setup_for_orm_update(self, statement, compiler, **kw):
1485
+ orm_level_statement = statement
1486
+
1487
+ toplevel = not compiler.stack
1488
+
1489
+ ext_info = statement.table._annotations["parententity"]
1490
+
1491
+ self.mapper = mapper = ext_info.mapper
1492
+
1493
+ self._resolved_values = self._get_resolved_values(mapper, statement)
1494
+
1495
+ self._init_global_attributes(
1496
+ statement,
1497
+ compiler,
1498
+ toplevel=toplevel,
1499
+ process_criteria_for_toplevel=toplevel,
1500
+ )
1501
+
1502
+ if statement._values:
1503
+ self._resolved_values = dict(self._resolved_values)
1504
+
1505
+ new_stmt = statement._clone()
1506
+
1507
+ if new_stmt.table._annotations["parententity"] is mapper:
1508
+ new_stmt.table = mapper.local_table
1509
+
1510
+ # note if the statement has _multi_values, these
1511
+ # are passed through to the new statement, which will then raise
1512
+ # InvalidRequestError because UPDATE doesn't support multi_values
1513
+ # right now.
1514
+ if statement._values:
1515
+ new_stmt._values = self._resolved_values
1516
+
1517
+ new_crit = self._adjust_for_extra_criteria(
1518
+ self.global_attributes, mapper
1519
+ )
1520
+ if new_crit:
1521
+ new_stmt = new_stmt.where(*new_crit)
1522
+
1523
+ # if we are against a lambda statement we might not be the
1524
+ # topmost object that received per-execute annotations
1525
+
1526
+ # do this first as we need to determine if there is
1527
+ # UPDATE..FROM
1528
+
1529
+ UpdateDMLState.__init__(self, new_stmt, compiler, **kw)
1530
+
1531
+ use_supplemental_cols = False
1532
+
1533
+ if not toplevel:
1534
+ synchronize_session = None
1535
+ else:
1536
+ synchronize_session = compiler._annotations.get(
1537
+ "synchronize_session", None
1538
+ )
1539
+ can_use_returning = compiler._annotations.get(
1540
+ "can_use_returning", None
1541
+ )
1542
+ if can_use_returning is not False:
1543
+ # even though pre_exec has determined basic
1544
+ # can_use_returning for the dialect, if we are to use
1545
+ # RETURNING we need to run can_use_returning() at this level
1546
+ # unconditionally because is_delete_using was not known
1547
+ # at the pre_exec level
1548
+ can_use_returning = (
1549
+ synchronize_session == "fetch"
1550
+ and self.can_use_returning(
1551
+ compiler.dialect, mapper, is_multitable=self.is_multitable
1552
+ )
1553
+ )
1554
+
1555
+ if synchronize_session == "fetch" and can_use_returning:
1556
+ use_supplemental_cols = True
1557
+
1558
+ # NOTE: we might want to RETURNING the actual columns to be
1559
+ # synchronized also. however this is complicated and difficult
1560
+ # to align against the behavior of "evaluate". Additionally,
1561
+ # in a large number (if not the majority) of cases, we have the
1562
+ # "evaluate" answer, usually a fixed value, in memory already and
1563
+ # there's no need to re-fetch the same value
1564
+ # over and over again. so perhaps if it could be RETURNING just
1565
+ # the elements that were based on a SQL expression and not
1566
+ # a constant. For now it doesn't quite seem worth it
1567
+ new_stmt = new_stmt.return_defaults(*new_stmt.table.primary_key)
1568
+
1569
+ if toplevel:
1570
+ new_stmt = self._setup_orm_returning(
1571
+ compiler,
1572
+ orm_level_statement,
1573
+ new_stmt,
1574
+ dml_mapper=mapper,
1575
+ use_supplemental_cols=use_supplemental_cols,
1576
+ )
1577
+
1578
+ self.statement = new_stmt
1579
+
1580
+ def _setup_for_bulk_update(self, statement, compiler, **kw):
1581
+ """establish an UPDATE statement within the context of
1582
+ bulk insert.
1583
+
1584
+ This method will be within the "conn.execute()" call that is invoked
1585
+ by persistence._emit_update_statement().
1586
+
1587
+ """
1588
+ statement = cast(dml.Update, statement)
1589
+ an = statement._annotations
1590
+
1591
+ emit_update_table, _ = (
1592
+ an["_emit_update_table"],
1593
+ an["_emit_update_mapper"],
1594
+ )
1595
+
1596
+ statement = statement._clone()
1597
+ statement.table = emit_update_table
1598
+
1599
+ UpdateDMLState.__init__(self, statement, compiler, **kw)
1600
+
1601
+ if self._maintain_values_ordering:
1602
+ raise sa_exc.InvalidRequestError(
1603
+ "bulk ORM UPDATE does not support ordered_values() for "
1604
+ "custom UPDATE statements with bulk parameter sets. Use a "
1605
+ "non-bulk UPDATE statement or use values()."
1606
+ )
1607
+
1608
+ if self._dict_parameters:
1609
+ self._dict_parameters = {
1610
+ col: val
1611
+ for col, val in self._dict_parameters.items()
1612
+ if col.table is emit_update_table
1613
+ }
1614
+ self.statement = statement
1615
+
1616
+ @classmethod
1617
+ def orm_execute_statement(
1618
+ cls,
1619
+ session: Session,
1620
+ statement: dml.Update,
1621
+ params: _CoreAnyExecuteParams,
1622
+ execution_options: OrmExecuteOptionsParameter,
1623
+ bind_arguments: _BindArguments,
1624
+ conn: Connection,
1625
+ ) -> _result.Result:
1626
+
1627
+ update_options = execution_options.get(
1628
+ "_sa_orm_update_options", cls.default_update_options
1629
+ )
1630
+
1631
+ if update_options._populate_existing:
1632
+ load_options = execution_options.get(
1633
+ "_sa_orm_load_options", QueryContext.default_load_options
1634
+ )
1635
+ load_options += {"_populate_existing": True}
1636
+ execution_options = execution_options.union(
1637
+ {"_sa_orm_load_options": load_options}
1638
+ )
1639
+
1640
+ if update_options._dml_strategy not in (
1641
+ "orm",
1642
+ "auto",
1643
+ "bulk",
1644
+ "core_only",
1645
+ ):
1646
+ raise sa_exc.ArgumentError(
1647
+ "Valid strategies for ORM UPDATE strategy "
1648
+ "are 'orm', 'auto', 'bulk', 'core_only'"
1649
+ )
1650
+
1651
+ result: _result.Result[Unpack[TupleAny]]
1652
+
1653
+ if update_options._dml_strategy == "bulk":
1654
+ enable_check_rowcount = not statement._where_criteria
1655
+
1656
+ assert update_options._synchronize_session != "fetch"
1657
+
1658
+ if (
1659
+ statement._where_criteria
1660
+ and update_options._synchronize_session == "evaluate"
1661
+ ):
1662
+ raise sa_exc.InvalidRequestError(
1663
+ "bulk synchronize of persistent objects not supported "
1664
+ "when using bulk update with additional WHERE "
1665
+ "criteria right now. add synchronize_session=None "
1666
+ "execution option to bypass synchronize of persistent "
1667
+ "objects."
1668
+ )
1669
+ mapper = update_options._subject_mapper
1670
+ assert mapper is not None
1671
+ assert session._transaction is not None
1672
+ result = _bulk_update(
1673
+ mapper,
1674
+ cast(
1675
+ "Iterable[Dict[str, Any]]",
1676
+ [params] if isinstance(params, dict) else params,
1677
+ ),
1678
+ session._transaction,
1679
+ isstates=False,
1680
+ update_changed_only=False,
1681
+ use_orm_update_stmt=statement,
1682
+ enable_check_rowcount=enable_check_rowcount,
1683
+ )
1684
+ return cls.orm_setup_cursor_result(
1685
+ session,
1686
+ statement,
1687
+ params,
1688
+ execution_options,
1689
+ bind_arguments,
1690
+ result,
1691
+ )
1692
+ else:
1693
+ return super().orm_execute_statement(
1694
+ session,
1695
+ statement,
1696
+ params,
1697
+ execution_options,
1698
+ bind_arguments,
1699
+ conn,
1700
+ )
1701
+
1702
+ @classmethod
1703
+ def can_use_returning(
1704
+ cls,
1705
+ dialect: Dialect,
1706
+ mapper: Mapper[Any],
1707
+ *,
1708
+ is_multitable: bool = False,
1709
+ is_update_from: bool = False,
1710
+ is_delete_using: bool = False,
1711
+ is_executemany: bool = False,
1712
+ ) -> bool:
1713
+ # normal answer for "should we use RETURNING" at all.
1714
+ normal_answer = (
1715
+ dialect.update_returning and mapper.local_table.implicit_returning
1716
+ )
1717
+ if not normal_answer:
1718
+ return False
1719
+
1720
+ if is_executemany:
1721
+ return dialect.update_executemany_returning
1722
+
1723
+ # these workarounds are currently hypothetical for UPDATE,
1724
+ # unlike DELETE where they impact MariaDB
1725
+ if is_update_from:
1726
+ return dialect.update_returning_multifrom
1727
+
1728
+ elif is_multitable and not dialect.update_returning_multifrom:
1729
+ raise sa_exc.CompileError(
1730
+ f'Dialect "{dialect.name}" does not support RETURNING '
1731
+ "with UPDATE..FROM; for synchronize_session='fetch', "
1732
+ "please add the additional execution option "
1733
+ "'is_update_from=True' to the statement to indicate that "
1734
+ "a separate SELECT should be used for this backend."
1735
+ )
1736
+
1737
+ return True
1738
+
1739
+ @classmethod
1740
+ def _do_post_synchronize_bulk_evaluate(
1741
+ cls, session, params, result, update_options
1742
+ ):
1743
+ if not params:
1744
+ return
1745
+
1746
+ mapper = update_options._subject_mapper
1747
+ pk_keys = [prop.key for prop in mapper._identity_key_props]
1748
+
1749
+ identity_map = session.identity_map
1750
+
1751
+ for param in params:
1752
+ identity_key = mapper.identity_key_from_primary_key(
1753
+ (param[key] for key in pk_keys),
1754
+ update_options._identity_token,
1755
+ )
1756
+ state = identity_map.fast_get_state(identity_key)
1757
+ if not state:
1758
+ continue
1759
+
1760
+ evaluated_keys = set(param).difference(pk_keys)
1761
+
1762
+ dict_ = state.dict
1763
+ # only evaluate unmodified attributes
1764
+ to_evaluate = state.unmodified.intersection(evaluated_keys)
1765
+ for key in to_evaluate:
1766
+ if key in dict_:
1767
+ dict_[key] = param[key]
1768
+
1769
+ state.manager.dispatch.refresh(state, None, to_evaluate)
1770
+
1771
+ state._commit(dict_, list(to_evaluate))
1772
+
1773
+ # attributes that were formerly modified instead get expired.
1774
+ # this only gets hit if the session had pending changes
1775
+ # and autoflush were set to False.
1776
+ to_expire = evaluated_keys.intersection(dict_).difference(
1777
+ to_evaluate
1778
+ )
1779
+ if to_expire:
1780
+ state._expire_attributes(dict_, to_expire)
1781
+
1782
+ @classmethod
1783
+ def _do_post_synchronize_evaluate(
1784
+ cls, session, statement, result, update_options
1785
+ ):
1786
+ matched_objects = cls._get_matched_objects_on_criteria(
1787
+ update_options,
1788
+ session.identity_map.all_states(),
1789
+ )
1790
+
1791
+ cls._apply_update_set_values_to_objects(
1792
+ session,
1793
+ update_options,
1794
+ statement,
1795
+ result.context.compiled_parameters[0],
1796
+ [(obj, state, dict_) for obj, state, dict_, _ in matched_objects],
1797
+ result.prefetch_cols(),
1798
+ result.postfetch_cols(),
1799
+ )
1800
+
1801
+ @classmethod
1802
+ def _do_post_synchronize_fetch(
1803
+ cls, session, statement, result, update_options
1804
+ ):
1805
+ target_mapper = update_options._subject_mapper
1806
+
1807
+ returned_defaults_rows = result.returned_defaults_rows
1808
+ if returned_defaults_rows:
1809
+ pk_rows = cls._interpret_returning_rows(
1810
+ result, target_mapper, returned_defaults_rows
1811
+ )
1812
+ matched_rows = [
1813
+ tuple(row) + (update_options._identity_token,)
1814
+ for row in pk_rows
1815
+ ]
1816
+ else:
1817
+ matched_rows = update_options._matched_rows
1818
+
1819
+ objs = [
1820
+ session.identity_map[identity_key]
1821
+ for identity_key in [
1822
+ target_mapper.identity_key_from_primary_key(
1823
+ list(primary_key),
1824
+ identity_token=identity_token,
1825
+ )
1826
+ for primary_key, identity_token in [
1827
+ (row[0:-1], row[-1]) for row in matched_rows
1828
+ ]
1829
+ if update_options._identity_token is None
1830
+ or identity_token == update_options._identity_token
1831
+ ]
1832
+ if identity_key in session.identity_map
1833
+ ]
1834
+
1835
+ if not objs:
1836
+ return
1837
+
1838
+ cls._apply_update_set_values_to_objects(
1839
+ session,
1840
+ update_options,
1841
+ statement,
1842
+ result.context.compiled_parameters[0],
1843
+ [
1844
+ (
1845
+ obj,
1846
+ attributes.instance_state(obj),
1847
+ attributes.instance_dict(obj),
1848
+ )
1849
+ for obj in objs
1850
+ ],
1851
+ result.prefetch_cols(),
1852
+ result.postfetch_cols(),
1853
+ )
1854
+
1855
+ @classmethod
1856
+ def _apply_update_set_values_to_objects(
1857
+ cls,
1858
+ session,
1859
+ update_options,
1860
+ statement,
1861
+ effective_params,
1862
+ matched_objects,
1863
+ prefetch_cols,
1864
+ postfetch_cols,
1865
+ ):
1866
+ """apply values to objects derived from an update statement, e.g.
1867
+ UPDATE..SET <values>
1868
+
1869
+ """
1870
+
1871
+ mapper = update_options._subject_mapper
1872
+ target_cls = mapper.class_
1873
+ evaluator_compiler = evaluator._EvaluatorCompiler(target_cls)
1874
+ resolved_values = cls._get_resolved_values(mapper, statement)
1875
+ resolved_keys_as_propnames = cls._resolved_keys_as_propnames(
1876
+ mapper, resolved_values
1877
+ )
1878
+ value_evaluators = {}
1879
+ for key, value in resolved_keys_as_propnames:
1880
+ try:
1881
+ _evaluator = evaluator_compiler.process(
1882
+ coercions.expect(roles.ExpressionElementRole, value)
1883
+ )
1884
+ except evaluator.UnevaluatableError:
1885
+ pass
1886
+ else:
1887
+ value_evaluators[key] = _evaluator
1888
+
1889
+ evaluated_keys = list(value_evaluators.keys())
1890
+ attrib = {k for k, v in resolved_keys_as_propnames}
1891
+
1892
+ states = set()
1893
+
1894
+ to_prefetch = {
1895
+ c
1896
+ for c in prefetch_cols
1897
+ if c.key in effective_params
1898
+ and c in mapper._columntoproperty
1899
+ and c.key not in evaluated_keys
1900
+ }
1901
+ to_expire = {
1902
+ mapper._columntoproperty[c].key
1903
+ for c in postfetch_cols
1904
+ if c in mapper._columntoproperty
1905
+ }.difference(evaluated_keys)
1906
+
1907
+ prefetch_transfer = [
1908
+ (mapper._columntoproperty[c].key, c.key) for c in to_prefetch
1909
+ ]
1910
+
1911
+ for obj, state, dict_ in matched_objects:
1912
+
1913
+ dict_.update(
1914
+ {
1915
+ col_to_prop: effective_params[c_key]
1916
+ for col_to_prop, c_key in prefetch_transfer
1917
+ }
1918
+ )
1919
+
1920
+ state._expire_attributes(state.dict, to_expire)
1921
+
1922
+ to_evaluate = state.unmodified.intersection(evaluated_keys)
1923
+
1924
+ for key in to_evaluate:
1925
+ if key in dict_:
1926
+ # only run eval for attributes that are present.
1927
+ dict_[key] = value_evaluators[key](obj)
1928
+
1929
+ state.manager.dispatch.refresh(state, None, to_evaluate)
1930
+
1931
+ state._commit(dict_, list(to_evaluate))
1932
+
1933
+ # attributes that were formerly modified instead get expired.
1934
+ # this only gets hit if the session had pending changes
1935
+ # and autoflush were set to False.
1936
+ to_expire = attrib.intersection(dict_).difference(to_evaluate)
1937
+ if to_expire:
1938
+ state._expire_attributes(dict_, to_expire)
1939
+
1940
+ states.add(state)
1941
+ session._register_altered(states)
1942
+
1943
+
1944
+ @CompileState.plugin_for("orm", "delete")
1945
+ class _BulkORMDelete(_BulkUDCompileState, DeleteDMLState):
1946
+ @classmethod
1947
+ def create_for_statement(cls, statement, compiler, **kw):
1948
+ self = cls.__new__(cls)
1949
+
1950
+ dml_strategy = statement._annotations.get(
1951
+ "dml_strategy", "unspecified"
1952
+ )
1953
+
1954
+ if (
1955
+ dml_strategy == "core_only"
1956
+ or dml_strategy == "unspecified"
1957
+ and "parententity" not in statement.table._annotations
1958
+ ):
1959
+ DeleteDMLState.__init__(self, statement, compiler, **kw)
1960
+ return self
1961
+
1962
+ toplevel = not compiler.stack
1963
+
1964
+ orm_level_statement = statement
1965
+
1966
+ ext_info = statement.table._annotations["parententity"]
1967
+ self.mapper = mapper = ext_info.mapper
1968
+
1969
+ self._init_global_attributes(
1970
+ statement,
1971
+ compiler,
1972
+ toplevel=toplevel,
1973
+ process_criteria_for_toplevel=toplevel,
1974
+ )
1975
+
1976
+ new_stmt = statement._clone()
1977
+
1978
+ if new_stmt.table._annotations["parententity"] is mapper:
1979
+ new_stmt.table = mapper.local_table
1980
+
1981
+ new_crit = cls._adjust_for_extra_criteria(
1982
+ self.global_attributes, mapper
1983
+ )
1984
+ if new_crit:
1985
+ new_stmt = new_stmt.where(*new_crit)
1986
+
1987
+ # do this first as we need to determine if there is
1988
+ # DELETE..FROM
1989
+ DeleteDMLState.__init__(self, new_stmt, compiler, **kw)
1990
+
1991
+ use_supplemental_cols = False
1992
+
1993
+ if not toplevel:
1994
+ synchronize_session = None
1995
+ else:
1996
+ synchronize_session = compiler._annotations.get(
1997
+ "synchronize_session", None
1998
+ )
1999
+ can_use_returning = compiler._annotations.get(
2000
+ "can_use_returning", None
2001
+ )
2002
+ if can_use_returning is not False:
2003
+ # even though pre_exec has determined basic
2004
+ # can_use_returning for the dialect, if we are to use
2005
+ # RETURNING we need to run can_use_returning() at this level
2006
+ # unconditionally because is_delete_using was not known
2007
+ # at the pre_exec level
2008
+ can_use_returning = (
2009
+ synchronize_session == "fetch"
2010
+ and self.can_use_returning(
2011
+ compiler.dialect,
2012
+ mapper,
2013
+ is_multitable=self.is_multitable,
2014
+ is_delete_using=compiler._annotations.get(
2015
+ "is_delete_using", False
2016
+ ),
2017
+ )
2018
+ )
2019
+
2020
+ if can_use_returning:
2021
+ use_supplemental_cols = True
2022
+
2023
+ new_stmt = new_stmt.return_defaults(*new_stmt.table.primary_key)
2024
+
2025
+ if toplevel:
2026
+ new_stmt = self._setup_orm_returning(
2027
+ compiler,
2028
+ orm_level_statement,
2029
+ new_stmt,
2030
+ dml_mapper=mapper,
2031
+ use_supplemental_cols=use_supplemental_cols,
2032
+ )
2033
+
2034
+ self.statement = new_stmt
2035
+
2036
+ return self
2037
+
2038
+ @classmethod
2039
+ def orm_execute_statement(
2040
+ cls,
2041
+ session: Session,
2042
+ statement: dml.Delete,
2043
+ params: _CoreAnyExecuteParams,
2044
+ execution_options: OrmExecuteOptionsParameter,
2045
+ bind_arguments: _BindArguments,
2046
+ conn: Connection,
2047
+ ) -> _result.Result:
2048
+ update_options = execution_options.get(
2049
+ "_sa_orm_update_options", cls.default_update_options
2050
+ )
2051
+
2052
+ if update_options._dml_strategy == "bulk":
2053
+ raise sa_exc.InvalidRequestError(
2054
+ "Bulk ORM DELETE not supported right now. "
2055
+ "Statement may be invoked at the "
2056
+ "Core level using "
2057
+ "session.connection().execute(stmt, parameters)"
2058
+ )
2059
+
2060
+ if update_options._dml_strategy not in ("orm", "auto", "core_only"):
2061
+ raise sa_exc.ArgumentError(
2062
+ "Valid strategies for ORM DELETE strategy are 'orm', 'auto', "
2063
+ "'core_only'"
2064
+ )
2065
+
2066
+ return super().orm_execute_statement(
2067
+ session, statement, params, execution_options, bind_arguments, conn
2068
+ )
2069
+
2070
+ @classmethod
2071
+ def can_use_returning(
2072
+ cls,
2073
+ dialect: Dialect,
2074
+ mapper: Mapper[Any],
2075
+ *,
2076
+ is_multitable: bool = False,
2077
+ is_update_from: bool = False,
2078
+ is_delete_using: bool = False,
2079
+ is_executemany: bool = False,
2080
+ ) -> bool:
2081
+ # normal answer for "should we use RETURNING" at all.
2082
+ normal_answer = (
2083
+ dialect.delete_returning and mapper.local_table.implicit_returning
2084
+ )
2085
+ if not normal_answer:
2086
+ return False
2087
+
2088
+ # now get into special workarounds because MariaDB supports
2089
+ # DELETE...RETURNING but not DELETE...USING...RETURNING.
2090
+ if is_delete_using:
2091
+ # is_delete_using hint was passed. use
2092
+ # additional dialect feature (True for PG, False for MariaDB)
2093
+ return dialect.delete_returning_multifrom
2094
+
2095
+ elif is_multitable and not dialect.delete_returning_multifrom:
2096
+ # is_delete_using hint was not passed, but we determined
2097
+ # at compile time that this is in fact a DELETE..USING.
2098
+ # it's too late to continue since we did not pre-SELECT.
2099
+ # raise that we need that hint up front.
2100
+
2101
+ raise sa_exc.CompileError(
2102
+ f'Dialect "{dialect.name}" does not support RETURNING '
2103
+ "with DELETE..USING; for synchronize_session='fetch', "
2104
+ "please add the additional execution option "
2105
+ "'is_delete_using=True' to the statement to indicate that "
2106
+ "a separate SELECT should be used for this backend."
2107
+ )
2108
+
2109
+ return True
2110
+
2111
+ @classmethod
2112
+ def _do_post_synchronize_evaluate(
2113
+ cls, session, statement, result, update_options
2114
+ ):
2115
+ matched_objects = cls._get_matched_objects_on_criteria(
2116
+ update_options,
2117
+ session.identity_map.all_states(),
2118
+ )
2119
+
2120
+ to_delete = []
2121
+
2122
+ for _, state, dict_, is_partially_expired in matched_objects:
2123
+ if is_partially_expired:
2124
+ state._expire(dict_, session.identity_map._modified)
2125
+ else:
2126
+ to_delete.append(state)
2127
+
2128
+ if to_delete:
2129
+ session._remove_newly_deleted(to_delete)
2130
+
2131
+ @classmethod
2132
+ def _do_post_synchronize_fetch(
2133
+ cls, session, statement, result, update_options
2134
+ ):
2135
+ target_mapper = update_options._subject_mapper
2136
+
2137
+ returned_defaults_rows = result.returned_defaults_rows
2138
+
2139
+ if returned_defaults_rows:
2140
+ pk_rows = cls._interpret_returning_rows(
2141
+ result, target_mapper, returned_defaults_rows
2142
+ )
2143
+
2144
+ matched_rows = [
2145
+ tuple(row) + (update_options._identity_token,)
2146
+ for row in pk_rows
2147
+ ]
2148
+ else:
2149
+ matched_rows = update_options._matched_rows
2150
+
2151
+ for row in matched_rows:
2152
+ primary_key = row[0:-1]
2153
+ identity_token = row[-1]
2154
+
2155
+ # TODO: inline this and call remove_newly_deleted
2156
+ # once
2157
+ identity_key = target_mapper.identity_key_from_primary_key(
2158
+ list(primary_key),
2159
+ identity_token=identity_token,
2160
+ )
2161
+ if identity_key in session.identity_map:
2162
+ session._remove_newly_deleted(
2163
+ [
2164
+ attributes.instance_state(
2165
+ session.identity_map[identity_key]
2166
+ )
2167
+ ]
2168
+ )