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