Skip to content

Commit 001effd

Browse files
committed
QueryBuilder: fix outer join filters and refactor process list
1 parent dd1e102 commit 001effd

8 files changed

Lines changed: 107 additions & 53 deletions

File tree

src/aiida/cmdline/commands/cmd_process.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def verdi_process():
8686
@options.FAILED()
8787
@options.PAST_DAYS()
8888
@options.LIMIT()
89-
@options.ROOT()
89+
@options.ONLY_ROOTS()
9090
@options.RAW()
9191
@click.pass_context
9292
@decorators.with_dbenv()
@@ -101,7 +101,7 @@ def process_list(
101101
failed,
102102
past_days,
103103
limit,
104-
root,
104+
only_roots,
105105
project,
106106
raw,
107107
order_by,
@@ -127,7 +127,9 @@ def process_list(
127127
relationships['with_node'] = group
128128

129129
builder = CalculationQueryBuilder()
130-
filters = builder.get_filters(all_entries, process_state, process_label, paused, exit_status, failed, root=root)
130+
filters = builder.get_filters(
131+
all_entries, process_state, process_label, paused, exit_status, failed, only_roots=only_roots
132+
)
131133
query_set = builder.get_query_set(
132134
relationships=relationships, filters=filters, order_by={order_by: order_dir}, past_days=past_days, limit=limit
133135
)

src/aiida/cmdline/params/options/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
'NODES',
8383
'NON_INTERACTIVE',
8484
'OLDER_THAN',
85+
'ONLY_ROOTS',
8586
'ONLY_TOP_LEVEL_CALCS',
8687
'ONLY_TOP_LEVEL_WORKFLOWS',
8788
'ORDER_BY',
@@ -103,7 +104,6 @@
103104
'RAW',
104105
'RELABEL_GROUPS',
105106
'REPOSITORY_PATH',
106-
'ROOT',
107107
'SCHEDULER',
108108
'SILENT',
109109
'SORT',

src/aiida/cmdline/params/options/main.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
'NODES',
9494
'NON_INTERACTIVE',
9595
'OLDER_THAN',
96+
'ONLY_ROOTS',
9697
'ONLY_TOP_LEVEL_CALCS',
9798
'ONLY_TOP_LEVEL_WORKFLOWS',
9899
'ORDER_BY',
@@ -114,7 +115,6 @@
114115
'RAW',
115116
'RELABEL_GROUPS',
116117
'REPOSITORY_PATH',
117-
'ROOT',
118118
'SCHEDULER',
119119
'SILENT',
120120
'SORT',
@@ -643,9 +643,9 @@ def set_log_level(ctx: click.Context, _param: click.Parameter, value: t.Any) ->
643643
default=False,
644644
help='Include all entries, disregarding all other filter options and flags.',
645645
)
646-
ROOT = OverridableOption(
647-
'--root',
648-
'root',
646+
ONLY_ROOTS = OverridableOption(
647+
'--only-roots',
648+
'only_roots',
649649
is_flag=True,
650650
default=False,
651651
help='Only include root processes (those without a caller).',

src/aiida/orm/querybuilder.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -426,11 +426,11 @@ def append(
426426
# So for those cases we need to construct the correct filters,
427427
# corresponding to the provided classes and value of `subclassing`.
428428
if ormclass == EntityTypes.NODE:
429-
self._add_node_type_filter(tag, classifiers, subclassing)
430-
self._add_process_type_filter(tag, classifiers, subclassing)
429+
self._add_node_type_filter(tag, classifiers, subclassing, outerjoin=outerjoin)
430+
self._add_process_type_filter(tag, classifiers, subclassing, outerjoin=outerjoin)
431431

432432
elif ormclass == EntityTypes.GROUP:
433-
self._add_group_type_filter(tag, classifiers, subclassing)
433+
self._add_group_type_filter(tag, classifiers, subclassing, outerjoin=outerjoin)
434434

435435
# The order has to be first _add_node_type_filter and then add_filter.
436436
# If the user adds a query on the type column, it overwrites what I did
@@ -733,12 +733,15 @@ def _process_filters(filters: FilterType) -> Dict[str, Any]:
733733

734734
return processed_filters
735735

736-
def _add_node_type_filter(self, tagspec: str, classifiers: List[Classifier], subclassing: bool):
736+
def _add_node_type_filter(
737+
self, tagspec: str, classifiers: List[Classifier], subclassing: bool, outerjoin: bool = False
738+
):
737739
"""Add a filter based on node type.
738740
739741
:param tagspec: The tag, which has to exist already as a key in self._filters
740742
:param classifiers: a dictionary with classifiers
741743
:param subclassing: if True, allow for subclasses of the ormclass
744+
:param outerjoin: if True, wrap the filter with OR id IS NULL
742745
"""
743746
if len(classifiers) > 1:
744747
# If a list was passed to QueryBuilder.append, this propagates to a list in the classifiers
@@ -748,14 +751,21 @@ def _add_node_type_filter(self, tagspec: str, classifiers: List[Classifier], sub
748751
else:
749752
entity_type_filter = _get_node_type_filter(classifiers[0], subclassing)
750753

751-
self.add_filter(tagspec, {'node_type': entity_type_filter})
754+
filters: dict = {'node_type': entity_type_filter}
755+
if outerjoin:
756+
filters = {'or': [filters, {'id': None}]}
752757

753-
def _add_process_type_filter(self, tagspec: str, classifiers: List[Classifier], subclassing: bool) -> None:
758+
self.add_filter(tagspec, filters)
759+
760+
def _add_process_type_filter(
761+
self, tagspec: str, classifiers: List[Classifier], subclassing: bool, outerjoin: bool = False
762+
) -> None:
754763
"""Add a filter based on process type.
755764
756765
:param tagspec: The tag, which has to exist already as a key in self._filters
757766
:param classifiers: a dictionary with classifiers
758767
:param subclassing: if True, allow for subclasses of the process type
768+
:param outerjoin: if True, wrap the filter with OR id IS NULL
759769
760770
Note: This function handles the case when process_type_string is None.
761771
"""
@@ -767,28 +777,41 @@ def _add_process_type_filter(self, tagspec: str, classifiers: List[Classifier],
767777
process_type_filter['or'].append(_get_process_type_filter(classifier, subclassing))
768778

769779
if len(process_type_filter['or']) > 0:
770-
self.add_filter(tagspec, {'process_type': process_type_filter})
780+
filters: dict = {'process_type': process_type_filter}
781+
if outerjoin:
782+
filters = {'or': [filters, {'id': None}]}
783+
self.add_filter(tagspec, filters)
771784

772785
elif classifiers[0].process_type_string is not None:
773786
process_type_filter = _get_process_type_filter(classifiers[0], subclassing)
774-
self.add_filter(tagspec, {'process_type': process_type_filter})
787+
filters = {'process_type': process_type_filter}
788+
if outerjoin:
789+
filters = {'or': [filters, {'id': None}]}
790+
self.add_filter(tagspec, filters)
775791

776-
def _add_group_type_filter(self, tagspec: str, classifiers: List[Classifier], subclassing: bool) -> None:
792+
def _add_group_type_filter(
793+
self, tagspec: str, classifiers: List[Classifier], subclassing: bool, outerjoin: bool = False
794+
) -> None:
777795
"""Add a filter based on group type.
778796
779797
:param tagspec: The tag, which has to exist already as a key in self._filters
780798
:param classifiers: a dictionary with classifiers
781799
:param subclassing: if True, allow for subclasses of the ormclass
800+
:param outerjoin: if True, wrap the filter with OR id IS NULL
782801
"""
783802
if len(classifiers) > 1:
784803
# If a list was passed to QueryBuilder.append, this propagates to a list in the classifiers
785-
type_string_filter: dict = {'or': []}
804+
entity_type_filter: dict = {'or': []}
786805
for classifier in classifiers:
787-
type_string_filter['or'].append(_get_group_type_filter(classifier, subclassing))
806+
entity_type_filter['or'].append(_get_group_type_filter(classifier, subclassing))
788807
else:
789-
type_string_filter = _get_group_type_filter(classifiers[0], subclassing)
808+
entity_type_filter = _get_group_type_filter(classifiers[0], subclassing)
809+
810+
filters: dict = {'type_string': entity_type_filter}
811+
if outerjoin:
812+
filters = {'or': [filters, {'id': None}]}
790813

791-
self.add_filter(tagspec, {'type_string': type_string_filter})
814+
self.add_filter(tagspec, filters)
792815

793816
def add_projection(self, tag_spec: Union[str, EntityClsType], projection_spec: ProjectType) -> None:
794817
r"""Adds a projection

src/aiida/storage/psql_dos/orm/querybuilder/joiner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ def _join_node_inputs(self, joined_entity, entity_to_join, isouterjoin: bool, **
378378
aliased_edge = aliased(self._entities.Link)
379379

380380
def new_query(q):
381-
return q.join(aliased_edge, aliased_edge.output_id == joined_entity.id).join(
381+
return q.join(aliased_edge, aliased_edge.output_id == joined_entity.id, isouter=isouterjoin).join(
382382
entity_to_join, aliased_edge.input_id == entity_to_join.id, isouter=isouterjoin
383383
)
384384

src/aiida/storage/psql_dos/orm/querybuilder/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ def _build(self, data: QueryDictType) -> BuiltQuery:
328328
for tag, filter_specs in data['filters'].items():
329329
if not filter_specs:
330330
continue
331+
331332
alias = tag_to_alias.get(tag)
332333
if not alias:
333334
raise ValueError(f'Unknown tag {tag!r} in filters, known: {list(tag_to_alias)}')

src/aiida/tools/query/calculation.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@
1313
import typing as t
1414
from collections.abc import Iterable
1515

16-
from aiida.common.lang import classproperty
17-
1816
from aiida import orm
1917
from aiida.common import timezone
20-
from aiida.common.links import LinkType
18+
from aiida.common.lang import classproperty
2119

2220
from .mapping import CalculationProjectionMapper, ProjectionMapper
2321

@@ -86,7 +84,7 @@ def get_filters(
8684
exit_status: str | None = None,
8785
failed: bool = False,
8886
node_types: list[Node] | None = None,
89-
root: bool = False,
87+
only_roots: bool = False,
9088
) -> dict[str, t.Any]:
9189
"""Return a set of QueryBuilder filters based on typical command line options.
9290
@@ -97,7 +95,7 @@ def get_filters(
9795
:param paused: Boolean, if True, filter for processes that are paused.
9896
:param exit_status: Filter for this exit status.
9997
:param failed: Boolean to filter only failed processes.
100-
:param root: Boolean, if True, filter for root processes (those without a caller).
98+
:param only_roots: Boolean, if True, filter for root processes (those without a caller).
10199
:return: Dictionary of filters suitable for a QueryBuilder.append() call.
102100
"""
103101
from aiida.engine import ProcessState
@@ -134,8 +132,8 @@ def get_filters(
134132
filters[process_state_attribute] = {'==': ProcessState.FINISHED.value}
135133
filters[exit_status_attribute] = {'==': exit_status}
136134

137-
if root:
138-
filters['root'] = True
135+
if only_roots:
136+
filters['only_roots'] = True
139137

140138
return filters
141139

@@ -171,19 +169,18 @@ def get_query_set(
171169
if filters is None:
172170
filters = {}
173171

174-
root = filters.pop('root', False)
172+
only_roots = filters.pop('only_roots', False)
175173

176174
if past_days is not None:
177175
filters['ctime'] = {'>': timezone.now() - datetime.timedelta(days=past_days)}
178176

179177
builder = orm.QueryBuilder()
180178
builder.append(cls=orm.ProcessNode, filters=filters, project=unique_projections, tag='process')
181179

182-
if root:
180+
if only_roots:
183181
builder.append(
184182
orm.ProcessNode,
185183
with_outgoing='process',
186-
edge_filters={'type': {'in': (LinkType.CALL_WORK.value, LinkType.CALL_CALC.value)}},
187184
tag='caller',
188185
outerjoin=True,
189186
)

0 commit comments

Comments
 (0)