Skip to content

Commit 1671dbb

Browse files
feat(core): support fetching entities and entity identifiers for multiple operations at once (#1228)
* feat(core): support fetching entities and entity identifiers for multiple operations at once Signed-off-by: Alessandro Pomponio <alessandro.pomponio1@ibm.com> * refactor(core): rename method Signed-off-by: Alessandro Pomponio <alessandro.pomponio1@ibm.com> --------- Signed-off-by: Alessandro Pomponio <alessandro.pomponio1@ibm.com>
1 parent 7610059 commit 1671dbb

4 files changed

Lines changed: 155 additions & 60 deletions

File tree

ado/core/discoveryspace/space.py

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
import os
77
import typing
8+
import warnings
89
from collections.abc import Callable, Iterator
910
from functools import wraps
1011
from typing import Any
@@ -623,28 +624,7 @@ def sampledEntities(self) -> list[Entity]:
623624
if not operation_ids:
624625
return []
625626

626-
# Optimize for single operation: use direct query (1 query instead of 2)
627-
if len(operation_ids) == 1:
628-
sampled_entities = self.sample_store.entities_in_operation(
629-
operation_id=next(iter(operation_ids))
630-
)
631-
else:
632-
# Multiple operations: get entity IDs first, then fetch entities
633-
# This approach handles deduplication across operations naturally
634-
sampled_entity_ids = set()
635-
for operationid in operation_ids:
636-
sampled_entity_ids.update(
637-
self.entity_identifiers_in_operation(operation_id=operationid)
638-
)
639-
640-
if not sampled_entity_ids:
641-
return []
642-
643-
# Efficiently fetch only the entities that were sampled in operations
644-
# This avoids loading all entities from the store when we only need a subset
645-
sampled_entities = self.sample_store.entities_with_identifiers(
646-
sampled_entity_ids
647-
)
627+
sampled_entities = self.sample_store.entities_in_operations(operation_ids)
648628

649629
# TODO: Consider removing isEntitySpace check
650630
# The additional check of isEntityInSpace should not be required if things are working correctly
@@ -1072,10 +1052,25 @@ def complete_measurement_request_with_results_timeseries(
10721052
)
10731053

10741054
@_perform_preflight_checks_for_sample_store_methods
1075-
def entity_identifiers_in_operation(self, operation_id: str) -> set[str]:
1076-
return self.sample_store.entity_identifiers_in_operation(
1077-
operation_id=operation_id
1055+
def entity_identifiers_in_operations(
1056+
self, operation_ids: str | set[str]
1057+
) -> set[str]:
1058+
"""Return entity identifiers sampled in the given operation(s)."""
1059+
return self.sample_store.entity_identifiers_in_operations(
1060+
operation_ids=operation_ids
1061+
)
1062+
1063+
@_perform_preflight_checks_for_sample_store_methods
1064+
def entity_identifiers_in_operation(
1065+
self, operation_ids: str | set[str]
1066+
) -> set[str]:
1067+
"""Deprecated: use entity_identifiers_in_operations instead."""
1068+
warnings.warn(
1069+
"entity_identifiers_in_operation is deprecated, use entity_identifiers_in_operations instead.",
1070+
DeprecationWarning,
1071+
stacklevel=2,
10781072
)
1073+
return self.entity_identifiers_in_operations(operation_ids)
10791074

10801075
@_perform_preflight_checks_for_sample_store_methods
10811076
def experiments_in_operation(self, operation_id: str) -> list[Experiment]:

ado/core/samplestore/base.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import abc
55
import typing
6+
import warnings
67
from abc import ABC
78
from typing import Annotated, Literal
89

@@ -491,8 +492,25 @@ def commit(self) -> None: # pragma: nocover
491492
"""Commits all the changes to the source"""
492493

493494
@abc.abstractmethod
494-
def entities_in_operation(self, operation_id: str) -> list[Entity]:
495-
"""Returns list of entities in the given operation."""
495+
def entities_in_operations(self, operation_ids: str | set[str]) -> list[Entity]:
496+
"""Returns list of entities in the given operation(s).
497+
498+
Args:
499+
operation_ids: A single operation identifier or a set of operation
500+
identifiers to fetch entities for.
501+
502+
Returns:
503+
List of Entity objects that were sampled in the specified operation(s).
504+
"""
505+
506+
def entities_in_operation(self, operation_ids: str | set[str]) -> list[Entity]:
507+
"""Deprecated: use entities_in_operations instead."""
508+
warnings.warn(
509+
"entities_in_operation is deprecated, use entities_in_operations instead.",
510+
DeprecationWarning,
511+
stacklevel=2,
512+
)
513+
return self.entities_in_operations(operation_ids)
496514

497515
@abc.abstractmethod
498516
def operation_entity_statistics(self, operation_id: str) -> dict[str, int]:

ado/core/samplestore/sql.py

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import typing
88
import uuid
9+
import warnings
910
from typing import TYPE_CHECKING, Annotated, Literal
1011

1112
import pydantic
@@ -822,19 +823,23 @@ def entities_with_identifiers(
822823

823824
return list(entities_dict.values())
824825

825-
def entities_in_operation(self, operation_id: str) -> list[Entity]:
826-
"""Get entities directly from a single operation in one query.
826+
def entities_in_operations(self, operation_ids: str | set[str]) -> list[Entity]:
827+
"""Get entities directly from one or more operations in one query.
827828
828-
This method is optimized for the common case of fetching entities from
829-
a single operation. It performs the entire operation in a single database
830-
query, avoiding the need to first fetch entity IDs and then fetch entities.
829+
This method fetches entities from one or more operations in a single
830+
database query, avoiding the need to first fetch entity IDs and then
831+
fetch entities.
831832
832833
Args:
833-
operation_id: The operation identifier to fetch entities for
834+
operation_ids: A single operation identifier or a set of operation
835+
identifiers to fetch entities for.
834836
835837
Returns:
836-
List of Entity objects that were sampled in the specified operation
838+
List of Entity objects that were sampled in the specified operation(s)
837839
"""
840+
if isinstance(operation_ids, str):
841+
operation_ids = {operation_ids}
842+
838843
query = sqlalchemy.text(f"""
839844
SELECT
840845
ent.identifier,
@@ -844,16 +849,18 @@ def entities_in_operation(self, operation_id: str) -> list[Entity]:
844849
JOIN {self._tablename}_measurement_results res ON res.entity_id = ent.identifier
845850
JOIN {self._tablename}_measurement_requests_results reqres ON reqres.result_uid = res.uid
846851
JOIN {self._tablename}_measurement_requests req ON reqres.request_uid = req.uid
847-
WHERE req.operation_id = :operation_id
852+
WHERE req.operation_id IN :operation_ids
848853
""").bindparams( # noqa: S608 - self._tablename is not untrusted
849-
operation_id=operation_id
854+
sqlalchemy.bindparam(
855+
"operation_ids", value=list(operation_ids), expanding=True
856+
)
850857
)
851858

852859
try:
853860
with self.engine.begin() as connectable:
854861
cur = connectable.execute(query)
855862
except SQLAlchemyError as error:
856-
msg = f"Unable to fetch entities for operation {operation_id} from sample store {self._tablename}"
863+
msg = f"Unable to fetch entities for operations {operation_ids} from sample store {self._tablename}"
857864
self.log.critical(f"{msg}. Error: {error}")
858865
raise SystemError(f"{msg}. Error: {error}") from error
859866

@@ -903,6 +910,15 @@ def entities_in_operation(self, operation_id: str) -> list[Entity]:
903910

904911
return list(entities_dict.values())
905912

913+
def entities_in_operation(self, operation_ids: str | set[str]) -> list[Entity]:
914+
"""Deprecated: use entities_in_operations instead."""
915+
warnings.warn(
916+
"entities_in_operation is deprecated, use entities_in_operations instead.",
917+
DeprecationWarning,
918+
stacklevel=2,
919+
)
920+
return self.entities_in_operations(operation_ids)
921+
906922
@property
907923
def numberOfEntities(self) -> int:
908924

@@ -2122,29 +2138,56 @@ def experiments_in_operation(self, operation_id: str) -> list[Experiment]:
21222138
for e in cur
21232139
]
21242140

2125-
def entity_identifiers_in_operation(self, operation_id: str) -> set[str]:
2141+
def entity_identifiers_in_operations(
2142+
self, operation_ids: str | set[str]
2143+
) -> set[str]:
2144+
"""Get the set of entity identifiers sampled in one or more operations.
2145+
2146+
Args:
2147+
operation_ids: A single operation identifier or a set of operation
2148+
identifiers to look up entity identifiers for.
2149+
2150+
Returns:
2151+
Set of entity identifier strings across all specified operations.
2152+
"""
2153+
if isinstance(operation_ids, str):
2154+
operation_ids = {operation_ids}
2155+
21262156
try:
21272157
with self.engine.begin() as connectable:
21282158
query = sqlalchemy.text(f"""
21292159
SELECT DISTINCT(res.entity_id)
21302160
FROM (
21312161
SELECT *
21322162
FROM {self._tablename}_measurement_requests
2133-
WHERE operation_id = :operation_id
2163+
WHERE operation_id IN :operation_ids
21342164
) req
21352165
JOIN {self._tablename}_measurement_requests_results reqres ON reqres.request_uid = req.uid
21362166
JOIN {self._tablename}_measurement_results res ON reqres.result_uid = res.uid
21372167
""").bindparams( # noqa: S608 - self._tablename is not untrusted
2138-
operation_id=operation_id
2168+
sqlalchemy.bindparam(
2169+
"operation_ids", value=list(operation_ids), expanding=True
2170+
)
21392171
)
21402172
cur = connectable.execute(query)
21412173
except SQLAlchemyError as error:
2142-
msg = f"Unable to get the entity ids for operation {operation_id}"
2174+
msg = f"Unable to get the entity ids for operations {operation_ids}"
21432175
self.log.critical(f"{msg}. Error: {error}")
21442176
raise SystemError(f"{msg}. Error: {error}") from error
21452177

21462178
return {ident[0] for ident in cur}
21472179

2180+
def entity_identifiers_in_operation(
2181+
self, operation_ids: str | set[str]
2182+
) -> set[str]:
2183+
"""Deprecated: use entity_identifiers_in_operations instead."""
2184+
warnings.warn(
2185+
"entity_identifiers_in_operation is deprecated, use entity_identifiers_in_operations instead.",
2186+
DeprecationWarning,
2187+
stacklevel=2,
2188+
)
2189+
return self.entity_identifiers_in_operations(operation_ids)
2190+
21482191
def complete_measurement_request_with_results_timeseries(
21492192
self,
21502193
operation_id: str,

0 commit comments

Comments
 (0)