client-query-cache 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- client_query_cache/__init__.py +15 -0
- client_query_cache/_core/__init__.py +44 -0
- client_query_cache/_core/canonical.py +63 -0
- client_query_cache/_core/codec.py +55 -0
- client_query_cache/_core/collation.py +22 -0
- client_query_cache/_core/collection_metadata.py +65 -0
- client_query_cache/_core/entries.py +32 -0
- client_query_cache/_core/errors.py +25 -0
- client_query_cache/_core/identity_reads.py +64 -0
- client_query_cache/_core/keys.py +43 -0
- client_query_cache/_core/lifecycle.py +8 -0
- client_query_cache/_core/locking.py +40 -0
- client_query_cache/_core/lru.py +109 -0
- client_query_cache/_core/manager.py +892 -0
- client_query_cache/_core/namespace.py +35 -0
- client_query_cache/_core/order_sensitive_keys.py +69 -0
- client_query_cache/_core/projection.py +59 -0
- client_query_cache/_core/read_validation.py +89 -0
- client_query_cache/_core/snapshots.py +67 -0
- client_query_cache/_core/stream_cost.py +242 -0
- client_query_cache/_core/stream_events.py +157 -0
- client_query_cache/_core/stream_health.py +47 -0
- client_query_cache/_core/stream_options.py +14 -0
- client_query_cache/_core/unique_keys.py +119 -0
- client_query_cache/asynchronous/__init__.py +16 -0
- client_query_cache/asynchronous/collection.py +682 -0
- client_query_cache/asynchronous/database.py +59 -0
- client_query_cache/asynchronous/manager.py +118 -0
- client_query_cache/asynchronous/streams.py +300 -0
- client_query_cache/otel.py +206 -0
- client_query_cache/py.typed +0 -0
- client_query_cache/synchronous/__init__.py +16 -0
- client_query_cache/synchronous/collection.py +678 -0
- client_query_cache/synchronous/database.py +53 -0
- client_query_cache/synchronous/manager.py +118 -0
- client_query_cache/synchronous/streams.py +297 -0
- client_query_cache-0.1.0.dist-info/METADATA +114 -0
- client_query_cache-0.1.0.dist-info/RECORD +39 -0
- client_query_cache-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
7
|
+
|
|
8
|
+
from pymongo import ReadPreference
|
|
9
|
+
from pymongo.asynchronous.collection import AsyncCollection
|
|
10
|
+
from pymongo.collation import Collation
|
|
11
|
+
from pymongo.cursor import CursorType
|
|
12
|
+
from pymongo.errors import PyMongoError
|
|
13
|
+
from pymongo.read_concern import ReadConcern
|
|
14
|
+
|
|
15
|
+
from client_query_cache._core.canonical import is_canonicalizable
|
|
16
|
+
from client_query_cache._core.codec import codec_fingerprint
|
|
17
|
+
from client_query_cache._core.collection_metadata import (
|
|
18
|
+
interpret_list_collections_entry,
|
|
19
|
+
)
|
|
20
|
+
from client_query_cache._core.entries import AdmissionOutcome
|
|
21
|
+
from client_query_cache._core.errors import UnsupportedCacheRequestError
|
|
22
|
+
from client_query_cache._core.identity_reads import (
|
|
23
|
+
NO_IDENTITY,
|
|
24
|
+
extract_id_identity,
|
|
25
|
+
normalize_identity_for_cache_key,
|
|
26
|
+
)
|
|
27
|
+
from client_query_cache._core.keys import NamespaceId, canonical_alias_key
|
|
28
|
+
from client_query_cache._core.order_sensitive_keys import (
|
|
29
|
+
order_sensitive_discriminator_key,
|
|
30
|
+
)
|
|
31
|
+
from client_query_cache._core.projection import (
|
|
32
|
+
ensure_id_present_for_resolution,
|
|
33
|
+
without_id,
|
|
34
|
+
)
|
|
35
|
+
from client_query_cache._core.read_validation import (
|
|
36
|
+
is_filter_cacheable,
|
|
37
|
+
is_pipeline_cacheable,
|
|
38
|
+
is_projection_cacheable,
|
|
39
|
+
pipeline_blocks_full_materialization,
|
|
40
|
+
)
|
|
41
|
+
from client_query_cache._core.unique_keys import match_unique_key
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from pymongo.asynchronous.client_session import AsyncClientSession
|
|
45
|
+
from pymongo.asynchronous.database import AsyncDatabase
|
|
46
|
+
|
|
47
|
+
from client_query_cache._core.collection_metadata import CollectionProbeResult
|
|
48
|
+
from client_query_cache._core.keys import AliasKey
|
|
49
|
+
from client_query_cache._core.unique_keys import UniqueKeyDefinition
|
|
50
|
+
from client_query_cache.asynchronous.database import CachedDatabase
|
|
51
|
+
|
|
52
|
+
_FORCED_READ_CONCERN = ReadConcern("majority")
|
|
53
|
+
_ACCEPTABLE_READ_CONCERN_LEVELS = (None, "majority")
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
type _CollationIn = Collation | Mapping[str, Any]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _collation_document(collation: _CollationIn | None) -> Mapping[str, Any] | None:
|
|
61
|
+
if collation is None:
|
|
62
|
+
return None
|
|
63
|
+
if isinstance(collation, Collation):
|
|
64
|
+
return collation.document
|
|
65
|
+
return dict(collation)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _blocks_full_materialization(kwargs: Mapping[str, object]) -> bool:
|
|
69
|
+
try:
|
|
70
|
+
cursor_type = kwargs["cursor_type"]
|
|
71
|
+
except KeyError:
|
|
72
|
+
cursor_type = CursorType.NON_TAILABLE
|
|
73
|
+
if cursor_type != CursorType.NON_TAILABLE:
|
|
74
|
+
return True
|
|
75
|
+
try:
|
|
76
|
+
allow_partial_results = kwargs["allow_partial_results"]
|
|
77
|
+
except KeyError:
|
|
78
|
+
allow_partial_results = False
|
|
79
|
+
return bool(allow_partial_results)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _count_documents_kwargs(
|
|
83
|
+
skip: int,
|
|
84
|
+
limit: int,
|
|
85
|
+
collation: _CollationIn | None,
|
|
86
|
+
hint: str | Sequence[tuple[str, int]] | None,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
kwargs: dict[str, Any] = {}
|
|
89
|
+
if skip:
|
|
90
|
+
kwargs["skip"] = skip
|
|
91
|
+
if limit:
|
|
92
|
+
kwargs["limit"] = limit
|
|
93
|
+
if collation is not None:
|
|
94
|
+
kwargs["collation"] = collation
|
|
95
|
+
if hint is not None:
|
|
96
|
+
kwargs["hint"] = hint
|
|
97
|
+
return kwargs
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CachedCollection[DocumentType: Mapping[str, Any]]:
|
|
101
|
+
__slots__ = ("_collection", "_database", "_forced_collection", "_forced_database")
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
database: CachedDatabase[DocumentType],
|
|
106
|
+
collection: AsyncCollection[DocumentType],
|
|
107
|
+
) -> None:
|
|
108
|
+
self._database = database
|
|
109
|
+
self._collection = collection
|
|
110
|
+
self._forced_collection: AsyncCollection[DocumentType] | None = None
|
|
111
|
+
self._forced_database: AsyncDatabase[DocumentType] | None = None
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def database(self) -> CachedDatabase[DocumentType]:
|
|
115
|
+
return self._database
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def name(self) -> str:
|
|
119
|
+
return self._collection.name
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def raw(self) -> AsyncCollection[DocumentType]:
|
|
123
|
+
return self._collection
|
|
124
|
+
|
|
125
|
+
def __getattr__(self, name: str) -> Any: # noqa: ANN401
|
|
126
|
+
return self._wrap_delegated(getattr(self._collection, name))
|
|
127
|
+
|
|
128
|
+
def _wrap_delegated(self, value: Any) -> Any: # noqa: ANN401
|
|
129
|
+
if isinstance(value, AsyncCollection):
|
|
130
|
+
return CachedCollection(self._database, value)
|
|
131
|
+
if inspect.iscoroutine(value):
|
|
132
|
+
return self._await_and_wrap(value)
|
|
133
|
+
if not callable(value):
|
|
134
|
+
return value
|
|
135
|
+
|
|
136
|
+
def _delegate(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
137
|
+
return self._wrap_delegated(value(*args, **kwargs))
|
|
138
|
+
|
|
139
|
+
return _delegate
|
|
140
|
+
|
|
141
|
+
async def _await_and_wrap(self, coroutine: Any) -> Any: # noqa: ANN401
|
|
142
|
+
return self._wrap_delegated(await coroutine)
|
|
143
|
+
|
|
144
|
+
async def find_one(
|
|
145
|
+
self,
|
|
146
|
+
filter: object = None, # noqa: A002
|
|
147
|
+
projection: Mapping[str, Any] | Sequence[str] | None = None,
|
|
148
|
+
*,
|
|
149
|
+
session: AsyncClientSession | None = None,
|
|
150
|
+
**kwargs: object,
|
|
151
|
+
) -> DocumentType | None:
|
|
152
|
+
identity = extract_id_identity(filter)
|
|
153
|
+
codec_options = self._collection.codec_options
|
|
154
|
+
read_shape = order_sensitive_discriminator_key(
|
|
155
|
+
("find_one", projection, codec_fingerprint(codec_options))
|
|
156
|
+
)
|
|
157
|
+
if (
|
|
158
|
+
self._wants_bypass(session=session, kwargs=kwargs)
|
|
159
|
+
or not is_projection_cacheable(projection)
|
|
160
|
+
or not is_canonicalizable(read_shape)
|
|
161
|
+
or not await self._is_cache_eligible()
|
|
162
|
+
):
|
|
163
|
+
self._record_bypass()
|
|
164
|
+
return await self._collection.find_one(
|
|
165
|
+
filter, projection, session=session, **kwargs
|
|
166
|
+
)
|
|
167
|
+
if identity is not NO_IDENTITY:
|
|
168
|
+
if not is_canonicalizable(identity):
|
|
169
|
+
self._record_bypass()
|
|
170
|
+
return await self._collection.find_one(
|
|
171
|
+
filter, projection, session=session, **kwargs
|
|
172
|
+
)
|
|
173
|
+
return await self._find_one_by_id(identity, projection, read_shape)
|
|
174
|
+
unique_key_match = await self._match_unique_key(filter)
|
|
175
|
+
if unique_key_match is None:
|
|
176
|
+
self._record_bypass()
|
|
177
|
+
return await self._collection.find_one(
|
|
178
|
+
filter, projection, session=session, **kwargs
|
|
179
|
+
)
|
|
180
|
+
key_definition, key_values = unique_key_match
|
|
181
|
+
return await self._find_one_by_unique_key(
|
|
182
|
+
key_definition,
|
|
183
|
+
key_values,
|
|
184
|
+
cast("Mapping[str, Any]", filter),
|
|
185
|
+
projection,
|
|
186
|
+
read_shape,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async def find(
|
|
190
|
+
self,
|
|
191
|
+
filter: Mapping[str, Any] | None = None, # noqa: A002
|
|
192
|
+
projection: Mapping[str, Any] | Sequence[str] | None = None,
|
|
193
|
+
*,
|
|
194
|
+
sort: Sequence[tuple[str, int]] | None = None,
|
|
195
|
+
skip: int = 0,
|
|
196
|
+
limit: int = 0,
|
|
197
|
+
collation: _CollationIn | None = None,
|
|
198
|
+
session: AsyncClientSession | None = None,
|
|
199
|
+
**kwargs: object,
|
|
200
|
+
) -> list[DocumentType]:
|
|
201
|
+
if kwargs and _blocks_full_materialization(kwargs):
|
|
202
|
+
message = (
|
|
203
|
+
"find() always fully materializes its result and cannot support a "
|
|
204
|
+
"tailable, exhaust, or partial-result cursor; use .raw.find() instead"
|
|
205
|
+
)
|
|
206
|
+
raise UnsupportedCacheRequestError(message)
|
|
207
|
+
codec_options = self._collection.codec_options
|
|
208
|
+
discriminator = order_sensitive_discriminator_key(
|
|
209
|
+
(
|
|
210
|
+
"find",
|
|
211
|
+
filter,
|
|
212
|
+
projection,
|
|
213
|
+
sort,
|
|
214
|
+
skip,
|
|
215
|
+
limit,
|
|
216
|
+
_collation_document(collation),
|
|
217
|
+
codec_fingerprint(codec_options),
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
if (
|
|
221
|
+
self._wants_bypass(session=session, kwargs=kwargs)
|
|
222
|
+
or not is_filter_cacheable(filter)
|
|
223
|
+
or not is_projection_cacheable(projection)
|
|
224
|
+
or not is_canonicalizable(discriminator)
|
|
225
|
+
or not await self._is_cache_eligible()
|
|
226
|
+
):
|
|
227
|
+
self._record_bypass()
|
|
228
|
+
cursor = self._collection.find(
|
|
229
|
+
filter,
|
|
230
|
+
projection,
|
|
231
|
+
skip=skip,
|
|
232
|
+
limit=limit,
|
|
233
|
+
sort=sort,
|
|
234
|
+
collation=collation,
|
|
235
|
+
session=session,
|
|
236
|
+
**kwargs,
|
|
237
|
+
)
|
|
238
|
+
return await cursor.to_list()
|
|
239
|
+
namespace = self._namespace()
|
|
240
|
+
cache = self._database.manager.cache_core
|
|
241
|
+
lookup_result = cache.lookup_namespace(
|
|
242
|
+
namespace, discriminator, codec_options=codec_options
|
|
243
|
+
)
|
|
244
|
+
if lookup_result.hit:
|
|
245
|
+
return cast("list[DocumentType]", lookup_result.value)
|
|
246
|
+
if not cache.is_database_available(namespace.database):
|
|
247
|
+
cursor = self._collection.find(
|
|
248
|
+
filter,
|
|
249
|
+
projection,
|
|
250
|
+
skip=skip,
|
|
251
|
+
limit=limit,
|
|
252
|
+
sort=sort,
|
|
253
|
+
collation=collation,
|
|
254
|
+
session=session,
|
|
255
|
+
**kwargs,
|
|
256
|
+
)
|
|
257
|
+
return await cursor.to_list()
|
|
258
|
+
capture = cache.capture_namespace_generation(namespace)
|
|
259
|
+
cursor = self._forced_collection_handle().find(
|
|
260
|
+
filter,
|
|
261
|
+
projection,
|
|
262
|
+
skip=skip,
|
|
263
|
+
limit=limit,
|
|
264
|
+
sort=sort,
|
|
265
|
+
collation=collation,
|
|
266
|
+
)
|
|
267
|
+
documents = await cursor.to_list()
|
|
268
|
+
cache.admit_namespace(
|
|
269
|
+
capture, discriminator, documents, codec_options=codec_options
|
|
270
|
+
)
|
|
271
|
+
return documents
|
|
272
|
+
|
|
273
|
+
async def aggregate(
|
|
274
|
+
self,
|
|
275
|
+
pipeline: Sequence[Mapping[str, Any]],
|
|
276
|
+
*,
|
|
277
|
+
collation: _CollationIn | None = None,
|
|
278
|
+
session: AsyncClientSession | None = None,
|
|
279
|
+
**kwargs: object,
|
|
280
|
+
) -> list[DocumentType]:
|
|
281
|
+
if pipeline_blocks_full_materialization(pipeline):
|
|
282
|
+
message = (
|
|
283
|
+
"aggregate() always fully materializes its result and cannot "
|
|
284
|
+
"support a $changeStream pipeline; use .raw.aggregate() instead"
|
|
285
|
+
)
|
|
286
|
+
raise UnsupportedCacheRequestError(message)
|
|
287
|
+
codec_options = self._collection.codec_options
|
|
288
|
+
discriminator = order_sensitive_discriminator_key(
|
|
289
|
+
(
|
|
290
|
+
"aggregate",
|
|
291
|
+
pipeline,
|
|
292
|
+
_collation_document(collation),
|
|
293
|
+
codec_fingerprint(codec_options),
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
if (
|
|
297
|
+
self._wants_bypass(session=session, kwargs=kwargs)
|
|
298
|
+
or not is_pipeline_cacheable(pipeline)
|
|
299
|
+
or not is_canonicalizable(discriminator)
|
|
300
|
+
or not await self._is_cache_eligible()
|
|
301
|
+
):
|
|
302
|
+
self._record_bypass()
|
|
303
|
+
cursor = await self._collection.aggregate(
|
|
304
|
+
pipeline,
|
|
305
|
+
collation=collation,
|
|
306
|
+
session=session,
|
|
307
|
+
**cast("dict[str, Any]", kwargs),
|
|
308
|
+
)
|
|
309
|
+
return await cursor.to_list()
|
|
310
|
+
namespace = self._namespace()
|
|
311
|
+
cache = self._database.manager.cache_core
|
|
312
|
+
lookup_result = cache.lookup_namespace(
|
|
313
|
+
namespace, discriminator, codec_options=codec_options
|
|
314
|
+
)
|
|
315
|
+
if lookup_result.hit:
|
|
316
|
+
return cast("list[DocumentType]", lookup_result.value)
|
|
317
|
+
if not cache.is_database_available(namespace.database):
|
|
318
|
+
cursor = await self._collection.aggregate(
|
|
319
|
+
pipeline,
|
|
320
|
+
collation=collation,
|
|
321
|
+
session=session,
|
|
322
|
+
**cast("dict[str, Any]", kwargs),
|
|
323
|
+
)
|
|
324
|
+
return await cursor.to_list()
|
|
325
|
+
capture = cache.capture_namespace_generation(namespace)
|
|
326
|
+
cursor = await self._forced_collection_handle().aggregate(
|
|
327
|
+
pipeline, collation=collation
|
|
328
|
+
)
|
|
329
|
+
documents = await cursor.to_list()
|
|
330
|
+
cache.admit_namespace(
|
|
331
|
+
capture, discriminator, documents, codec_options=codec_options
|
|
332
|
+
)
|
|
333
|
+
return documents
|
|
334
|
+
|
|
335
|
+
async def count_documents(
|
|
336
|
+
self,
|
|
337
|
+
filter: Mapping[str, Any], # noqa: A002
|
|
338
|
+
*,
|
|
339
|
+
skip: int = 0,
|
|
340
|
+
limit: int = 0,
|
|
341
|
+
collation: _CollationIn | None = None,
|
|
342
|
+
hint: str | Sequence[tuple[str, int]] | None = None,
|
|
343
|
+
session: AsyncClientSession | None = None,
|
|
344
|
+
**kwargs: object,
|
|
345
|
+
) -> int:
|
|
346
|
+
merged_kwargs = _count_documents_kwargs(skip, limit, collation, hint) | kwargs
|
|
347
|
+
codec_options = self._collection.codec_options
|
|
348
|
+
discriminator = order_sensitive_discriminator_key(
|
|
349
|
+
(
|
|
350
|
+
"count_documents",
|
|
351
|
+
filter,
|
|
352
|
+
skip,
|
|
353
|
+
limit,
|
|
354
|
+
_collation_document(collation),
|
|
355
|
+
hint,
|
|
356
|
+
codec_fingerprint(codec_options),
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
if (
|
|
360
|
+
self._wants_bypass(session=session, kwargs=kwargs)
|
|
361
|
+
or not is_filter_cacheable(filter)
|
|
362
|
+
or not is_canonicalizable(discriminator)
|
|
363
|
+
or not await self._is_cache_eligible()
|
|
364
|
+
):
|
|
365
|
+
self._record_bypass()
|
|
366
|
+
return await self._collection.count_documents(
|
|
367
|
+
filter, session=session, **merged_kwargs
|
|
368
|
+
)
|
|
369
|
+
namespace = self._namespace()
|
|
370
|
+
cache = self._database.manager.cache_core
|
|
371
|
+
lookup_result = cache.lookup_namespace(
|
|
372
|
+
namespace, discriminator, codec_options=codec_options
|
|
373
|
+
)
|
|
374
|
+
if lookup_result.hit:
|
|
375
|
+
return cast("int", lookup_result.value)
|
|
376
|
+
if not cache.is_database_available(namespace.database):
|
|
377
|
+
return await self._collection.count_documents(
|
|
378
|
+
filter, session=session, **merged_kwargs
|
|
379
|
+
)
|
|
380
|
+
capture = cache.capture_namespace_generation(namespace)
|
|
381
|
+
count = await self._forced_collection_handle().count_documents(
|
|
382
|
+
filter, **merged_kwargs
|
|
383
|
+
)
|
|
384
|
+
cache.admit_namespace(
|
|
385
|
+
capture, discriminator, count, codec_options=codec_options
|
|
386
|
+
)
|
|
387
|
+
return count
|
|
388
|
+
|
|
389
|
+
async def estimated_document_count(self, **kwargs: object) -> int:
|
|
390
|
+
if (
|
|
391
|
+
kwargs
|
|
392
|
+
or not self._is_primary_majority()
|
|
393
|
+
or not await self._is_cache_eligible()
|
|
394
|
+
):
|
|
395
|
+
self._record_bypass()
|
|
396
|
+
return await self._collection.estimated_document_count(**kwargs)
|
|
397
|
+
namespace = self._namespace()
|
|
398
|
+
discriminator = ("estimated_document_count",)
|
|
399
|
+
cache = self._database.manager.cache_core
|
|
400
|
+
codec_options = self._collection.codec_options
|
|
401
|
+
lookup_result = cache.lookup_namespace(
|
|
402
|
+
namespace, discriminator, codec_options=codec_options
|
|
403
|
+
)
|
|
404
|
+
if lookup_result.hit:
|
|
405
|
+
return cast("int", lookup_result.value)
|
|
406
|
+
if not cache.is_database_available(namespace.database):
|
|
407
|
+
return await self._collection.estimated_document_count()
|
|
408
|
+
capture = cache.capture_namespace_generation(namespace)
|
|
409
|
+
count = await self._forced_collection_handle().estimated_document_count()
|
|
410
|
+
cache.admit_namespace(
|
|
411
|
+
capture, discriminator, count, codec_options=codec_options
|
|
412
|
+
)
|
|
413
|
+
return count
|
|
414
|
+
|
|
415
|
+
async def distinct(
|
|
416
|
+
self,
|
|
417
|
+
key: str,
|
|
418
|
+
filter: Mapping[str, Any] | None = None, # noqa: A002
|
|
419
|
+
*,
|
|
420
|
+
collation: _CollationIn | None = None,
|
|
421
|
+
session: AsyncClientSession | None = None,
|
|
422
|
+
**kwargs: object,
|
|
423
|
+
) -> list[Any]:
|
|
424
|
+
codec_options = self._collection.codec_options
|
|
425
|
+
discriminator = order_sensitive_discriminator_key(
|
|
426
|
+
(
|
|
427
|
+
"distinct",
|
|
428
|
+
key,
|
|
429
|
+
filter,
|
|
430
|
+
_collation_document(collation),
|
|
431
|
+
codec_fingerprint(codec_options),
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
if (
|
|
435
|
+
self._wants_bypass(session=session, kwargs=kwargs)
|
|
436
|
+
or not is_filter_cacheable(filter)
|
|
437
|
+
or not is_canonicalizable(discriminator)
|
|
438
|
+
or not await self._is_cache_eligible()
|
|
439
|
+
):
|
|
440
|
+
self._record_bypass()
|
|
441
|
+
return await self._collection.distinct(
|
|
442
|
+
key,
|
|
443
|
+
filter,
|
|
444
|
+
collation=collation,
|
|
445
|
+
session=session,
|
|
446
|
+
**cast("dict[str, Any]", kwargs),
|
|
447
|
+
)
|
|
448
|
+
namespace = self._namespace()
|
|
449
|
+
cache = self._database.manager.cache_core
|
|
450
|
+
lookup_result = cache.lookup_namespace(
|
|
451
|
+
namespace, discriminator, codec_options=codec_options
|
|
452
|
+
)
|
|
453
|
+
if lookup_result.hit:
|
|
454
|
+
return cast("list[Any]", lookup_result.value)
|
|
455
|
+
if not cache.is_database_available(namespace.database):
|
|
456
|
+
return await self._collection.distinct(
|
|
457
|
+
key,
|
|
458
|
+
filter,
|
|
459
|
+
collation=collation,
|
|
460
|
+
session=session,
|
|
461
|
+
**cast("dict[str, Any]", kwargs),
|
|
462
|
+
)
|
|
463
|
+
capture = cache.capture_namespace_generation(namespace)
|
|
464
|
+
values = await self._forced_collection_handle().distinct(
|
|
465
|
+
key, filter, collation=collation
|
|
466
|
+
)
|
|
467
|
+
cache.admit_namespace(
|
|
468
|
+
capture, discriminator, values, codec_options=codec_options
|
|
469
|
+
)
|
|
470
|
+
return values
|
|
471
|
+
|
|
472
|
+
async def _find_one_by_id(
|
|
473
|
+
self,
|
|
474
|
+
identity: object,
|
|
475
|
+
projection: Mapping[str, Any] | Sequence[str] | None,
|
|
476
|
+
read_shape: object,
|
|
477
|
+
) -> DocumentType | None:
|
|
478
|
+
namespace = self._namespace()
|
|
479
|
+
cache = self._database.manager.cache_core
|
|
480
|
+
codec_options = self._collection.codec_options
|
|
481
|
+
cache_identity = normalize_identity_for_cache_key(
|
|
482
|
+
identity, codec_options, self._database.manager.client.codec_options
|
|
483
|
+
)
|
|
484
|
+
if not is_canonicalizable(cache_identity):
|
|
485
|
+
self._record_bypass()
|
|
486
|
+
return await self._collection.find_one({"_id": identity}, projection)
|
|
487
|
+
lookup_result = cache.lookup_identity(
|
|
488
|
+
namespace, cache_identity, read_shape, codec_options=codec_options
|
|
489
|
+
)
|
|
490
|
+
if lookup_result.hit:
|
|
491
|
+
return cast("DocumentType | None", lookup_result.value)
|
|
492
|
+
if not cache.is_database_available(namespace.database):
|
|
493
|
+
return await self._collection.find_one({"_id": identity}, projection)
|
|
494
|
+
capture = cache.begin_identity_admission(namespace, cache_identity)
|
|
495
|
+
try:
|
|
496
|
+
document = await self._forced_collection_handle().find_one(
|
|
497
|
+
{"_id": identity}, projection
|
|
498
|
+
)
|
|
499
|
+
except BaseException:
|
|
500
|
+
cache.discard_identity_admission(capture)
|
|
501
|
+
raise
|
|
502
|
+
cache.admit_identity(capture, read_shape, document, codec_options=codec_options)
|
|
503
|
+
return document
|
|
504
|
+
|
|
505
|
+
async def _match_unique_key(
|
|
506
|
+
self, filter_query: object
|
|
507
|
+
) -> tuple[UniqueKeyDefinition, tuple[Any, ...]] | None:
|
|
508
|
+
namespace = self._namespace()
|
|
509
|
+
manager = self._database.manager
|
|
510
|
+
keys = await manager.unique_keys_for(namespace, self._list_indexes_probe)
|
|
511
|
+
if not keys:
|
|
512
|
+
return None
|
|
513
|
+
default_collation = manager.default_collation_for(namespace)
|
|
514
|
+
return match_unique_key(filter_query, keys, default_collation)
|
|
515
|
+
|
|
516
|
+
async def _find_one_by_unique_key(
|
|
517
|
+
self,
|
|
518
|
+
key_definition: UniqueKeyDefinition,
|
|
519
|
+
key_values: tuple[Any, ...],
|
|
520
|
+
original_filter: Mapping[str, Any],
|
|
521
|
+
projection: Mapping[str, Any] | Sequence[str] | None,
|
|
522
|
+
read_shape: object,
|
|
523
|
+
) -> DocumentType | None:
|
|
524
|
+
namespace = self._namespace()
|
|
525
|
+
cache = self._database.manager.cache_core
|
|
526
|
+
codec_options = self._collection.codec_options
|
|
527
|
+
alias = canonical_alias_key(
|
|
528
|
+
key_definition.fields, key_values, key_definition.collation
|
|
529
|
+
)
|
|
530
|
+
discriminator = (alias, read_shape)
|
|
531
|
+
|
|
532
|
+
resolved_identity = cache.resolve_alias(
|
|
533
|
+
namespace, key_definition.fields, key_values, key_definition.collation
|
|
534
|
+
)
|
|
535
|
+
if resolved_identity is not None:
|
|
536
|
+
lookup_result = cache.lookup_identity(
|
|
537
|
+
namespace, resolved_identity, read_shape, codec_options=codec_options
|
|
538
|
+
)
|
|
539
|
+
if lookup_result.hit:
|
|
540
|
+
return cast("DocumentType | None", lookup_result.value)
|
|
541
|
+
if not cache.is_database_available(namespace.database):
|
|
542
|
+
return await self._collection.find_one(original_filter, projection)
|
|
543
|
+
return await self._resolve_unique_key_read(
|
|
544
|
+
alias,
|
|
545
|
+
discriminator,
|
|
546
|
+
original_filter,
|
|
547
|
+
projection,
|
|
548
|
+
read_shape,
|
|
549
|
+
previous_identity=resolved_identity,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
lookup_result = cache.lookup_namespace(
|
|
553
|
+
namespace, discriminator, codec_options=codec_options
|
|
554
|
+
)
|
|
555
|
+
if lookup_result.hit:
|
|
556
|
+
return cast("DocumentType | None", lookup_result.value)
|
|
557
|
+
if not cache.is_database_available(namespace.database):
|
|
558
|
+
return await self._collection.find_one(original_filter, projection)
|
|
559
|
+
return await self._resolve_unique_key_read(
|
|
560
|
+
alias, discriminator, original_filter, projection, read_shape
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
async def _resolve_unique_key_read(
|
|
564
|
+
self,
|
|
565
|
+
alias: AliasKey,
|
|
566
|
+
discriminator: object,
|
|
567
|
+
original_filter: Mapping[str, Any],
|
|
568
|
+
projection: Mapping[str, Any] | Sequence[str] | None,
|
|
569
|
+
read_shape: object,
|
|
570
|
+
*,
|
|
571
|
+
previous_identity: object | None = None,
|
|
572
|
+
) -> DocumentType | None:
|
|
573
|
+
namespace = self._namespace()
|
|
574
|
+
cache = self._database.manager.cache_core
|
|
575
|
+
codec_options = self._collection.codec_options
|
|
576
|
+
capture = cache.capture_namespace_generation(namespace)
|
|
577
|
+
server_projection, exclude_id = ensure_id_present_for_resolution(projection)
|
|
578
|
+
document = await self._forced_collection_handle().find_one(
|
|
579
|
+
original_filter, server_projection
|
|
580
|
+
)
|
|
581
|
+
if document is None:
|
|
582
|
+
if previous_identity is not None:
|
|
583
|
+
cache.discard_stale_alias(namespace, alias, previous_identity)
|
|
584
|
+
cache.discard_namespace_entry(namespace, discriminator, capture.generation)
|
|
585
|
+
cache.admit_namespace(
|
|
586
|
+
capture, discriminator, None, codec_options=codec_options
|
|
587
|
+
)
|
|
588
|
+
return None
|
|
589
|
+
raw_identity = document["_id"]
|
|
590
|
+
if exclude_id:
|
|
591
|
+
document = cast("DocumentType", without_id(document, codec_options))
|
|
592
|
+
cache_identity = normalize_identity_for_cache_key(
|
|
593
|
+
raw_identity, codec_options, self._database.manager.client.codec_options
|
|
594
|
+
)
|
|
595
|
+
if cache_identity is not None and is_canonicalizable(cache_identity):
|
|
596
|
+
cache.discard_namespace_entry(namespace, discriminator, capture.generation)
|
|
597
|
+
outcome = cache.admit_unique_key_match(
|
|
598
|
+
capture,
|
|
599
|
+
discriminator,
|
|
600
|
+
cache_identity,
|
|
601
|
+
read_shape,
|
|
602
|
+
document,
|
|
603
|
+
alias=alias,
|
|
604
|
+
codec_options=codec_options,
|
|
605
|
+
)
|
|
606
|
+
if (
|
|
607
|
+
outcome is not AdmissionOutcome.ADMITTED
|
|
608
|
+
and previous_identity is not None
|
|
609
|
+
):
|
|
610
|
+
cache.discard_stale_alias(namespace, alias, previous_identity)
|
|
611
|
+
elif previous_identity is not None:
|
|
612
|
+
cache.discard_stale_alias(namespace, alias, previous_identity)
|
|
613
|
+
cache.discard_namespace_entry(namespace, discriminator, capture.generation)
|
|
614
|
+
return document
|
|
615
|
+
|
|
616
|
+
def _namespace(self) -> NamespaceId:
|
|
617
|
+
return NamespaceId(self._database.name, self.name)
|
|
618
|
+
|
|
619
|
+
def _is_primary_majority(self) -> bool:
|
|
620
|
+
read_concern = self._collection.read_concern
|
|
621
|
+
return (
|
|
622
|
+
self._collection.read_preference == ReadPreference.PRIMARY
|
|
623
|
+
and read_concern.level in _ACCEPTABLE_READ_CONCERN_LEVELS
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
def _wants_bypass(
|
|
627
|
+
self, *, session: AsyncClientSession | None, kwargs: Mapping[str, object]
|
|
628
|
+
) -> bool:
|
|
629
|
+
return session is not None or bool(kwargs) or not self._is_primary_majority()
|
|
630
|
+
|
|
631
|
+
def _record_bypass(self) -> None:
|
|
632
|
+
self._database.manager.cache_core.record_bypass()
|
|
633
|
+
|
|
634
|
+
def _forced_collection_handle(self) -> AsyncCollection[DocumentType]:
|
|
635
|
+
if self._forced_collection is None:
|
|
636
|
+
self._forced_collection = self._collection.with_options(
|
|
637
|
+
read_preference=ReadPreference.PRIMARY,
|
|
638
|
+
read_concern=_FORCED_READ_CONCERN,
|
|
639
|
+
)
|
|
640
|
+
return self._forced_collection
|
|
641
|
+
|
|
642
|
+
def _forced_database_handle(self) -> AsyncDatabase[DocumentType]:
|
|
643
|
+
if self._forced_database is None:
|
|
644
|
+
self._forced_database = self._database.raw.with_options(
|
|
645
|
+
read_preference=ReadPreference.PRIMARY,
|
|
646
|
+
read_concern=_FORCED_READ_CONCERN,
|
|
647
|
+
)
|
|
648
|
+
return self._forced_database
|
|
649
|
+
|
|
650
|
+
async def _is_cache_eligible(self) -> bool:
|
|
651
|
+
return await self._database.manager.ensure_cache_eligible(
|
|
652
|
+
self._namespace(), self._probe_collection
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
async def _probe_collection(self) -> CollectionProbeResult | None:
|
|
656
|
+
try:
|
|
657
|
+
cursor = await self._forced_database_handle().list_collections(
|
|
658
|
+
filter={"name": self.name}
|
|
659
|
+
)
|
|
660
|
+
entries = await cursor.to_list(length=1)
|
|
661
|
+
except PyMongoError:
|
|
662
|
+
logger.warning(
|
|
663
|
+
"collection-type probe failed; this read bypasses the cache",
|
|
664
|
+
extra={"database": self._database.name, "collection": self.name},
|
|
665
|
+
exc_info=True,
|
|
666
|
+
)
|
|
667
|
+
return None
|
|
668
|
+
entry = entries[0] if entries else None
|
|
669
|
+
return interpret_list_collections_entry(entry)
|
|
670
|
+
|
|
671
|
+
async def _list_indexes_probe(self) -> list[Mapping[str, Any]] | None:
|
|
672
|
+
try:
|
|
673
|
+
cursor = await self._forced_collection_handle().list_indexes()
|
|
674
|
+
return cast("list[Mapping[str, Any]]", await cursor.to_list())
|
|
675
|
+
except PyMongoError:
|
|
676
|
+
logger.warning(
|
|
677
|
+
"index metadata probe failed; unique-key discovery is skipped for "
|
|
678
|
+
"this read",
|
|
679
|
+
extra={"database": self._database.name, "collection": self.name},
|
|
680
|
+
exc_info=True,
|
|
681
|
+
)
|
|
682
|
+
return None
|