Coverage for python/lsst/daf/relation/_diagnostics.py: 18%

73 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-19 10:06 +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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("Diagnostics",) 

25 

26import dataclasses 

27from collections.abc import Callable 

28 

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 

34 

35 

36@dataclasses.dataclass 

37class Diagnostics: 

38 """A relation-processing algorithm that attempts to explain why a relation 

39 has no rows. 

40 

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 """ 

49 

50 is_doomed: bool 

51 """Whether the given relation will have no rows (`bool`). 

52 """ 

53 

54 messages: list[str] 

55 """Messages that explain why the relation will have fewer rows that it 

56 might have (`list` [ `str` ]). 

57 

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 """ 

63 

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. 

67 

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. 

78 

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")