Coverage for python / lsst / daf / butler / queries / tree / _base.py: 84%

58 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-26 08:49 +0000

1# This file is part of daf_butler. 

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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ( 

31 "ANY_DATASET", 

32 "DATASET_FIELD_NAMES", 

33 "AnyDatasetFieldName", 

34 "AnyDatasetType", 

35 "ColumnExpressionBase", 

36 "DatasetFieldName", 

37 "QueryTreeBase", 

38 "is_dataset_field", 

39) 

40 

41import enum 

42from abc import ABC, abstractmethod 

43from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeAlias, TypeGuard, TypeVar, cast, get_args 

44 

45import pydantic 

46 

47from ...column_spec import ColumnType 

48 

49if TYPE_CHECKING: 

50 from ..visitors import ColumnExpressionVisitor 

51 from ._column_literal import ColumnLiteral 

52 from ._column_set import ColumnSet 

53 

54 

55# Type annotation for string literals that can be used as dataset fields in 

56# the public API. The 'collection' and 'run' fields are string collection 

57# names. Internal interfaces may define other dataset field strings (e.g. 

58# collection primary key values) and hence should use `str` rather than this 

59# type. 

60DatasetFieldName: TypeAlias = Literal["dataset_id", "ingest_date", "run", "collection", "timespan"] 

61InternalDatasetFieldName: TypeAlias = Literal["calib_pkey", "collection_key"] 

62AnyDatasetFieldName: TypeAlias = DatasetFieldName | InternalDatasetFieldName 

63 

64# Tuple of the strings that can be use as dataset fields in public APIs. 

65DATASET_FIELD_NAMES: tuple[DatasetFieldName, ...] = tuple(get_args(DatasetFieldName)) 

66 

67_T = TypeVar("_T") 

68_L = TypeVar("_L") 

69_A = TypeVar("_A") 

70_O = TypeVar("_O") 

71 

72 

73class AnyDatasetType(enum.Enum): 

74 """A single-element enum that provides a typed sentinal dataset type name 

75 for queries over multiple dataset types. 

76 """ 

77 

78 # Since we often use a type-union of ANY_DATASET and str, it's critical 

79 # the enum value (which is what Pydantic uses for serialization) not be 

80 # a str. 

81 ANY_DATASET = -1 

82 

83 def __str__(self) -> str: 

84 # This string appears in both human-read error messages and the aliases 

85 # used for columns in SQL (with quotes, of course). This is a 

86 # convenient value because it sorts before and cannot clash with any 

87 # regular alphabetical name, while still being human readable. 

88 return "[any]" 

89 

90 def __repr__(self) -> str: 

91 return "ANY_DATASET" 

92 

93 

94ANY_DATASET: AnyDatasetType = AnyDatasetType.ANY_DATASET 

95 

96 

97def is_dataset_field(s: str) -> TypeGuard[DatasetFieldName]: 

98 """Validate a field name. 

99 

100 Parameters 

101 ---------- 

102 s : `str` 

103 The field name to test. 

104 

105 Returns 

106 ------- 

107 is_field : `bool` 

108 Whether or not this is a dataset field. 

109 """ 

110 return s in DATASET_FIELD_NAMES 

111 

112 

113class QueryTreeBase(pydantic.BaseModel): 

114 """Base class for all non-primitive types in a query tree.""" 

115 

116 model_config = pydantic.ConfigDict(frozen=True, strict=True) 

117 

118 

119class ColumnExpressionBase(QueryTreeBase, ABC): 

120 """Base class for objects that represent non-boolean column expressions in 

121 a query tree. 

122 

123 Notes 

124 ----- 

125 This is a closed hierarchy whose concrete, `~typing.final` derived classes 

126 are members of the `ColumnExpression` union. That union should be used in 

127 type annotations rather than the technically-open base class. 

128 """ 

129 

130 expression_type: str 

131 """String literal corresponding to a concrete expression type.""" 

132 

133 is_literal: ClassVar[bool] = False 

134 """Whether this expression wraps a literal Python value.""" 

135 

136 is_column_reference: ClassVar[bool] = False 

137 """Whether this expression wraps a direct reference to column.""" 

138 

139 @property 

140 @abstractmethod 

141 def column_type(self) -> ColumnType: 

142 """A string enumeration value representing the type of the column 

143 expression. 

144 """ 

145 raise NotImplementedError() 

146 

147 def get_literal_value(self) -> Any | None: 

148 """Return the literal value wrapped by this expression, or `None` if 

149 it is not a literal. 

150 """ 

151 return None 

152 

153 @abstractmethod 

154 def gather_required_columns(self, columns: ColumnSet) -> None: 

155 """Add any columns required to evaluate this expression to the 

156 given column set. 

157 

158 Parameters 

159 ---------- 

160 columns : `ColumnSet` 

161 Set of columns to modify in place. 

162 """ 

163 raise NotImplementedError() 

164 

165 @abstractmethod 

166 def gather_governors(self, governors: set[str]) -> None: 

167 """Add any governor dimensions that need to be fully identified for 

168 this column expression to be sound. 

169 

170 Parameters 

171 ---------- 

172 governors : `set` [ `str` ] 

173 Set of governor dimension names to modify in place. 

174 """ 

175 raise NotImplementedError() 

176 

177 @abstractmethod 

178 def visit(self, visitor: ColumnExpressionVisitor[_T]) -> _T: 

179 """Invoke the visitor interface. 

180 

181 Parameters 

182 ---------- 

183 visitor : `ColumnExpressionVisitor` 

184 Visitor to invoke a method on. 

185 

186 Returns 

187 ------- 

188 result : `object` 

189 Forwarded result from the visitor. 

190 """ 

191 raise NotImplementedError() 

192 

193 

194class ColumnLiteralBase(ColumnExpressionBase): 

195 """Base class for objects that represent literal values as column 

196 expressions in a query tree. 

197 

198 Notes 

199 ----- 

200 This is a closed hierarchy whose concrete, `~typing.final` derived classes 

201 are members of the `ColumnLiteral` union. That union should be used in 

202 type annotations rather than the technically-open base class. The concrete 

203 members of that union are only semi-public; they appear in the serialized 

204 form of a column expression tree, but should only be constructed via the 

205 `make_column_literal` factory function. All concrete members of the union 

206 are also guaranteed to have a read-only ``value`` attribute holding the 

207 wrapped literal, but it is unspecified whether that is a regular attribute 

208 or a `property` (and hence cannot be type-annotated). 

209 """ 

210 

211 is_literal: ClassVar[bool] = True 

212 """Whether this expression wraps a literal Python value.""" 

213 

214 def get_literal_value(self) -> Any: 

215 # Docstring inherited. 

216 return cast("ColumnLiteral", self).value 

217 

218 def gather_required_columns(self, columns: ColumnSet) -> None: 

219 # Docstring inherited. 

220 pass 

221 

222 def gather_governors(self, governors: set[str]) -> None: 

223 # Docstring inherited. 

224 pass 

225 

226 @property 

227 def column_type(self) -> ColumnType: 

228 # Docstring inherited. 

229 return cast(ColumnType, self.expression_type) 

230 

231 def visit(self, visitor: ColumnExpressionVisitor[_T]) -> _T: 

232 # Docstring inherited 

233 return visitor.visit_literal(cast("ColumnLiteral", self))