refscan 0.2.2__tar.gz → 0.2.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: refscan
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Command-line program that scans the NMDC MongoDB database for referential integrity violations
5
5
  Keywords: mongodb,mongo,relationships,reference,database,data,referential integrity,scan
6
6
  Requires-Python: >=3.9,<4.0
@@ -2,7 +2,7 @@
2
2
 
3
3
  [tool.poetry]
4
4
  name = "refscan"
5
- version = "v0.2.2"
5
+ version = "v0.2.4"
6
6
  description = "Command-line program that scans the NMDC MongoDB database for referential integrity violations"
7
7
  authors = []
8
8
  readme = "README.md"
@@ -48,6 +48,18 @@ def encode_json_value_as_base64_str(json_value: Union[dict, list]) -> str:
48
48
  return encoded_bytes_as_string
49
49
 
50
50
 
51
+ def is_class_abstract(class_name: str, schema_view: linkml_runtime.SchemaView) -> bool:
52
+ r"""
53
+ Returns whether the specified schema class is abstract.
54
+
55
+ Note: The `abstract` property can contain `True`, `False`, or `None`.
56
+
57
+ Reference: https://linkml.io/linkml/schemas/inheritance.html#abstract-classes-and-slots
58
+ """
59
+
60
+ return schema_view.get_class(class_name).abstract is True
61
+
62
+
51
63
  def graph(
52
64
  schema_view: linkml_runtime.SchemaView,
53
65
  subject: Subject,
@@ -106,12 +118,17 @@ def graph(
106
118
  source_name = r.source_collection_name if subject == Subject.collection else r.source_class_name
107
119
  target_name = r.target_collection_name if subject == Subject.collection else r.target_class_name
108
120
 
121
+ # If the subject of the graph is "class", determine whether the source class is abstract. Same for the target class.
122
+ # Note: This will allow us to display abstract classes differently from concrete classes in the diagram.
123
+ is_source_abstract = is_class_abstract(source_name, schema_view) if subject == Subject.class_ else None
124
+ is_target_abstract = is_class_abstract(target_name, schema_view) if subject == Subject.class_ else None
125
+
109
126
  # Append a node for the source subject if we haven't already done so. Same for the target subject.
110
127
  node_ids = [n["data"]["id"] for n in nodes]
111
128
  if source_name not in node_ids:
112
- nodes.append(dict(data=dict(id=source_name)))
129
+ nodes.append(dict(data=dict(id=source_name, is_abstract=is_source_abstract)))
113
130
  if target_name not in node_ids:
114
- nodes.append(dict(data=dict(id=target_name)))
131
+ nodes.append(dict(data=dict(id=target_name, is_abstract=is_target_abstract)))
115
132
 
116
133
  # Append an edge for this source subject-to-target subject relationship if we haven't already done so.
117
134
  edge_id = f"{source_name}__to__{target_name}"
@@ -147,6 +164,8 @@ def graph(
147
164
  dict(
148
165
  app_version=get_package_metadata("Version"),
149
166
  schema_version=schema_view.schema.version,
167
+ subject_singular="class" if subject == Subject.class_ else "collection",
168
+ subject_plural="classes" if subject == Subject.class_ else "collections",
150
169
  )
151
170
  )
152
171
  html_result = html_template.replace("{{ graph_data_json_base64 }}", graph_data_json_base64_str)
@@ -0,0 +1,184 @@
1
+ from typing import List, Optional, Dict
2
+
3
+ from pymongo.database import Database
4
+ from pymongo.client_session import ClientSession
5
+
6
+
7
+ class Finder:
8
+ r"""
9
+ A class that can be used to find a document in a MongoDB database, in a way that uses past searches to try to
10
+ speed up future searches.
11
+ """
12
+
13
+ def __init__(self, database: Database):
14
+ r"""
15
+ Initializes an instance of the `Finder` class. The instance is bound to the specified MongoDB database.
16
+ """
17
+ self.db = database
18
+
19
+ # Initialize our cache of names of collections we most recently found referenced document `id`s in.
20
+ #
21
+ # Note: This is a queue we will use to keep track of which collections we most recently found referenced
22
+ # document `id`s in. This will enable us to _begin_ searching in those same collections for subsequent
23
+ # searches. The idea that this will speed things up is based on the assumption that references emanating
24
+ # from source documents that we process _temporally close_ to one another, likely point to target
25
+ # documents residing _in the same collections_ as one another.
26
+ #
27
+ self.cached_collection_names_where_recently_found = []
28
+ self.cache_size = 2 # we'll cache up to 2 collection names
29
+
30
+ # Initialize our cache of document `id` presences/absences by collection.
31
+ #
32
+ # Note: This is a dictionary we will use to keep track of the referenced `id`s we find in each collection,
33
+ # and the referenced `id`s we fail to find (i.e. confirm do _not_ exist) in each collection. This will
34
+ # enable us to avoid querying the database for the same information that we have previously obtained
35
+ # by querying the database.
36
+ #
37
+ # Note: The top-level items in the dictionary have keys that are collection names and values that are
38
+ # dictionaries. Each of those dictionaries has keys that are document `id`s and values that are boolean
39
+ # flags, which indicate whether we know that a document having that `id` exists in that collection
40
+ # (`True`) or we know that a document having that `id` does _not_ exist in that collection (`False`).
41
+ # Here's an example value of this attribute:
42
+ # ```
43
+ # {
44
+ # "biosample_set": {"nmdc:bsm-00-000001": True, "nmdc:bsm-00-000002": False},
45
+ # "study_set": {"nmdc:sty-00-000001": True},
46
+ # }
47
+ # ```
48
+ #
49
+ # Note: If this program ever leads to memory consumption (RAM usage) issues in practice, consider
50
+ # redesigning this cache and/or allowing the user to disable its use.
51
+ #
52
+ self.cached_id_presence_by_collection: Dict[str, Dict[str, bool]] = {}
53
+
54
+ def _set_name_of_collection_most_recently_found_in(self, collection_name: str):
55
+ r"""
56
+ Helper function that updates our cache of collection names in which target documents were most recently found,
57
+ so that the specified collection is at the front/start of the list. That will make it so it gets searched first
58
+ next time.
59
+ """
60
+ # Make a concise alias for the instance attribute (both will refer to the same data structure).
61
+ queue = self.cached_collection_names_where_recently_found
62
+
63
+ # Check whether either (a) the queue is empty or (b) this collection name is not at the front of the queue.
64
+ # Example: [] is empty, or "b_set" is not the first item in ["a_set", "b_set", "c_set"]
65
+ if len(queue) == 0 or queue[0] != collection_name:
66
+
67
+ # If this collection name is already (elsewhere) in the queue, remove it from that position, automatically
68
+ # updating the list indexes of subsequent list items (so there is no "gap" in the list).
69
+ # Example: Removing "b_set" from ["a_set", "b_set", "c_set"] → ["a_set", "c_set"]
70
+ if collection_name in queue:
71
+ queue.remove(collection_name)
72
+
73
+ # Prepend this collection name to the beginning of the queue, dropping any excess collection names.
74
+ # Example: Prepending ["b_set"] to ["a_set", "c_set"] → ["b_set", "a_set", "c_set"]
75
+ queue = [collection_name] + queue[0 : self.cache_size - 1]
76
+
77
+ # Note: This unnecessary assignment is here only to tell my IDE that `queue` isn't an unused variable.
78
+ self.cached_collection_names_where_recently_found = queue
79
+
80
+ def _optimize_collection_search_order(self, names_of_eligible_collections: List[str]) -> List[str]:
81
+ r"""
82
+ Helper function that returns an optimized order in which the specified collections could be searched, in order
83
+ to take advantage of things the instance "remembers" from recent searches.
84
+ """
85
+ ordered_collection_names = names_of_eligible_collections.copy()
86
+
87
+ # Iterate over the collection names in the queue, from back (oldest) to front (newest).
88
+ for cached_collection_name in reversed(self.cached_collection_names_where_recently_found):
89
+
90
+ # If this cached collection name is among the eligible ones, move it to the front of the list.
91
+ # Example: Since "b_set" is in ["a_set", "b_set", "c_set"] → ["b_set, "a_set", "c_set"]
92
+ if cached_collection_name in names_of_eligible_collections:
93
+ ordered_collection_names.remove(cached_collection_name)
94
+ ordered_collection_names.insert(0, cached_collection_name)
95
+
96
+ return ordered_collection_names
97
+
98
+ def _set_cached_id_presence_in_collection(self, collection_name: str, document_id: str, is_present: bool) -> None:
99
+ r"""
100
+ Helper function that updates our cache of document `id` presences/absences, setting the presence flag for the
101
+ specified document `id` in the specified collection.
102
+ """
103
+ if collection_name not in self.cached_id_presence_by_collection:
104
+ self.cached_id_presence_by_collection[collection_name] = {} # creates key if it does not exist
105
+ self.cached_id_presence_by_collection[collection_name][document_id] = is_present
106
+
107
+ def _get_cached_id_presence_in_collection(self, collection_name: str, document_id: str) -> Optional[bool]:
108
+ r"""
109
+ Helper function that checks our cache of document `id` presences/absences, returning the presence/absence flag,
110
+ if any, for the specified document `id` in the specified collection.
111
+ """
112
+ if (
113
+ collection_name not in self.cached_id_presence_by_collection
114
+ or document_id not in self.cached_id_presence_by_collection[collection_name]
115
+ ):
116
+ return None
117
+ else:
118
+ return self.cached_id_presence_by_collection[collection_name][document_id]
119
+
120
+ def check_whether_document_having_id_exists_among_collections(
121
+ self, document_id: str, collection_names: List[str], client_session: ClientSession = None
122
+ ) -> Optional[str]:
123
+ r"""
124
+ Checks whether any document in any of the specified collections has the specified value in its `id` field.
125
+ Returns the name of the first collection, if any, containing such a document. If none of the collections
126
+ contain such a document, the function returns `None`.
127
+
128
+ If a `pymongo.client_session.ClientSession` is specified, this function will access the database within
129
+ the context of that session. If a transaction is pending on that session, this feature can be used
130
+ to check whether the specified document _would_ exist if that transaction were to be committed.
131
+
132
+ References:
133
+ - https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.find_one
134
+ - https://pymongo.readthedocs.io/en/stable/api/pymongo/client_session.html#transactions
135
+ """
136
+
137
+ # Initialize the name of the containing collection to `None`.
138
+ name_of_collection_containing_target_document = None
139
+
140
+ # Optimize the search order of the collections, based upon the outcomes of recent searches.
141
+ ordered_collection_names = self._optimize_collection_search_order(collection_names)
142
+
143
+ # Search the collections.
144
+ query_filter = dict(id=document_id)
145
+ for collection_name in ordered_collection_names:
146
+
147
+ # Before querying the database, check our cache of document `id` presences/absences per collection.
148
+ is_id_present: Optional[bool] = self._get_cached_id_presence_in_collection(
149
+ collection_name=collection_name, document_id=document_id
150
+ )
151
+
152
+ # If we already know it's _not_ in this collection, stop evaluating this collection and, instead,
153
+ # proceed to evaluate the next collection.
154
+ if is_id_present is False:
155
+ continue # stop evaluating this collection
156
+
157
+ # If we already know it _is_ in this collection, record the collection name, update the cache of names
158
+ # of collections where a referenced document was recently found, and skip searching additional collections.
159
+ if is_id_present is True:
160
+ name_of_collection_containing_target_document = collection_name
161
+ self._set_name_of_collection_most_recently_found_in(collection_name=collection_name)
162
+ break
163
+
164
+ # If we find it in the collection in the database, record the collection name, update both (a) the cache
165
+ # of which documents we have found in which collections, and (b) the cache of names of collections where
166
+ # a referenced document was recently found, and skip searching additional collections.
167
+ collection = self.db.get_collection(collection_name)
168
+ if collection.find_one(query_filter, projection=["_id"], session=client_session) is not None:
169
+ name_of_collection_containing_target_document = collection_name
170
+
171
+ # Record that a document having this `id` is _present_ in this collection.
172
+ self._set_cached_id_presence_in_collection(
173
+ collection_name=collection_name, document_id=document_id, is_present=True
174
+ )
175
+
176
+ self._set_name_of_collection_most_recently_found_in(collection_name=collection_name)
177
+ break
178
+
179
+ # If we made it this far, record that a document having this `id` does _not_ exist in this collection.
180
+ self._set_cached_id_presence_in_collection(
181
+ collection_name=collection_name, document_id=document_id, is_present=False
182
+ )
183
+
184
+ return name_of_collection_containing_target_document
@@ -1,6 +1,7 @@
1
1
  from typing import List, Optional, Dict
2
2
 
3
3
  from pymongo.database import Database
4
+ from pymongo.client_session import ClientSession
4
5
  from rich.console import Console
5
6
  from linkml_runtime import SchemaView
6
7
 
@@ -24,6 +25,7 @@ def scan_outgoing_references(
24
25
  finder: Finder,
25
26
  collection_names: List[str],
26
27
  source_collection_name: str,
28
+ client_session: ClientSession = None,
27
29
  user_wants_to_locate_misplaced_documents: bool = False,
28
30
  ) -> ViolationList:
29
31
  r"""
@@ -40,6 +42,10 @@ def scan_outgoing_references(
40
42
  all the legal collections
41
43
  :param source_collection_name: Name of collection in which source document resides (this is included in the
42
44
  violation report in an attempt to facilitate investigation of violations)
45
+ :param client_session: A `pymongo.client_session.ClientSession` instance that, if specified, will be used when
46
+ searching for referenced documents. If a transaction happens to be pending on that session,
47
+ the scan will effectively happen on the database as it _would_ exist if that transaction
48
+ were to be committed.
43
49
  :param collection_names: List of collection names that are described by the schema
44
50
  """
45
51
 
@@ -77,7 +83,7 @@ def scan_outgoing_references(
77
83
  for target_id in target_ids:
78
84
  name_of_collection_containing_target_document = (
79
85
  finder.check_whether_document_having_id_exists_among_collections(
80
- collection_names=target_collection_names, document_id=target_id
86
+ collection_names=target_collection_names, document_id=target_id, client_session=client_session
81
87
  )
82
88
  )
83
89
  if name_of_collection_containing_target_document is None:
@@ -88,7 +94,9 @@ def scan_outgoing_references(
88
94
  names_of_ineligible_collections = list(set(collection_names) - set(target_collection_names))
89
95
  name_of_collection_containing_target_document = (
90
96
  finder.check_whether_document_having_id_exists_among_collections(
91
- collection_names=names_of_ineligible_collections, document_id=target_id
97
+ collection_names=names_of_ineligible_collections,
98
+ document_id=target_id,
99
+ client_session=client_session,
92
100
  )
93
101
  )
94
102
 
@@ -19,6 +19,15 @@
19
19
  list-style-type: none;
20
20
  margin: 0;
21
21
  }
22
+ .text-button {
23
+ color: inherit;
24
+ text-decoration: underline;
25
+ background: none;
26
+ border: none;
27
+ margin: 0;
28
+ padding: 0;
29
+ cursor: pointer;
30
+ }
22
31
  </style>
23
32
  </head>
24
33
  <body>
@@ -82,6 +91,8 @@
82
91
  li.textContent = `refscan version ${graphMetadata["app_version"]}`;
83
92
  metadataListEl.appendChild(li);
84
93
  }
94
+ let subjectSingular = graphMetadata["subject_singular"];
95
+ let subjectPlural = graphMetadata["subject_plural"];
85
96
  metadataContainerEl.appendChild(metadataListEl);
86
97
 
87
98
  // Base64-decode the graph data and parse it as JSON.
@@ -174,7 +185,19 @@
174
185
  color: "#0000FF", // text color
175
186
  backgroundColor: "#0000FF"
176
187
  }
177
- }
188
+ },
189
+ {
190
+ selector: "node.isAbstract",
191
+ style: {
192
+ opacity: 0.5,
193
+ }
194
+ },
195
+ {
196
+ selector: "edge.isAbstract",
197
+ style: {
198
+ opacity: 0.5,
199
+ }
200
+ },
178
201
  ],
179
202
  });
180
203
 
@@ -214,6 +237,60 @@
214
237
  .removeClass("incomer")
215
238
  .removeClass("inFocus");
216
239
  });
240
+
241
+ /**
242
+ * Sets up a text button that can be used to toggle the visibility of
243
+ * nodes that represent abstract things.
244
+ */
245
+ function setupAbstractNodeVisibilityToggler() {
246
+ // Get a collection of all nodes that represent abstract things.
247
+ // Note: For conciseness, we refer to them as "abstract nodes" here.
248
+ // Reference: https://js.cytoscape.org/#eles.filter
249
+ const abstractNodes = cy.filter(function(el, idx, allEls) {
250
+ return el.isNode() && el.data("is_abstract");
251
+ });
252
+
253
+ // If there are any abstract nodes, give them (and the edges connected to them)
254
+ // a specific "stylesheet" class and set up their visibility toggler.
255
+ if (abstractNodes.length > 0) {
256
+ abstractNodes.addClass("isAbstract");
257
+ abstractNodes.connectedEdges().addClass("isAbstract");
258
+
259
+ // Set up a clickable element that can be used to show/hide the abstract nodes.
260
+ // Note: When a node gets hidden, its connected edges will be automatically hidden also.
261
+ const strings = {
262
+ prefix: `Abstract ${subjectPlural} are `,
263
+ visible: "visible",
264
+ hidden: "hidden",
265
+ };
266
+ let removedNodes = cy.collection(); // initially empty
267
+ const prefixEl = document.createElement("span");
268
+ prefixEl.textContent =strings.prefix;
269
+ const togglerEl = document.createElement("button");
270
+ togglerEl.classList.add("text-button");
271
+ togglerEl.title = "Toggle visibility";
272
+ const togglerTextEl = document.createElement("span");
273
+ togglerTextEl.textContent = strings.visible;
274
+ togglerEl.appendChild(togglerTextEl);
275
+ togglerEl.addEventListener("click", () => {
276
+ if (removedNodes.length > 0) {
277
+ // Add them back into the graph.
278
+ removedNodes.restore();
279
+ removedNodes = cy.collection();
280
+ togglerTextEl.textContent = strings.visible;
281
+ } else {
282
+ // Remove them from the graph, but save them in memory.
283
+ removedNodes = cy.remove(abstractNodes);
284
+ togglerTextEl.textContent = strings.hidden;
285
+ }
286
+ });
287
+ metadataContainerEl.appendChild(prefixEl);
288
+ metadataContainerEl.appendChild(togglerEl);
289
+ }
290
+ }
291
+
292
+ // Set up the abstract node visibility toggler.
293
+ setupAbstractNodeVisibilityToggler();
217
294
  })();
218
295
  </script>
219
296
  </body>
@@ -1,139 +0,0 @@
1
- from typing import List, Optional, Dict
2
-
3
- from pymongo.database import Database
4
-
5
-
6
- class Finder:
7
- r"""
8
- A class that can be used to find a document in a MongoDB database,
9
- in a way that uses past searches to try to speed up future searches.
10
- """
11
-
12
- def __init__(self, database: Database):
13
- self.db = database
14
-
15
- # Initialize a variable we can use to keep track of the collections in which we most recently found targeted
16
- # documents.
17
- #
18
- # Note: This will enable us to _start_ searching in these same collections next time.
19
- #
20
- self.names_of_collections_most_recently_found_in = []
21
- self.cache_size = 2 # we'll cache up to 2 collection names
22
-
23
- # Initialize a dictionary we can use to keep track of the referenced `id`s we have found in a given collection,
24
- # and the referenced `id`s we have confirmed do _not_ exist in a given collection.
25
- #
26
- # The top-level items have keys that are collection names and values that are dictionaries. The latter
27
- # dictionaries have keys that are `id` values and values that are boolean flags. The boolean flags indicate
28
- # whether we have found a document having that `id` in that collection; where `True` means we have,
29
- # and `False` means we search and _did not_ find such a document in that collection.
30
- #
31
- # Example:
32
- # ```
33
- # {
34
- # "biosample_set": {"bsm-00-000001": True, "bsm-00-000002": False},
35
- # "study_set": {"sty-00-000001": True},
36
- # }
37
- # ```
38
- #
39
- # Note: This will enable us to _avoid_ searching for the same `id` values in the same collections in the future,
40
- # decreasing execution time (in exchange for memory consumption).
41
- #
42
- # TODO: Measure memory usage when scanning the NMDC database, reverting this perf. optimization if necessary.
43
- #
44
- self.cached_id_presence_by_collection: Dict[str, Dict[str, bool]] = {}
45
-
46
- def _set_cached_id_presence_in_collection(self, collection_name: str, document_id: str, is_present: bool) -> None:
47
- r"""
48
- Helper function that updates our cache of `id` presences/absences, setting the presence flag for the specified
49
- document `id` in the specified collection.
50
- """
51
- if collection_name not in self.cached_id_presence_by_collection:
52
- self.cached_id_presence_by_collection[collection_name] = {}
53
- self.cached_id_presence_by_collection[collection_name][document_id] = is_present
54
-
55
- def _get_cached_id_presence_in_collection(self, collection_name: str, document_id: str) -> Optional[bool]:
56
- r"""
57
- Helper function that checks our cache of `id` presences/absences, getting the presence/absence flag, if any,
58
- for the specified document `id` in the specified collection.
59
- """
60
- if (
61
- collection_name not in self.cached_id_presence_by_collection
62
- or document_id not in self.cached_id_presence_by_collection[collection_name]
63
- ):
64
- return None
65
- else:
66
- return self.cached_id_presence_by_collection[collection_name][document_id]
67
-
68
- def check_whether_document_having_id_exists_among_collections(
69
- self, document_id: str, collection_names: List[str]
70
- ) -> Optional[str]:
71
- r"""
72
- Checks whether any document in any of the specified collections has the specified value in its `id` field.
73
- Returns the name of the first collection, if any, containing such a document. If none of the collections
74
- contain such a document, the function returns `None`.
75
-
76
- TODO: This function refers to "cache" as though we are only dealing with one—but we are now dealing with two
77
- (see the two instance attributes of this class). The outstanding task here is to update the comments
78
- in this function to be more specific about which cache is being referenced in a given statement.
79
-
80
- References:
81
- - https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.find_one
82
- """
83
- names_of_collections_to_search = collection_names.copy()
84
-
85
- # Make a concise alias.
86
- cache = self.names_of_collections_most_recently_found_in
87
-
88
- # If any cached collection name is among the ones eligible to search, move it to the front of list.
89
- for cached_collection_name in reversed(cache): # goes in reverse, so latest addition ends up in front
90
- if cached_collection_name in collection_names:
91
- names_of_collections_to_search.remove(cached_collection_name)
92
- names_of_collections_to_search.insert(0, cached_collection_name) # makes it the first item
93
-
94
- # Search the collections in their current order.
95
- name_of_collection_containing_target_document = None
96
- query_filter = dict(id=document_id)
97
- for collection_name in names_of_collections_to_search:
98
-
99
- # Before querying the database, check our in-memory record of `id` presences in this collection.
100
- is_id_present: Optional[bool] = self._get_cached_id_presence_in_collection(
101
- collection_name=collection_name, document_id=document_id
102
- )
103
-
104
- # If we already know it's _not_ in this collection, stop evaluating this collection and, instead,
105
- # proceed to evaluate the next collection.
106
- if is_id_present is False:
107
- continue # stop evaluating this collection
108
-
109
- # If we _don't know_ whether it is in the collection or not, we can't take any shortcuts. Aw, shucks.
110
- if is_id_present is None:
111
- pass
112
-
113
- # If it _is_ in this collection—based on either what we already know, or what we find now—cache
114
- # the collection name and skip searching additional collections for documents having this `id`.
115
- collection = self.db.get_collection(collection_name)
116
- if is_id_present or collection.find_one(query_filter, projection=["_id"]) is not None:
117
- name_of_collection_containing_target_document = collection_name
118
-
119
- # Put this collection name at the front/start of the cache.
120
- if len(cache) == 0 or cache[0] != collection_name:
121
- if collection_name in cache: # if it's elsewhere in the cache, remove it from the cache
122
- cache.remove(collection_name)
123
- cache = [collection_name] + cache[0 : self.cache_size - 1] # prepends item, dropping any excess
124
- self.names_of_collections_most_recently_found_in = cache # persists it to the instance attribute
125
-
126
- # Record that a document having this `id` _exists_ in this collection.
127
- # Note: This assignment will be redundant if we got here due to `is_id_present` being `True`.
128
- self._set_cached_id_presence_in_collection(
129
- collection_name=collection_name, document_id=document_id, is_present=True
130
- )
131
-
132
- break
133
-
134
- # Record that a document having this `id` does _not_ exist in this collection.
135
- self._set_cached_id_presence_in_collection(
136
- collection_name=collection_name, document_id=document_id, is_present=False
137
- )
138
-
139
- return name_of_collection_containing_target_document
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes