Coverage for python/lsst/daf/butler/queries/tree/_base.py: 84%
56 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-11 01:27 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-11 01:27 -0700
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/>.
28from __future__ import annotations
30__all__ = (
31 "ANY_DATASET",
32 "DATASET_FIELD_NAMES",
33 "AnyDatasetType",
34 "ColumnExpressionBase",
35 "DatasetFieldName",
36 "QueryTreeBase",
37 "is_dataset_field",
38)
40import enum
41from abc import ABC, abstractmethod
42from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeAlias, TypeGuard, TypeVar, cast, get_args
44import pydantic
46from ...column_spec import ColumnType
48if TYPE_CHECKING:
49 from ..visitors import ColumnExpressionVisitor
50 from ._column_literal import ColumnLiteral
51 from ._column_set import ColumnSet
54# Type annotation for string literals that can be used as dataset fields in
55# the public API. The 'collection' and 'run' fields are string collection
56# names. Internal interfaces may define other dataset field strings (e.g.
57# collection primary key values) and hence should use `str` rather than this
58# type.
59DatasetFieldName: TypeAlias = Literal["dataset_id", "ingest_date", "run", "collection", "timespan"]
61# Tuple of the strings that can be use as dataset fields in public APIs.
62DATASET_FIELD_NAMES: tuple[DatasetFieldName, ...] = tuple(get_args(DatasetFieldName))
64_T = TypeVar("_T")
65_L = TypeVar("_L")
66_A = TypeVar("_A")
67_O = TypeVar("_O")
70class AnyDatasetType(enum.Enum):
71 """A single-element enum that provides a typed sentinal dataset type name
72 for queries over multiple dataset types.
73 """
75 # Since we often use a type-union of ANY_DATASET and str, it's critical
76 # the enum value (which is what Pydantic uses for serialization) not be
77 # a str.
78 ANY_DATASET = -1
80 def __str__(self) -> str:
81 # This string appears in both human-read error messages and the aliases
82 # used for columns in SQL (with quotes, of course). This is a
83 # convenient value because it sorts before and cannot clash with any
84 # regular alphabetical name, while still being human readable.
85 return "[any]"
87 def __repr__(self) -> str:
88 return "ANY_DATASET"
91ANY_DATASET: AnyDatasetType = AnyDatasetType.ANY_DATASET
94def is_dataset_field(s: str) -> TypeGuard[DatasetFieldName]:
95 """Validate a field name.
97 Parameters
98 ----------
99 s : `str`
100 The field name to test.
102 Returns
103 -------
104 is_field : `bool`
105 Whether or not this is a dataset field.
106 """
107 return s in DATASET_FIELD_NAMES
110class QueryTreeBase(pydantic.BaseModel):
111 """Base class for all non-primitive types in a query tree."""
113 model_config = pydantic.ConfigDict(frozen=True, strict=True)
116class ColumnExpressionBase(QueryTreeBase, ABC):
117 """Base class for objects that represent non-boolean column expressions in
118 a query tree.
120 Notes
121 -----
122 This is a closed hierarchy whose concrete, `~typing.final` derived classes
123 are members of the `ColumnExpression` union. That union should be used in
124 type annotations rather than the technically-open base class.
125 """
127 expression_type: str
128 """String literal corresponding to a concrete expression type."""
130 is_literal: ClassVar[bool] = False
131 """Whether this expression wraps a literal Python value."""
133 is_column_reference: ClassVar[bool] = False
134 """Whether this expression wraps a direct reference to column."""
136 @property
137 @abstractmethod
138 def column_type(self) -> ColumnType:
139 """A string enumeration value representing the type of the column
140 expression.
141 """
142 raise NotImplementedError()
144 def get_literal_value(self) -> Any | None:
145 """Return the literal value wrapped by this expression, or `None` if
146 it is not a literal.
147 """
148 return None
150 @abstractmethod
151 def gather_required_columns(self, columns: ColumnSet) -> None:
152 """Add any columns required to evaluate this expression to the
153 given column set.
155 Parameters
156 ----------
157 columns : `ColumnSet`
158 Set of columns to modify in place.
159 """
160 raise NotImplementedError()
162 @abstractmethod
163 def gather_governors(self, governors: set[str]) -> None:
164 """Add any governor dimensions that need to be fully identified for
165 this column expression to be sound.
167 Parameters
168 ----------
169 governors : `set` [ `str` ]
170 Set of governor dimension names to modify in place.
171 """
172 raise NotImplementedError()
174 @abstractmethod
175 def visit(self, visitor: ColumnExpressionVisitor[_T]) -> _T:
176 """Invoke the visitor interface.
178 Parameters
179 ----------
180 visitor : `ColumnExpressionVisitor`
181 Visitor to invoke a method on.
183 Returns
184 -------
185 result : `object`
186 Forwarded result from the visitor.
187 """
188 raise NotImplementedError()
191class ColumnLiteralBase(ColumnExpressionBase):
192 """Base class for objects that represent literal values as column
193 expressions in a query tree.
195 Notes
196 -----
197 This is a closed hierarchy whose concrete, `~typing.final` derived classes
198 are members of the `ColumnLiteral` union. That union should be used in
199 type annotations rather than the technically-open base class. The concrete
200 members of that union are only semi-public; they appear in the serialized
201 form of a column expression tree, but should only be constructed via the
202 `make_column_literal` factory function. All concrete members of the union
203 are also guaranteed to have a read-only ``value`` attribute holding the
204 wrapped literal, but it is unspecified whether that is a regular attribute
205 or a `property` (and hence cannot be type-annotated).
206 """
208 is_literal: ClassVar[bool] = True
209 """Whether this expression wraps a literal Python value."""
211 def get_literal_value(self) -> Any:
212 # Docstring inherited.
213 return cast("ColumnLiteral", self).value
215 def gather_required_columns(self, columns: ColumnSet) -> None:
216 # Docstring inherited.
217 pass
219 def gather_governors(self, governors: set[str]) -> None:
220 # Docstring inherited.
221 pass
223 @property
224 def column_type(self) -> ColumnType:
225 # Docstring inherited.
226 return cast(ColumnType, self.expression_type)
228 def visit(self, visitor: ColumnExpressionVisitor[_T]) -> _T:
229 # Docstring inherited
230 return visitor.visit_literal(cast("ColumnLiteral", self))