Coverage for python / lsst / daf / butler / dimensions / _database.py: 46%
117 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 08:36 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 08:36 +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/>.
28from __future__ import annotations
30__all__ = (
31 "DatabaseDimension",
32 "DatabaseDimensionCombination",
33 "DatabaseDimensionElement",
34 "DatabaseTopologicalFamily",
35)
37from collections.abc import Iterable, Mapping, Set
38from types import MappingProxyType
39from typing import TYPE_CHECKING
41from lsst.utils.classes import cached_getter
43from .._named import NamedValueAbstractSet, NamedValueSet
44from .._topology import TopologicalFamily, TopologicalRelationshipEndpoint, TopologicalSpace
45from ._elements import Dimension, DimensionCombination, DimensionElement, KeyColumnSpec, MetadataColumnSpec
46from .construction import DimensionConstructionBuilder, DimensionConstructionVisitor
48if TYPE_CHECKING:
49 from ..queries.tree import DimensionFieldReference
50 from ._governor import GovernorDimension
51 from ._group import DimensionGroup
54class DatabaseTopologicalFamily(TopologicalFamily):
55 """Database topological family implementation.
57 A `TopologicalFamily` implementation for the `DatabaseDimension` and
58 `DatabaseDimensionCombination` objects that have direct database
59 representations.
61 Parameters
62 ----------
63 name : `str`
64 Name of the family.
65 space : `TopologicalSpace`
66 Space in which this family's regions live.
67 members : `NamedValueAbstractSet` [ `DimensionElement` ]
68 The members of this family, ordered according to the priority used
69 in `choose` (first-choice member first).
70 """
72 def __init__(
73 self,
74 name: str,
75 space: TopologicalSpace,
76 *,
77 members: NamedValueAbstractSet[DimensionElement],
78 ):
79 super().__init__(name, space)
80 self.members = members
82 def choose(self, dimensions: DimensionGroup) -> DimensionElement:
83 # Docstring inherited from TopologicalFamily.
84 for member in self.members:
85 if member.name in dimensions.elements:
86 return member
87 raise RuntimeError(f"No recognized endpoints for {self.name} in {dimensions}.")
89 @property
90 @cached_getter
91 def governor(self) -> GovernorDimension:
92 """Return `GovernorDimension` common to all members of this family.
94 (`GovernorDimension`).
95 """
96 governors = {m.governor for m in self.members}
97 if None in governors:
98 raise RuntimeError(
99 f"Bad {self.space.name} family definition {self.name}: at least one member "
100 f"in {self.members} has no GovernorDimension dependency."
101 )
102 try:
103 (result,) = governors
104 except ValueError:
105 raise RuntimeError(
106 f"Bad {self.space.name} family definition {self.name}: multiple governors {governors} "
107 f"in {self.members}."
108 ) from None
109 return result # type: ignore
111 def make_column_reference(self, endpoint: TopologicalRelationshipEndpoint) -> DimensionFieldReference:
112 # Docstring inherited from TopologicalFamily.
113 from ..queries.tree import DimensionFieldReference
115 assert isinstance(endpoint, DimensionElement)
116 return DimensionFieldReference(
117 element=endpoint,
118 field=("region" if self.space is TopologicalSpace.SPATIAL else "timespan"),
119 )
121 members: NamedValueAbstractSet[DimensionElement]
122 """The members of this family, ordered according to the priority used in
123 `choose` (first-choice member first).
124 """
127class DatabaseTopologicalFamilyConstructionVisitor(DimensionConstructionVisitor):
128 """A construction visitor for `DatabaseTopologicalFamily`.
130 This visitor depends on (and is thus visited after) its members.
132 Parameters
133 ----------
134 space : `TopologicalSpace`
135 Space in which this family's regions live.
136 members : `~collections.abc.Iterable` [ `str` ]
137 The names of the members of this family, ordered according to the
138 priority used in `choose` (first-choice member first).
139 """
141 def __init__(self, space: TopologicalSpace, members: Iterable[str]):
142 self._space = space
143 self._members = tuple(members)
145 def has_dependencies_in(self, others: Set[str]) -> bool:
146 # Docstring inherited from DimensionConstructionVisitor.
147 return not others.isdisjoint(self._members)
149 def visit(self, name: str, builder: DimensionConstructionBuilder) -> None:
150 # Docstring inherited from DimensionConstructionVisitor.
151 members = NamedValueSet(builder.elements[member_name] for member_name in self._members)
152 family = DatabaseTopologicalFamily(name, self._space, members=members.freeze())
153 builder.topology[self._space].add(family)
154 for member in members:
155 assert isinstance(member, DatabaseDimension | DatabaseDimensionCombination)
156 other = member._topology.setdefault(self._space, family)
157 if other is not family:
158 raise RuntimeError(
159 f"{member.name} is declared to be a member of (at least) two "
160 f"{self._space.name} families: {other.name} and {family.name}."
161 )
164class DatabaseDimensionElement(DimensionElement):
165 """An intermediate base class for `DimensionElement` database classes.
167 Instances of these element classes map directly to a database table or
168 query.
170 Parameters
171 ----------
172 name : `str`
173 Name of the dimension.
174 implied : `NamedValueAbstractSet` [ `Dimension` ]
175 Other dimensions whose keys are included in this dimension's (logical)
176 table as foreign keys.
177 metadata_columns : `NamedValueAbstractSet` [ `MetadataColumnSpec` ]
178 Field specifications for all non-key fields in this dimension's table.
179 is_cached : `bool`
180 Whether this element's records should be persistently cached in the
181 client.
182 doc : `str`
183 Extended description of this element.
184 """
186 def __init__(
187 self,
188 name: str,
189 *,
190 implied: NamedValueAbstractSet[Dimension],
191 metadata_columns: NamedValueAbstractSet[MetadataColumnSpec],
192 is_cached: bool,
193 doc: str,
194 ):
195 self._name = name
196 self._implied = implied
197 self._metadata_columns = metadata_columns
198 self._topology: dict[TopologicalSpace, DatabaseTopologicalFamily] = {}
199 self._is_cached = is_cached
200 self._doc = doc
202 @property
203 def name(self) -> str:
204 # Docstring inherited from TopologicalRelationshipEndpoint.
205 return self._name
207 @property
208 def implied(self) -> NamedValueAbstractSet[Dimension]:
209 # Docstring inherited from DimensionElement.
210 return self._implied
212 @property
213 def metadata_columns(self) -> NamedValueAbstractSet[MetadataColumnSpec]:
214 # Docstring inherited from DimensionElement.
215 return self._metadata_columns
217 @property
218 def is_cached(self) -> bool:
219 # Docstring inherited.
220 return self._is_cached
222 @property
223 def documentation(self) -> str:
224 # Docstring inherited from DimensionElement.
225 return self._doc
227 @property
228 def topology(self) -> Mapping[TopologicalSpace, DatabaseTopologicalFamily]:
229 # Docstring inherited from TopologicalRelationshipEndpoint
230 return MappingProxyType(self._topology)
232 @property
233 def spatial(self) -> DatabaseTopologicalFamily | None:
234 # Docstring inherited from TopologicalRelationshipEndpoint
235 return self.topology.get(TopologicalSpace.SPATIAL)
237 @property
238 def temporal(self) -> DatabaseTopologicalFamily | None:
239 # Docstring inherited from TopologicalRelationshipEndpoint
240 return self.topology.get(TopologicalSpace.TEMPORAL)
243class DatabaseDimension(Dimension, DatabaseDimensionElement):
244 """A `Dimension` class that maps directly to a database table or query.
246 Parameters
247 ----------
248 name : `str`
249 Name of the dimension.
250 required : `NamedValueSet` [ `Dimension` ]
251 Other dimensions whose keys are part of the compound primary key for
252 this dimension's (logical) table, as well as references to their own
253 tables. The ``required`` parameter does not include ``self`` (it
254 can't, of course), but the corresponding attribute does - it is added
255 by the constructor.
256 implied : `NamedValueAbstractSet` [ `Dimension` ]
257 Other dimensions whose keys are included in this dimension's (logical)
258 table as foreign keys.
259 metadata_columns : `NamedValueAbstractSet` [ `MetadataColumnSpec` ]
260 Field specifications for all non-key fields in this dimension's table.
261 unique_keys : `NamedValueAbstractSet` [ `KeyColumnSpec` ]
262 Fields that can each be used to uniquely identify this dimension (given
263 values for all required dimensions). The first of these is used as
264 (part of) this dimension's table's primary key, while others are used
265 to define unique constraints.
266 implied_union_target : `str` or `None`
267 If not `None`, the name of an element whose implied values for
268 this element form the set of allowable values.
269 is_cached : `bool`
270 Whether this element's records should be persistently cached in the
271 client.
272 doc : `str`
273 Extended description of this element.
275 Notes
276 -----
277 `DatabaseDimension` objects may belong to a `TopologicalFamily`, but it is
278 the responsibility of `DatabaseTopologicalFamilyConstructionVisitor` to
279 update the `~TopologicalRelationshipEndpoint.topology` attribute of their
280 members.
281 """
283 def __init__(
284 self,
285 name: str,
286 *,
287 required: NamedValueSet[Dimension],
288 implied: NamedValueAbstractSet[Dimension],
289 metadata_columns: NamedValueAbstractSet[MetadataColumnSpec],
290 unique_keys: NamedValueAbstractSet[KeyColumnSpec],
291 implied_union_target: str | None,
292 is_cached: bool,
293 doc: str,
294 ):
295 super().__init__(
296 name, implied=implied, metadata_columns=metadata_columns, is_cached=is_cached, doc=doc
297 )
298 required.add(self)
299 self._required = required.freeze()
300 self._unique_keys = unique_keys
301 self._implied_union_target = implied_union_target
303 @property
304 def required(self) -> NamedValueAbstractSet[Dimension]:
305 # Docstring inherited from DimensionElement.
306 return self._required
308 @property
309 def unique_keys(self) -> NamedValueAbstractSet[KeyColumnSpec]:
310 # Docstring inherited from Dimension.
311 return self._unique_keys
313 @property
314 def implied_union_target(self) -> DimensionElement | None:
315 # Docstring inherited from DimensionElement.
316 # This is a bit encapsulation-breaking, but it'll all be cleaned up
317 # soon when we get rid of the storage objects entirely.
318 return self.universe[self._implied_union_target] if self._implied_union_target is not None else None
321class DatabaseDimensionCombination(DimensionCombination, DatabaseDimensionElement):
322 """A combination class that maps directly to a database table or query.
324 Parameters
325 ----------
326 name : `str`
327 Name of the dimension.
328 required : `NamedValueAbstractSet` [ `Dimension` ]
329 Dimensions whose keys define the compound primary key for this
330 combinations's (logical) table, as well as references to their own
331 tables.
332 implied : `NamedValueAbstractSet` [ `Dimension` ]
333 Dimensions whose keys are included in this combinations's (logical)
334 table as foreign keys.
335 metadata_columns : `NamedValueAbstractSet` [ `MetadataColumnSpec` ]
336 Field specifications for all non-key fields in this combination's
337 table.
338 is_cached : `bool`
339 Whether this element's records should be persistently cached in the
340 client.
341 always_join : `bool`, optional
342 If `True`, always include this element in any query or data ID in
343 which its ``required`` dimensions appear, because it defines a
344 relationship between those dimensions that must always be satisfied.
345 populated_by : `Dimension` or `None`
346 The dimension that this element's records are always inserted,
347 exported, and imported alongside.
348 doc : `str`
349 Extended description of this element.
351 Notes
352 -----
353 `DatabaseDimensionCombination` objects may belong to a `TopologicalFamily`,
354 but it is the responsibility of
355 `DatabaseTopologicalFamilyConstructionVisitor` to update the
356 `~TopologicalRelationshipEndpoint.topology` attribute of their members.
358 This class has a lot in common with `DatabaseDimension`, but they are
359 expected to diverge in future changes, and the only way to make them share
360 method implementations would be via multiple inheritance. Given the
361 trivial nature of all of those implementations, this does not seem worth
362 the drawbacks (particularly the constraints it imposes on constructor
363 signatures).
364 """
366 def __init__(
367 self,
368 name: str,
369 *,
370 required: NamedValueAbstractSet[Dimension],
371 implied: NamedValueAbstractSet[Dimension],
372 metadata_columns: NamedValueAbstractSet[MetadataColumnSpec],
373 is_cached: bool,
374 always_join: bool,
375 populated_by: Dimension | None,
376 doc: str,
377 ):
378 super().__init__(
379 name, implied=implied, metadata_columns=metadata_columns, is_cached=is_cached, doc=doc
380 )
381 self._required = required
382 self._always_join = always_join
383 self._populated_by = populated_by
385 @property
386 def required(self) -> NamedValueAbstractSet[Dimension]:
387 # Docstring inherited from DimensionElement.
388 return self._required
390 @property
391 def alwaysJoin(self) -> bool:
392 # Docstring inherited from DimensionElement.
393 return self._always_join
395 @property
396 def defines_relationships(self) -> bool:
397 # Docstring inherited from DimensionElement.
398 return self._always_join or bool(self.implied)
400 @property
401 def populated_by(self) -> Dimension | None:
402 # Docstring inherited.
403 return self._populated_by