SQLAlchemy 2.0.47__cp313-cp313t-win32.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 (274) hide show
  1. sqlalchemy/__init__.py +283 -0
  2. sqlalchemy/connectors/__init__.py +18 -0
  3. sqlalchemy/connectors/aioodbc.py +184 -0
  4. sqlalchemy/connectors/asyncio.py +429 -0
  5. sqlalchemy/connectors/pyodbc.py +250 -0
  6. sqlalchemy/cyextension/__init__.py +6 -0
  7. sqlalchemy/cyextension/collections.cp313t-win32.pyd +0 -0
  8. sqlalchemy/cyextension/collections.pyx +409 -0
  9. sqlalchemy/cyextension/immutabledict.cp313t-win32.pyd +0 -0
  10. sqlalchemy/cyextension/immutabledict.pxd +8 -0
  11. sqlalchemy/cyextension/immutabledict.pyx +133 -0
  12. sqlalchemy/cyextension/processors.cp313t-win32.pyd +0 -0
  13. sqlalchemy/cyextension/processors.pyx +68 -0
  14. sqlalchemy/cyextension/resultproxy.cp313t-win32.pyd +0 -0
  15. sqlalchemy/cyextension/resultproxy.pyx +102 -0
  16. sqlalchemy/cyextension/util.cp313t-win32.pyd +0 -0
  17. sqlalchemy/cyextension/util.pyx +90 -0
  18. sqlalchemy/dialects/__init__.py +62 -0
  19. sqlalchemy/dialects/_typing.py +30 -0
  20. sqlalchemy/dialects/mssql/__init__.py +88 -0
  21. sqlalchemy/dialects/mssql/aioodbc.py +63 -0
  22. sqlalchemy/dialects/mssql/base.py +4093 -0
  23. sqlalchemy/dialects/mssql/information_schema.py +285 -0
  24. sqlalchemy/dialects/mssql/json.py +129 -0
  25. sqlalchemy/dialects/mssql/provision.py +185 -0
  26. sqlalchemy/dialects/mssql/pymssql.py +126 -0
  27. sqlalchemy/dialects/mssql/pyodbc.py +760 -0
  28. sqlalchemy/dialects/mysql/__init__.py +104 -0
  29. sqlalchemy/dialects/mysql/aiomysql.py +250 -0
  30. sqlalchemy/dialects/mysql/asyncmy.py +231 -0
  31. sqlalchemy/dialects/mysql/base.py +3949 -0
  32. sqlalchemy/dialects/mysql/cymysql.py +106 -0
  33. sqlalchemy/dialects/mysql/dml.py +225 -0
  34. sqlalchemy/dialects/mysql/enumerated.py +282 -0
  35. sqlalchemy/dialects/mysql/expression.py +146 -0
  36. sqlalchemy/dialects/mysql/json.py +91 -0
  37. sqlalchemy/dialects/mysql/mariadb.py +72 -0
  38. sqlalchemy/dialects/mysql/mariadbconnector.py +322 -0
  39. sqlalchemy/dialects/mysql/mysqlconnector.py +302 -0
  40. sqlalchemy/dialects/mysql/mysqldb.py +314 -0
  41. sqlalchemy/dialects/mysql/provision.py +153 -0
  42. sqlalchemy/dialects/mysql/pymysql.py +158 -0
  43. sqlalchemy/dialects/mysql/pyodbc.py +157 -0
  44. sqlalchemy/dialects/mysql/reflection.py +727 -0
  45. sqlalchemy/dialects/mysql/reserved_words.py +570 -0
  46. sqlalchemy/dialects/mysql/types.py +835 -0
  47. sqlalchemy/dialects/oracle/__init__.py +81 -0
  48. sqlalchemy/dialects/oracle/base.py +3802 -0
  49. sqlalchemy/dialects/oracle/cx_oracle.py +1555 -0
  50. sqlalchemy/dialects/oracle/dictionary.py +507 -0
  51. sqlalchemy/dialects/oracle/oracledb.py +941 -0
  52. sqlalchemy/dialects/oracle/provision.py +297 -0
  53. sqlalchemy/dialects/oracle/types.py +316 -0
  54. sqlalchemy/dialects/oracle/vector.py +365 -0
  55. sqlalchemy/dialects/postgresql/__init__.py +167 -0
  56. sqlalchemy/dialects/postgresql/_psycopg_common.py +189 -0
  57. sqlalchemy/dialects/postgresql/array.py +519 -0
  58. sqlalchemy/dialects/postgresql/asyncpg.py +1284 -0
  59. sqlalchemy/dialects/postgresql/base.py +5378 -0
  60. sqlalchemy/dialects/postgresql/dml.py +339 -0
  61. sqlalchemy/dialects/postgresql/ext.py +540 -0
  62. sqlalchemy/dialects/postgresql/hstore.py +406 -0
  63. sqlalchemy/dialects/postgresql/json.py +404 -0
  64. sqlalchemy/dialects/postgresql/named_types.py +524 -0
  65. sqlalchemy/dialects/postgresql/operators.py +129 -0
  66. sqlalchemy/dialects/postgresql/pg8000.py +669 -0
  67. sqlalchemy/dialects/postgresql/pg_catalog.py +326 -0
  68. sqlalchemy/dialects/postgresql/provision.py +183 -0
  69. sqlalchemy/dialects/postgresql/psycopg.py +862 -0
  70. sqlalchemy/dialects/postgresql/psycopg2.py +892 -0
  71. sqlalchemy/dialects/postgresql/psycopg2cffi.py +61 -0
  72. sqlalchemy/dialects/postgresql/ranges.py +1031 -0
  73. sqlalchemy/dialects/postgresql/types.py +313 -0
  74. sqlalchemy/dialects/sqlite/__init__.py +57 -0
  75. sqlalchemy/dialects/sqlite/aiosqlite.py +482 -0
  76. sqlalchemy/dialects/sqlite/base.py +3056 -0
  77. sqlalchemy/dialects/sqlite/dml.py +263 -0
  78. sqlalchemy/dialects/sqlite/json.py +92 -0
  79. sqlalchemy/dialects/sqlite/provision.py +229 -0
  80. sqlalchemy/dialects/sqlite/pysqlcipher.py +157 -0
  81. sqlalchemy/dialects/sqlite/pysqlite.py +756 -0
  82. sqlalchemy/dialects/type_migration_guidelines.txt +145 -0
  83. sqlalchemy/engine/__init__.py +62 -0
  84. sqlalchemy/engine/_py_processors.py +136 -0
  85. sqlalchemy/engine/_py_row.py +128 -0
  86. sqlalchemy/engine/_py_util.py +74 -0
  87. sqlalchemy/engine/base.py +3390 -0
  88. sqlalchemy/engine/characteristics.py +155 -0
  89. sqlalchemy/engine/create.py +893 -0
  90. sqlalchemy/engine/cursor.py +2298 -0
  91. sqlalchemy/engine/default.py +2394 -0
  92. sqlalchemy/engine/events.py +965 -0
  93. sqlalchemy/engine/interfaces.py +3471 -0
  94. sqlalchemy/engine/mock.py +134 -0
  95. sqlalchemy/engine/processors.py +61 -0
  96. sqlalchemy/engine/reflection.py +2102 -0
  97. sqlalchemy/engine/result.py +2399 -0
  98. sqlalchemy/engine/row.py +400 -0
  99. sqlalchemy/engine/strategies.py +16 -0
  100. sqlalchemy/engine/url.py +924 -0
  101. sqlalchemy/engine/util.py +167 -0
  102. sqlalchemy/event/__init__.py +26 -0
  103. sqlalchemy/event/api.py +220 -0
  104. sqlalchemy/event/attr.py +676 -0
  105. sqlalchemy/event/base.py +472 -0
  106. sqlalchemy/event/legacy.py +258 -0
  107. sqlalchemy/event/registry.py +390 -0
  108. sqlalchemy/events.py +17 -0
  109. sqlalchemy/exc.py +832 -0
  110. sqlalchemy/ext/__init__.py +11 -0
  111. sqlalchemy/ext/associationproxy.py +2027 -0
  112. sqlalchemy/ext/asyncio/__init__.py +25 -0
  113. sqlalchemy/ext/asyncio/base.py +281 -0
  114. sqlalchemy/ext/asyncio/engine.py +1471 -0
  115. sqlalchemy/ext/asyncio/exc.py +21 -0
  116. sqlalchemy/ext/asyncio/result.py +965 -0
  117. sqlalchemy/ext/asyncio/scoping.py +1599 -0
  118. sqlalchemy/ext/asyncio/session.py +1947 -0
  119. sqlalchemy/ext/automap.py +1701 -0
  120. sqlalchemy/ext/baked.py +570 -0
  121. sqlalchemy/ext/compiler.py +600 -0
  122. sqlalchemy/ext/declarative/__init__.py +65 -0
  123. sqlalchemy/ext/declarative/extensions.py +564 -0
  124. sqlalchemy/ext/horizontal_shard.py +478 -0
  125. sqlalchemy/ext/hybrid.py +1535 -0
  126. sqlalchemy/ext/indexable.py +364 -0
  127. sqlalchemy/ext/instrumentation.py +450 -0
  128. sqlalchemy/ext/mutable.py +1085 -0
  129. sqlalchemy/ext/mypy/__init__.py +6 -0
  130. sqlalchemy/ext/mypy/apply.py +324 -0
  131. sqlalchemy/ext/mypy/decl_class.py +515 -0
  132. sqlalchemy/ext/mypy/infer.py +590 -0
  133. sqlalchemy/ext/mypy/names.py +335 -0
  134. sqlalchemy/ext/mypy/plugin.py +303 -0
  135. sqlalchemy/ext/mypy/util.py +357 -0
  136. sqlalchemy/ext/orderinglist.py +439 -0
  137. sqlalchemy/ext/serializer.py +185 -0
  138. sqlalchemy/future/__init__.py +16 -0
  139. sqlalchemy/future/engine.py +15 -0
  140. sqlalchemy/inspection.py +174 -0
  141. sqlalchemy/log.py +288 -0
  142. sqlalchemy/orm/__init__.py +171 -0
  143. sqlalchemy/orm/_orm_constructors.py +2661 -0
  144. sqlalchemy/orm/_typing.py +179 -0
  145. sqlalchemy/orm/attributes.py +2845 -0
  146. sqlalchemy/orm/base.py +971 -0
  147. sqlalchemy/orm/bulk_persistence.py +2135 -0
  148. sqlalchemy/orm/clsregistry.py +571 -0
  149. sqlalchemy/orm/collections.py +1627 -0
  150. sqlalchemy/orm/context.py +3334 -0
  151. sqlalchemy/orm/decl_api.py +2004 -0
  152. sqlalchemy/orm/decl_base.py +2192 -0
  153. sqlalchemy/orm/dependency.py +1302 -0
  154. sqlalchemy/orm/descriptor_props.py +1092 -0
  155. sqlalchemy/orm/dynamic.py +300 -0
  156. sqlalchemy/orm/evaluator.py +379 -0
  157. sqlalchemy/orm/events.py +3252 -0
  158. sqlalchemy/orm/exc.py +237 -0
  159. sqlalchemy/orm/identity.py +302 -0
  160. sqlalchemy/orm/instrumentation.py +754 -0
  161. sqlalchemy/orm/interfaces.py +1496 -0
  162. sqlalchemy/orm/loading.py +1686 -0
  163. sqlalchemy/orm/mapped_collection.py +557 -0
  164. sqlalchemy/orm/mapper.py +4444 -0
  165. sqlalchemy/orm/path_registry.py +809 -0
  166. sqlalchemy/orm/persistence.py +1788 -0
  167. sqlalchemy/orm/properties.py +935 -0
  168. sqlalchemy/orm/query.py +3459 -0
  169. sqlalchemy/orm/relationships.py +3508 -0
  170. sqlalchemy/orm/scoping.py +2148 -0
  171. sqlalchemy/orm/session.py +5280 -0
  172. sqlalchemy/orm/state.py +1168 -0
  173. sqlalchemy/orm/state_changes.py +196 -0
  174. sqlalchemy/orm/strategies.py +3470 -0
  175. sqlalchemy/orm/strategy_options.py +2568 -0
  176. sqlalchemy/orm/sync.py +164 -0
  177. sqlalchemy/orm/unitofwork.py +796 -0
  178. sqlalchemy/orm/util.py +2403 -0
  179. sqlalchemy/orm/writeonly.py +674 -0
  180. sqlalchemy/pool/__init__.py +44 -0
  181. sqlalchemy/pool/base.py +1524 -0
  182. sqlalchemy/pool/events.py +375 -0
  183. sqlalchemy/pool/impl.py +588 -0
  184. sqlalchemy/py.typed +0 -0
  185. sqlalchemy/schema.py +69 -0
  186. sqlalchemy/sql/__init__.py +145 -0
  187. sqlalchemy/sql/_dml_constructors.py +132 -0
  188. sqlalchemy/sql/_elements_constructors.py +1872 -0
  189. sqlalchemy/sql/_orm_types.py +20 -0
  190. sqlalchemy/sql/_py_util.py +75 -0
  191. sqlalchemy/sql/_selectable_constructors.py +763 -0
  192. sqlalchemy/sql/_typing.py +482 -0
  193. sqlalchemy/sql/annotation.py +587 -0
  194. sqlalchemy/sql/base.py +2293 -0
  195. sqlalchemy/sql/cache_key.py +1057 -0
  196. sqlalchemy/sql/coercions.py +1404 -0
  197. sqlalchemy/sql/compiler.py +8081 -0
  198. sqlalchemy/sql/crud.py +1752 -0
  199. sqlalchemy/sql/ddl.py +1444 -0
  200. sqlalchemy/sql/default_comparator.py +551 -0
  201. sqlalchemy/sql/dml.py +1850 -0
  202. sqlalchemy/sql/elements.py +5589 -0
  203. sqlalchemy/sql/events.py +458 -0
  204. sqlalchemy/sql/expression.py +159 -0
  205. sqlalchemy/sql/functions.py +2158 -0
  206. sqlalchemy/sql/lambdas.py +1442 -0
  207. sqlalchemy/sql/naming.py +209 -0
  208. sqlalchemy/sql/operators.py +2623 -0
  209. sqlalchemy/sql/roles.py +323 -0
  210. sqlalchemy/sql/schema.py +6222 -0
  211. sqlalchemy/sql/selectable.py +7265 -0
  212. sqlalchemy/sql/sqltypes.py +3930 -0
  213. sqlalchemy/sql/traversals.py +1024 -0
  214. sqlalchemy/sql/type_api.py +2368 -0
  215. sqlalchemy/sql/util.py +1485 -0
  216. sqlalchemy/sql/visitors.py +1164 -0
  217. sqlalchemy/testing/__init__.py +96 -0
  218. sqlalchemy/testing/assertions.py +994 -0
  219. sqlalchemy/testing/assertsql.py +520 -0
  220. sqlalchemy/testing/asyncio.py +135 -0
  221. sqlalchemy/testing/config.py +434 -0
  222. sqlalchemy/testing/engines.py +483 -0
  223. sqlalchemy/testing/entities.py +117 -0
  224. sqlalchemy/testing/exclusions.py +476 -0
  225. sqlalchemy/testing/fixtures/__init__.py +28 -0
  226. sqlalchemy/testing/fixtures/base.py +384 -0
  227. sqlalchemy/testing/fixtures/mypy.py +332 -0
  228. sqlalchemy/testing/fixtures/orm.py +227 -0
  229. sqlalchemy/testing/fixtures/sql.py +482 -0
  230. sqlalchemy/testing/pickleable.py +155 -0
  231. sqlalchemy/testing/plugin/__init__.py +6 -0
  232. sqlalchemy/testing/plugin/bootstrap.py +51 -0
  233. sqlalchemy/testing/plugin/plugin_base.py +828 -0
  234. sqlalchemy/testing/plugin/pytestplugin.py +892 -0
  235. sqlalchemy/testing/profiling.py +329 -0
  236. sqlalchemy/testing/provision.py +603 -0
  237. sqlalchemy/testing/requirements.py +1945 -0
  238. sqlalchemy/testing/schema.py +198 -0
  239. sqlalchemy/testing/suite/__init__.py +19 -0
  240. sqlalchemy/testing/suite/test_cte.py +237 -0
  241. sqlalchemy/testing/suite/test_ddl.py +389 -0
  242. sqlalchemy/testing/suite/test_deprecations.py +153 -0
  243. sqlalchemy/testing/suite/test_dialect.py +776 -0
  244. sqlalchemy/testing/suite/test_insert.py +630 -0
  245. sqlalchemy/testing/suite/test_reflection.py +3557 -0
  246. sqlalchemy/testing/suite/test_results.py +504 -0
  247. sqlalchemy/testing/suite/test_rowcount.py +258 -0
  248. sqlalchemy/testing/suite/test_select.py +2010 -0
  249. sqlalchemy/testing/suite/test_sequence.py +317 -0
  250. sqlalchemy/testing/suite/test_types.py +2147 -0
  251. sqlalchemy/testing/suite/test_unicode_ddl.py +189 -0
  252. sqlalchemy/testing/suite/test_update_delete.py +139 -0
  253. sqlalchemy/testing/util.py +535 -0
  254. sqlalchemy/testing/warnings.py +52 -0
  255. sqlalchemy/types.py +74 -0
  256. sqlalchemy/util/__init__.py +162 -0
  257. sqlalchemy/util/_collections.py +712 -0
  258. sqlalchemy/util/_concurrency_py3k.py +288 -0
  259. sqlalchemy/util/_has_cy.py +40 -0
  260. sqlalchemy/util/_py_collections.py +541 -0
  261. sqlalchemy/util/compat.py +421 -0
  262. sqlalchemy/util/concurrency.py +110 -0
  263. sqlalchemy/util/deprecations.py +401 -0
  264. sqlalchemy/util/langhelpers.py +2203 -0
  265. sqlalchemy/util/preloaded.py +150 -0
  266. sqlalchemy/util/queue.py +322 -0
  267. sqlalchemy/util/tool_support.py +201 -0
  268. sqlalchemy/util/topological.py +120 -0
  269. sqlalchemy/util/typing.py +734 -0
  270. sqlalchemy-2.0.47.dist-info/METADATA +243 -0
  271. sqlalchemy-2.0.47.dist-info/RECORD +274 -0
  272. sqlalchemy-2.0.47.dist-info/WHEEL +5 -0
  273. sqlalchemy-2.0.47.dist-info/licenses/LICENSE +19 -0
  274. sqlalchemy-2.0.47.dist-info/top_level.txt +1 -0
@@ -0,0 +1,478 @@
1
+ # ext/horizontal_shard.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
+
8
+ """Horizontal sharding support.
9
+
10
+ Defines a rudimental 'horizontal sharding' system which allows a Session to
11
+ distribute queries and persistence operations across multiple databases.
12
+
13
+ For a usage example, see the :ref:`examples_sharding` example included in
14
+ the source distribution.
15
+
16
+ .. deepalchemy:: The horizontal sharding extension is an advanced feature,
17
+ involving a complex statement -> database interaction as well as
18
+ use of semi-public APIs for non-trivial cases. Simpler approaches to
19
+ referring to multiple database "shards", most commonly using a distinct
20
+ :class:`_orm.Session` per "shard", should always be considered first
21
+ before using this more complex and less-production-tested system.
22
+
23
+
24
+
25
+ """
26
+ from __future__ import annotations
27
+
28
+ from typing import Any
29
+ from typing import Callable
30
+ from typing import Dict
31
+ from typing import Iterable
32
+ from typing import Optional
33
+ from typing import Tuple
34
+ from typing import Type
35
+ from typing import TYPE_CHECKING
36
+ from typing import TypeVar
37
+ from typing import Union
38
+
39
+ from .. import event
40
+ from .. import exc
41
+ from .. import inspect
42
+ from .. import util
43
+ from ..orm import PassiveFlag
44
+ from ..orm._typing import OrmExecuteOptionsParameter
45
+ from ..orm.interfaces import ORMOption
46
+ from ..orm.mapper import Mapper
47
+ from ..orm.query import Query
48
+ from ..orm.session import _BindArguments
49
+ from ..orm.session import _PKIdentityArgument
50
+ from ..orm.session import Session
51
+ from ..util.typing import Protocol
52
+ from ..util.typing import Self
53
+
54
+ if TYPE_CHECKING:
55
+ from ..engine.base import Connection
56
+ from ..engine.base import Engine
57
+ from ..engine.base import OptionEngine
58
+ from ..engine.result import IteratorResult
59
+ from ..engine.result import Result
60
+ from ..orm import LoaderCallableStatus
61
+ from ..orm._typing import _O
62
+ from ..orm.bulk_persistence import BulkUDCompileState
63
+ from ..orm.context import QueryContext
64
+ from ..orm.session import _EntityBindKey
65
+ from ..orm.session import _SessionBind
66
+ from ..orm.session import ORMExecuteState
67
+ from ..orm.state import InstanceState
68
+ from ..sql import Executable
69
+ from ..sql._typing import _TP
70
+ from ..sql.elements import ClauseElement
71
+
72
+ __all__ = ["ShardedSession", "ShardedQuery"]
73
+
74
+ _T = TypeVar("_T", bound=Any)
75
+
76
+
77
+ ShardIdentifier = str
78
+
79
+
80
+ class ShardChooser(Protocol):
81
+ def __call__(
82
+ self,
83
+ mapper: Optional[Mapper[_T]],
84
+ instance: Any,
85
+ clause: Optional[ClauseElement],
86
+ ) -> Any: ...
87
+
88
+
89
+ class IdentityChooser(Protocol):
90
+ def __call__(
91
+ self,
92
+ mapper: Mapper[_T],
93
+ primary_key: _PKIdentityArgument,
94
+ *,
95
+ lazy_loaded_from: Optional[InstanceState[Any]],
96
+ execution_options: OrmExecuteOptionsParameter,
97
+ bind_arguments: _BindArguments,
98
+ **kw: Any,
99
+ ) -> Any: ...
100
+
101
+
102
+ class ShardedQuery(Query[_T]):
103
+ """Query class used with :class:`.ShardedSession`.
104
+
105
+ .. legacy:: The :class:`.ShardedQuery` is a subclass of the legacy
106
+ :class:`.Query` class. The :class:`.ShardedSession` now supports
107
+ 2.0 style execution via the :meth:`.ShardedSession.execute` method.
108
+
109
+ """
110
+
111
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
112
+ super().__init__(*args, **kwargs)
113
+ assert isinstance(self.session, ShardedSession)
114
+
115
+ self.identity_chooser = self.session.identity_chooser
116
+ self.execute_chooser = self.session.execute_chooser
117
+ self._shard_id = None
118
+
119
+ def set_shard(self, shard_id: ShardIdentifier) -> Self:
120
+ """Return a new query, limited to a single shard ID.
121
+
122
+ All subsequent operations with the returned query will
123
+ be against the single shard regardless of other state.
124
+
125
+ The shard_id can be passed for a 2.0 style execution to the
126
+ bind_arguments dictionary of :meth:`.Session.execute`::
127
+
128
+ results = session.execute(stmt, bind_arguments={"shard_id": "my_shard"})
129
+
130
+ """ # noqa: E501
131
+ return self.execution_options(_sa_shard_id=shard_id)
132
+
133
+
134
+ class ShardedSession(Session):
135
+ shard_chooser: ShardChooser
136
+ identity_chooser: IdentityChooser
137
+ execute_chooser: Callable[[ORMExecuteState], Iterable[Any]]
138
+
139
+ def __init__(
140
+ self,
141
+ shard_chooser: ShardChooser,
142
+ identity_chooser: Optional[IdentityChooser] = None,
143
+ execute_chooser: Optional[
144
+ Callable[[ORMExecuteState], Iterable[Any]]
145
+ ] = None,
146
+ shards: Optional[Dict[str, Any]] = None,
147
+ query_cls: Type[Query[_T]] = ShardedQuery,
148
+ *,
149
+ id_chooser: Optional[
150
+ Callable[[Query[_T], Iterable[_T]], Iterable[Any]]
151
+ ] = None,
152
+ query_chooser: Optional[Callable[[Executable], Iterable[Any]]] = None,
153
+ **kwargs: Any,
154
+ ) -> None:
155
+ """Construct a ShardedSession.
156
+
157
+ :param shard_chooser: A callable which, passed a Mapper, a mapped
158
+ instance, and possibly a SQL clause, returns a shard ID. This id
159
+ may be based off of the attributes present within the object, or on
160
+ some round-robin scheme. If the scheme is based on a selection, it
161
+ should set whatever state on the instance to mark it in the future as
162
+ participating in that shard.
163
+
164
+ :param identity_chooser: A callable, passed a Mapper and primary key
165
+ argument, which should return a list of shard ids where this
166
+ primary key might reside.
167
+
168
+ .. versionchanged:: 2.0 The ``identity_chooser`` parameter
169
+ supersedes the ``id_chooser`` parameter.
170
+
171
+ :param execute_chooser: For a given :class:`.ORMExecuteState`,
172
+ returns the list of shard_ids
173
+ where the query should be issued. Results from all shards returned
174
+ will be combined together into a single listing.
175
+
176
+ .. versionchanged:: 1.4 The ``execute_chooser`` parameter
177
+ supersedes the ``query_chooser`` parameter.
178
+
179
+ :param shards: A dictionary of string shard names
180
+ to :class:`~sqlalchemy.engine.Engine` objects.
181
+
182
+ """
183
+ super().__init__(query_cls=query_cls, **kwargs)
184
+
185
+ event.listen(
186
+ self, "do_orm_execute", execute_and_instances, retval=True
187
+ )
188
+ self.shard_chooser = shard_chooser
189
+
190
+ if id_chooser:
191
+ _id_chooser = id_chooser
192
+ util.warn_deprecated(
193
+ "The ``id_chooser`` parameter is deprecated; "
194
+ "please use ``identity_chooser``.",
195
+ "2.0",
196
+ )
197
+
198
+ def _legacy_identity_chooser(
199
+ mapper: Mapper[_T],
200
+ primary_key: _PKIdentityArgument,
201
+ *,
202
+ lazy_loaded_from: Optional[InstanceState[Any]],
203
+ execution_options: OrmExecuteOptionsParameter,
204
+ bind_arguments: _BindArguments,
205
+ **kw: Any,
206
+ ) -> Any:
207
+ q = self.query(mapper)
208
+ if lazy_loaded_from:
209
+ q = q._set_lazyload_from(lazy_loaded_from)
210
+ return _id_chooser(q, primary_key)
211
+
212
+ self.identity_chooser = _legacy_identity_chooser
213
+ elif identity_chooser:
214
+ self.identity_chooser = identity_chooser
215
+ else:
216
+ raise exc.ArgumentError(
217
+ "identity_chooser or id_chooser is required"
218
+ )
219
+
220
+ if query_chooser:
221
+ _query_chooser = query_chooser
222
+ util.warn_deprecated(
223
+ "The ``query_chooser`` parameter is deprecated; "
224
+ "please use ``execute_chooser``.",
225
+ "1.4",
226
+ )
227
+ if execute_chooser:
228
+ raise exc.ArgumentError(
229
+ "Can't pass query_chooser and execute_chooser "
230
+ "at the same time."
231
+ )
232
+
233
+ def _default_execute_chooser(
234
+ orm_context: ORMExecuteState,
235
+ ) -> Iterable[Any]:
236
+ return _query_chooser(orm_context.statement)
237
+
238
+ if execute_chooser is None:
239
+ execute_chooser = _default_execute_chooser
240
+
241
+ if execute_chooser is None:
242
+ raise exc.ArgumentError(
243
+ "execute_chooser or query_chooser is required"
244
+ )
245
+ self.execute_chooser = execute_chooser
246
+ self.__shards: Dict[ShardIdentifier, _SessionBind] = {}
247
+ if shards is not None:
248
+ for k in shards:
249
+ self.bind_shard(k, shards[k])
250
+
251
+ def _identity_lookup(
252
+ self,
253
+ mapper: Mapper[_O],
254
+ primary_key_identity: Union[Any, Tuple[Any, ...]],
255
+ identity_token: Optional[Any] = None,
256
+ passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
257
+ lazy_loaded_from: Optional[InstanceState[Any]] = None,
258
+ execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
259
+ bind_arguments: Optional[_BindArguments] = None,
260
+ **kw: Any,
261
+ ) -> Union[Optional[_O], LoaderCallableStatus]:
262
+ """override the default :meth:`.Session._identity_lookup` method so
263
+ that we search for a given non-token primary key identity across all
264
+ possible identity tokens (e.g. shard ids).
265
+
266
+ .. versionchanged:: 1.4 Moved :meth:`.Session._identity_lookup` from
267
+ the :class:`_query.Query` object to the :class:`.Session`.
268
+
269
+ """
270
+
271
+ if identity_token is not None:
272
+ obj = super()._identity_lookup(
273
+ mapper,
274
+ primary_key_identity,
275
+ identity_token=identity_token,
276
+ **kw,
277
+ )
278
+
279
+ return obj
280
+ else:
281
+ for shard_id in self.identity_chooser(
282
+ mapper,
283
+ primary_key_identity,
284
+ lazy_loaded_from=lazy_loaded_from,
285
+ execution_options=execution_options,
286
+ bind_arguments=dict(bind_arguments) if bind_arguments else {},
287
+ ):
288
+ obj2 = super()._identity_lookup(
289
+ mapper,
290
+ primary_key_identity,
291
+ identity_token=shard_id,
292
+ lazy_loaded_from=lazy_loaded_from,
293
+ **kw,
294
+ )
295
+ if obj2 is not None:
296
+ return obj2
297
+
298
+ return None
299
+
300
+ def _choose_shard_and_assign(
301
+ self,
302
+ mapper: Optional[_EntityBindKey[_O]],
303
+ instance: Any,
304
+ **kw: Any,
305
+ ) -> Any:
306
+ if instance is not None:
307
+ state = inspect(instance)
308
+ if state.key:
309
+ token = state.key[2]
310
+ assert token is not None
311
+ return token
312
+ elif state.identity_token:
313
+ return state.identity_token
314
+
315
+ assert isinstance(mapper, Mapper)
316
+ shard_id = self.shard_chooser(mapper, instance, **kw)
317
+ if instance is not None:
318
+ state.identity_token = shard_id
319
+ return shard_id
320
+
321
+ def connection_callable(
322
+ self,
323
+ mapper: Optional[Mapper[_T]] = None,
324
+ instance: Optional[Any] = None,
325
+ shard_id: Optional[ShardIdentifier] = None,
326
+ **kw: Any,
327
+ ) -> Connection:
328
+ """Provide a :class:`_engine.Connection` to use in the unit of work
329
+ flush process.
330
+
331
+ """
332
+
333
+ if shard_id is None:
334
+ shard_id = self._choose_shard_and_assign(mapper, instance)
335
+
336
+ if self.in_transaction():
337
+ trans = self.get_transaction()
338
+ assert trans is not None
339
+ return trans.connection(mapper, shard_id=shard_id)
340
+ else:
341
+ bind = self.get_bind(
342
+ mapper=mapper, shard_id=shard_id, instance=instance
343
+ )
344
+
345
+ if isinstance(bind, Engine):
346
+ return bind.connect(**kw)
347
+ else:
348
+ assert isinstance(bind, Connection)
349
+ return bind
350
+
351
+ def get_bind(
352
+ self,
353
+ mapper: Optional[_EntityBindKey[_O]] = None,
354
+ *,
355
+ shard_id: Optional[ShardIdentifier] = None,
356
+ instance: Optional[Any] = None,
357
+ clause: Optional[ClauseElement] = None,
358
+ **kw: Any,
359
+ ) -> _SessionBind:
360
+ if shard_id is None:
361
+ shard_id = self._choose_shard_and_assign(
362
+ mapper, instance=instance, clause=clause
363
+ )
364
+ assert shard_id is not None
365
+ return self.__shards[shard_id]
366
+
367
+ def bind_shard(
368
+ self, shard_id: ShardIdentifier, bind: Union[Engine, OptionEngine]
369
+ ) -> None:
370
+ self.__shards[shard_id] = bind
371
+
372
+
373
+ class set_shard_id(ORMOption):
374
+ """a loader option for statements to apply a specific shard id to the
375
+ primary query as well as for additional relationship and column
376
+ loaders.
377
+
378
+ The :class:`_horizontal.set_shard_id` option may be applied using
379
+ the :meth:`_sql.Executable.options` method of any executable statement::
380
+
381
+ stmt = (
382
+ select(MyObject)
383
+ .where(MyObject.name == "some name")
384
+ .options(set_shard_id("shard1"))
385
+ )
386
+
387
+ Above, the statement when invoked will limit to the "shard1" shard
388
+ identifier for the primary query as well as for all relationship and
389
+ column loading strategies, including eager loaders such as
390
+ :func:`_orm.selectinload`, deferred column loaders like :func:`_orm.defer`,
391
+ and the lazy relationship loader :func:`_orm.lazyload`.
392
+
393
+ In this way, the :class:`_horizontal.set_shard_id` option has much wider
394
+ scope than using the "shard_id" argument within the
395
+ :paramref:`_orm.Session.execute.bind_arguments` dictionary.
396
+
397
+
398
+ .. versionadded:: 2.0.0
399
+
400
+ """
401
+
402
+ __slots__ = ("shard_id", "propagate_to_loaders")
403
+
404
+ def __init__(
405
+ self, shard_id: ShardIdentifier, propagate_to_loaders: bool = True
406
+ ):
407
+ """Construct a :class:`_horizontal.set_shard_id` option.
408
+
409
+ :param shard_id: shard identifier
410
+ :param propagate_to_loaders: if left at its default of ``True``, the
411
+ shard option will take place for lazy loaders such as
412
+ :func:`_orm.lazyload` and :func:`_orm.defer`; if False, the option
413
+ will not be propagated to loaded objects. Note that :func:`_orm.defer`
414
+ always limits to the shard_id of the parent row in any case, so the
415
+ parameter only has a net effect on the behavior of the
416
+ :func:`_orm.lazyload` strategy.
417
+
418
+ """
419
+ self.shard_id = shard_id
420
+ self.propagate_to_loaders = propagate_to_loaders
421
+
422
+
423
+ def execute_and_instances(
424
+ orm_context: ORMExecuteState,
425
+ ) -> Union[Result[_T], IteratorResult[_TP]]:
426
+ active_options: Union[
427
+ None,
428
+ QueryContext.default_load_options,
429
+ Type[QueryContext.default_load_options],
430
+ BulkUDCompileState.default_update_options,
431
+ Type[BulkUDCompileState.default_update_options],
432
+ ]
433
+
434
+ if orm_context.is_select:
435
+ active_options = orm_context.load_options
436
+
437
+ elif orm_context.is_update or orm_context.is_delete:
438
+ active_options = orm_context.update_delete_options
439
+ else:
440
+ active_options = None
441
+
442
+ session = orm_context.session
443
+ assert isinstance(session, ShardedSession)
444
+
445
+ def iter_for_shard(
446
+ shard_id: ShardIdentifier,
447
+ ) -> Union[Result[_T], IteratorResult[_TP]]:
448
+ bind_arguments = dict(orm_context.bind_arguments)
449
+ bind_arguments["shard_id"] = shard_id
450
+
451
+ orm_context.update_execution_options(identity_token=shard_id)
452
+ return orm_context.invoke_statement(bind_arguments=bind_arguments)
453
+
454
+ for orm_opt in orm_context._non_compile_orm_options:
455
+ # TODO: if we had an ORMOption that gets applied at ORM statement
456
+ # execution time, that would allow this to be more generalized.
457
+ # for now just iterate and look for our options
458
+ if isinstance(orm_opt, set_shard_id):
459
+ shard_id = orm_opt.shard_id
460
+ break
461
+ else:
462
+ if active_options and active_options._identity_token is not None:
463
+ shard_id = active_options._identity_token
464
+ elif "_sa_shard_id" in orm_context.execution_options:
465
+ shard_id = orm_context.execution_options["_sa_shard_id"]
466
+ elif "shard_id" in orm_context.bind_arguments:
467
+ shard_id = orm_context.bind_arguments["shard_id"]
468
+ else:
469
+ shard_id = None
470
+
471
+ if shard_id is not None:
472
+ return iter_for_shard(shard_id)
473
+ else:
474
+ partial = []
475
+ for shard_id in session.execute_chooser(orm_context):
476
+ result_ = iter_for_shard(shard_id)
477
+ partial.append(result_)
478
+ return partial[0].merge(*partial[1:])