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