Coverage for python/lsst/daf/butler/core/dimensions/_schema.py : 19%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 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/>.
21from __future__ import annotations
23__all__ = (
24 "addDimensionForeignKey",
25)
27import copy
29from typing import Tuple, Type, TYPE_CHECKING
31from .. import ddl
32from ..named import NamedValueSet
33from .._topology import SpatialRegionDatabaseRepresentation
34from ..timespan import TimespanDatabaseRepresentation
36if TYPE_CHECKING: # Imports needed only for type annotations; may be circular. 36 ↛ 37line 36 didn't jump to line 37, because the condition on line 36 was never true
37 from ._elements import DimensionElement, Dimension
40def _makeForeignKeySpec(dimension: Dimension) -> ddl.ForeignKeySpec:
41 """Make a `ddl.ForeignKeySpec`.
43 This will reference the table for the given `Dimension` table.
45 Most callers should use the higher-level `addDimensionForeignKey` function
46 instead.
48 Parameters
49 ----------
50 dimension : `Dimension`
51 The dimension to be referenced. Caller guarantees that it is actually
52 associated with a table.
54 Returns
55 -------
56 spec : `ddl.ForeignKeySpec`
57 A database-agnostic foreign key specification.
58 """
59 source = []
60 target = []
61 for other in dimension.required:
62 if other == dimension:
63 target.append(dimension.primaryKey.name)
64 else:
65 target.append(other.name)
66 source.append(other.name)
67 return ddl.ForeignKeySpec(table=dimension.name, source=tuple(source), target=tuple(target))
70def addDimensionForeignKey(tableSpec: ddl.TableSpec, dimension: Dimension, *,
71 primaryKey: bool, nullable: bool = False, constraint: bool = True
72 ) -> ddl.FieldSpec:
73 """Add a field and possibly a foreign key to a table specification.
75 The field will reference the table for the given `Dimension`.
77 Parameters
78 ----------
79 tableSpec : `ddl.TableSpec`
80 Specification the field and foreign key are to be added to.
81 dimension : `Dimension`
82 Dimension to be referenced. If this dimension has required
83 dependencies, those must have already been added to the table. A field
84 will be added that correspond to this dimension's primary key, and a
85 foreign key constraint will be added only if the dimension is
86 associated with a table of its own.
87 primaryKey : `bool`
88 If `True`, the new field will be added as part of a compound primary
89 key for the table.
90 nullable : `bool`, optional
91 If `False` (default) the new field will be added with a NOT NULL
92 constraint.
93 constraint : `bool`
94 If `False` (`True` is default), just add the field, not the foreign
95 key constraint.
97 Returns
98 -------
99 fieldSpec : `ddl.FieldSpec`
100 Specification for the field just added.
101 """
102 # Add the dependency's primary key field, but use the dimension name for
103 # the field name to make it unique and more meaningful in this table.
104 fieldSpec = copy.copy(dimension.primaryKey)
105 fieldSpec.name = dimension.name
106 fieldSpec.primaryKey = primaryKey
107 fieldSpec.nullable = nullable
108 tableSpec.fields.add(fieldSpec)
109 # Also add a foreign key constraint on the dependency table, but only if
110 # there actually is one and we weren't told not to.
111 if dimension.hasTable() and dimension.viewOf is None and constraint:
112 tableSpec.foreignKeys.append(_makeForeignKeySpec(dimension))
113 return fieldSpec
116class DimensionElementFields:
117 """Class for constructing table schemas for `DimensionElement`.
119 This creates an object that constructs the table schema for a
120 `DimensionElement` and provides a categorized view of its fields.
122 Parameters
123 ----------
124 element : `DimensionElement`
125 Element for which to make a table specification.
127 Notes
128 -----
129 This combines the foreign key fields from dependencies, unique keys
130 for true `Dimension` instances, metadata fields, and region/timestamp
131 fields for spatial/temporal elements.
133 Callers should use `DimensionUniverse.makeSchemaSpec` if they want to
134 account for elements that have no table or reference another table; this
135 class simply creates a specification for the table an element _would_ have
136 without checking whether it does have one. That can be useful in contexts
137 (e.g. `DimensionRecord`) where we want to simulate the existence of such a
138 table.
139 """
141 def __init__(self, element: DimensionElement):
142 self.element = element
143 self._tableSpec = ddl.TableSpec(fields=())
144 # Add the primary key fields of required dimensions. These continue to
145 # be primary keys in the table for this dimension.
146 self.required = NamedValueSet()
147 self.dimensions = NamedValueSet()
148 self.facts = NamedValueSet()
149 self.standard = NamedValueSet()
150 dependencies = []
151 for dimension in element.required:
152 if dimension != element:
153 fieldSpec = addDimensionForeignKey(self._tableSpec, dimension, primaryKey=True)
154 dependencies.append(fieldSpec.name)
155 else:
156 fieldSpec = element.primaryKey # type: ignore
157 # A Dimension instance is in its own required dependency graph
158 # (always at the end, because of topological ordering). In
159 # this case we don't want to rename the field.
160 self._tableSpec.fields.add(fieldSpec)
161 self.required.add(fieldSpec)
162 self.dimensions.add(fieldSpec)
163 self.standard.add(fieldSpec)
164 # Add fields and foreign keys for implied dimensions. These are
165 # primary keys in their own table, but should not be here. As with
166 # required dependencies, we rename the fields with the dimension name.
167 # We use element.implied instead of element.graph.implied because we
168 # don't want *recursive* implied dependencies.
169 self.implied = NamedValueSet()
170 for dimension in element.implied:
171 fieldSpec = addDimensionForeignKey(self._tableSpec, dimension, primaryKey=False, nullable=True)
172 self.implied.add(fieldSpec)
173 self.dimensions.add(fieldSpec)
174 self.standard.add(fieldSpec)
175 # Add non-primary unique keys and unique constraints for them.
176 for fieldSpec in getattr(element, "alternateKeys", ()):
177 self._tableSpec.fields.add(fieldSpec)
178 self._tableSpec.unique.add(tuple(dependencies) + (fieldSpec.name,))
179 self.standard.add(fieldSpec)
180 self.facts.add(fieldSpec)
181 # Add other metadata fields.
182 for fieldSpec in element.metadata:
183 self._tableSpec.fields.add(fieldSpec)
184 self.standard.add(fieldSpec)
185 self.facts.add(fieldSpec)
186 names = list(self.standard.names)
187 # Add fields for regions and/or timespans.
188 if element.spatial is not None:
189 names.append(SpatialRegionDatabaseRepresentation.NAME)
190 if element.temporal is not None:
191 names.append(TimespanDatabaseRepresentation.NAME)
192 self.names = tuple(names)
194 def makeTableSpec(
195 self,
196 RegionReprClass: Type[SpatialRegionDatabaseRepresentation],
197 TimespanReprClass: Type[TimespanDatabaseRepresentation],
198 ) -> ddl.TableSpec:
199 """Construct a complete specification for a table.
201 The table could hold the records of this element.
203 Parameters
204 ----------
205 RegionReprClass : `type` [ `SpatialRegionDatabaseRepresentation` ]
206 Class object that specifies how spatial regions are represented in
207 the database.
208 TimespanReprClass : `type` [ `TimespanDatabaseRepresentation` ]
209 Class object that specifies how timespans are represented in the
210 database.
212 Returns
213 -------
214 spec : `ddl.TableSpec`
215 Specification for a table.
216 """
217 if self.element.temporal is not None or self.element.spatial is not None:
218 spec = ddl.TableSpec(
219 fields=NamedValueSet(self._tableSpec.fields),
220 unique=self._tableSpec.unique,
221 indexes=self._tableSpec.indexes,
222 foreignKeys=self._tableSpec.foreignKeys,
223 )
224 if self.element.spatial is not None:
225 spec.fields.update(RegionReprClass.makeFieldSpecs(nullable=True))
226 if self.element.temporal is not None:
227 spec.fields.update(TimespanReprClass.makeFieldSpecs(nullable=True))
228 else:
229 spec = self._tableSpec
230 return spec
232 def __str__(self) -> str:
233 lines = [f"{self.element.name}: "]
234 lines.extend(f" {field.name}: {field.getPythonType().__name__}" for field in self.standard)
235 if self.element.spatial is not None:
236 lines.append(" region: lsst.sphgeom.Region")
237 if self.element.temporal is not None:
238 lines.append(" timespan: lsst.daf.butler.Timespan")
239 return "\n".join(lines)
241 element: DimensionElement
242 """The dimension element these fields correspond to.
244 (`DimensionElement`)
245 """
247 required: NamedValueSet[ddl.FieldSpec]
248 """The required dimension fields of this table.
250 They correspond to the element's required
251 dimensions, in that order, i.e. `DimensionElement.required`
252 (`NamedValueSet` [ `ddl.FieldSpec` ]).
253 """
255 implied: NamedValueSet[ddl.FieldSpec]
256 """The implied dimension fields of this table.
258 They correspond to the element's implied
259 dimensions, in that order, i.e. `DimensionElement.implied`
260 (`NamedValueSet` [ `ddl.FieldSpec` ]).
261 """
263 dimensions: NamedValueSet[ddl.FieldSpec]
264 """The direct and implied dimension fields of this table.
266 They correspond to the element's direct
267 required and implied dimensions, in that order, i.e.
268 `DimensionElement.dimensions` (`NamedValueSet` [ `ddl.FieldSpec` ]).
269 """
271 facts: NamedValueSet[ddl.FieldSpec]
272 """The standard fields of this table that do not correspond to dimensions.
274 (`NamedValueSet` [ `ddl.FieldSpec` ]).
276 This is equivalent to ``standard - dimensions`` (but possibly in a
277 different order).
278 """
280 standard: NamedValueSet[ddl.FieldSpec]
281 """All standard fields that are expected to have the same form.
283 They are expected to have the same form in all
284 databases; this is all fields other than those that represent a region
285 and/or timespan (`NamedValueSet` [ `ddl.FieldSpec` ]).
286 """
288 names: Tuple[str, ...]
289 """The names of all fields in the specification (`tuple` [ `str` ]).
291 This includes "region" and/or "timespan" if `element` is spatial and/or
292 temporal (respectively). The actual database representation of these
293 quantities may involve multiple fields (or even fields only on a different
294 table), but the Python representation of those rows (i.e. `DimensionRecord`
295 instances) will always contain exactly these fields.
296 """