Coverage for python/lsst/daf/relation/_diagnostics.py: 16%
74 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-10 10:30 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-10 10:30 +0000
1# This file is part of daf_relation.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22from __future__ import annotations
24__all__ = ("Diagnostics",)
26import dataclasses
27from collections.abc import Callable
29from ._leaf_relation import LeafRelation
30from ._marker_relation import MarkerRelation
31from ._operation_relations import BinaryOperationRelation, UnaryOperationRelation
32from ._operations import Chain, Join, Selection, Slice
33from ._relation import Relation
36@dataclasses.dataclass
37class Diagnostics:
38 """A relation-processing algorithm that attempts to explain why a relation
39 has no rows.
41 The `Diagnostics` class itself is just the type returned by its `run` class
42 method, which performs a depth-first tree traversal looking for relations
43 that are either known in advance to have no rows (`Relation.max_rows` zero)
44 or shown to have no rows via an ``executor`` callable; when present, these
45 are then propagated downstream to the root. Only operations that can
46 remove all rows (`UnaryOperation.is_empty_invariant` is `False`) are
47 executed when no empty leaf relations are found.x
48 """
50 is_doomed: bool
51 """Whether the given relation will have no rows (`bool`).
52 """
54 messages: list[str]
55 """Messages that explain why the relation will have fewer rows that it
56 might have (`list` [ `str` ]).
58 This is usually non-empty only when the relation is doomed, but in rare
59 cases it can also provide information about what's missing when the
60 relation is not doomed, such as when only one of the two branches of a
61 `Chain` operation are doomed.
62 """
64 @classmethod
65 def run(cls, relation: Relation, executor: Callable[[Relation], bool] | None = None) -> Diagnostics:
66 """Report on whether the given relation has no rows, and if so, why.
68 Parameters
69 ----------
70 relation : `Relation`
71 Relation to analyze.
72 executor : `Callable`, optional
73 If provided, a callable that takes a `Relation` and does some
74 engine-specific processing to determine whether it has any rows,
75 such as a ``LIMIT 1`` query in SQL. If not provided, diagnostics
76 will be based only on relations with `Relation.max_rows` set to
77 zero.
79 Returns
80 -------
81 diagnostics : `Diagnostics`
82 Struct containing the diagnostics report.
83 """
84 match relation:
85 case LeafRelation(messages=messages):
86 messages = list(messages)
87 if relation.max_rows == 0:
88 if not messages:
89 messages.append(f"Relation '{relation!s}' has no rows (static).")
90 return cls(True, messages)
91 elif executor is not None and not executor(relation):
92 if not messages:
93 messages.append(f"Relation '{relation!s}' has no rows (executed).")
94 return cls(True, messages)
95 else:
96 return cls(False, messages)
97 case MarkerRelation(target=target):
98 return cls.run(target, executor)
99 case UnaryOperationRelation(operation=operation, target=target):
100 if (result := cls.run(target, executor)).is_doomed:
101 return result
102 if relation.max_rows == 0 and operation.applied_max_rows(target) != 0:
103 result.messages.append(f"Unary operation relation {relation} has no rows (static).")
104 return cls(True, result.messages)
105 match operation:
106 case Slice(limit=limit):
107 if limit == 0:
108 result.is_doomed = True
109 result.messages.append(f"Slice with limit=0 applied to '{target!s}'")
110 return result
111 case Selection(predicate=predicate):
112 if predicate.as_trivial() is False:
113 result.is_doomed = True
114 result.messages.append(
115 f"Predicate '{predicate}' is trivially false (applied to '{target!s}')"
116 )
117 return result
118 if not operation.is_empty_invariant and executor is not None and not executor(relation):
119 result.is_doomed = True
120 result.messages.append(
121 f"Operation {operation} yields no results when applied to '{target!s}'"
122 )
123 return result
124 return result
125 case BinaryOperationRelation(operation=operation, lhs=lhs, rhs=rhs):
126 lhs_result = cls.run(lhs, executor)
127 rhs_result = cls.run(rhs, executor)
128 messages = lhs_result.messages + rhs_result.messages
129 if relation.max_rows == 0 and operation.applied_max_rows(lhs, rhs) != 0:
130 messages.append(f"Binary operation relation '{relation}' has no rows (static).")
131 return cls(True, messages)
132 match operation:
133 case Chain():
134 return cls(lhs_result.is_doomed and rhs_result.is_doomed, messages)
135 case Join(predicate=predicate):
136 if lhs_result.is_doomed or rhs_result.is_doomed:
137 return cls(True, messages)
138 if predicate.as_trivial() is False:
139 messages.append(
140 f"Join predicate '{predicate}' is trivially false in '{relation}'."
141 )
142 return cls(True, messages)
143 if executor is not None and not executor(relation):
144 messages.append(f"Operation {operation} yields no results when executed: '{relation!s}'")
145 return cls(True, messages)
146 return cls(False, messages)
147 raise AssertionError("match should be exhaustive and all branches should return")