Coverage for python/lsst/daf/relation/_operations/_selection.py: 50%

53 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-21 09:39 +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# (http://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 <http://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("Selection",) 

25 

26import dataclasses 

27from collections.abc import Set 

28from typing import TYPE_CHECKING, final 

29 

30from .._columns import ColumnTag, Predicate, flatten_logical_and 

31from .._exceptions import ColumnError 

32from .._operation_relations import UnaryOperationRelation 

33from .._unary_operation import Identity, RowFilter, UnaryCommutator, UnaryOperation 

34 

35if TYPE_CHECKING: 

36 from .._engine import Engine 

37 from .._relation import Relation 

38 

39 

40@final 

41@dataclasses.dataclass(frozen=True) 

42class Selection(RowFilter): 

43 """A relation operation that filters rows according to a boolean column 

44 expression. 

45 """ 

46 

47 predicate: Predicate 

48 """Boolean column expression that evaluates to `True` for rows to be 

49 kept and `False` for rows to be filtered out (`Predicate`). 

50 """ 

51 

52 def __post_init__(self) -> None: 

53 # Simplify-out nested ANDs and literal True/False values. 

54 if (and_sequence := flatten_logical_and(self.predicate)) is not False: 

55 object.__setattr__(self, "predicate", Predicate.logical_and(*and_sequence)) 

56 

57 @property 

58 def columns_required(self) -> Set[ColumnTag]: 

59 # Docstring inherited. 

60 return self.predicate.columns_required 

61 

62 @property 

63 def is_empty_invariant(self) -> bool: 

64 # Docstring inherited. 

65 return False 

66 

67 @property 

68 def is_order_dependent(self) -> bool: 

69 # Docstring inherited. 

70 return False 

71 

72 def __str__(self) -> str: 

73 return f"σ[{self.predicate}]" 

74 

75 def is_supported_by(self, engine: Engine) -> bool: 

76 # Docstring inherited. 

77 return self.predicate.is_supported_by(engine) 

78 

79 def _begin_apply( 

80 self, target: Relation, preferred_engine: Engine | None 

81 ) -> tuple[UnaryOperation, Engine]: 

82 # Docstring inherited. 

83 if self.predicate.as_trivial() is True: 

84 return Identity(), target.engine 

85 # We don't simplify the trivially-false predicate case, in keeping with 

86 # our policy of leaving doomed relations in place for diagnostics 

87 # to report on later. 

88 if not self.predicate.columns_required <= target.columns: 

89 raise ColumnError( 

90 f"Predicate {self.predicate} for target relation {target} needs " 

91 f"columns {self.predicate.columns_required - target.columns}." 

92 ) 

93 return super()._begin_apply(target, preferred_engine) 

94 

95 def _finish_apply(self, target: Relation) -> Relation: 

96 # Docstring inherited. 

97 if self.predicate.as_trivial() is True: 

98 return target 

99 else: 

100 return super()._finish_apply(target) 

101 

102 def applied_min_rows(self, target: Relation) -> int: 

103 # Docstring inherited. 

104 return 0 

105 

106 def commute(self, current: UnaryOperationRelation) -> UnaryCommutator: 

107 # Docstring inherited. 

108 if not self.columns_required <= current.target.columns: 

109 return UnaryCommutator( 

110 first=None, 

111 second=current.operation, 

112 done=False, 

113 messages=( 

114 f"{current.target} is missing columns " 

115 f"{set(self.columns_required - current.target.columns)}", 

116 ), 

117 ) 

118 if current.operation.is_count_dependent: 

119 return UnaryCommutator( 

120 first=None, 

121 second=current.operation, 

122 done=False, 

123 messages=(f"{current.operation} is count-dependent",), 

124 ) 

125 return UnaryCommutator(self, current.operation) 

126 

127 def simplify(self, upstream: UnaryOperation) -> UnaryOperation | None: 

128 # Docstring inherited. 

129 match upstream: 

130 case Selection(predicate=other_predicate): 

131 return Selection(predicate=other_predicate.logical_and(self.predicate)) 

132 return None