Coverage for python/lsst/daf/butler/core/dimensions/schema.py : 18%

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 "makeDimensionElementTableSpec",
26 "REGION_FIELD_SPEC",
27)
29import copy
31from typing import TYPE_CHECKING
33from .. import ddl
34from ..named import NamedValueSet
35from ..timespan import TIMESPAN_FIELD_SPECS
37if TYPE_CHECKING: # Imports needed only for type annotations; may be circular. 37 ↛ 38line 37 didn't jump to line 38, because the condition on line 37 was never true
38 from .elements import DimensionElement, Dimension
41# Most regions are small (they're quadrilaterals), but visit ones can be quite
42# large because they have a complicated boundary. For HSC, about ~1400 bytes.
43REGION_FIELD_SPEC = ddl.FieldSpec(name="region", nbytes=2048, dtype=ddl.Base64Region)
46def _makeForeignKeySpec(dimension: Dimension) -> ddl.ForeignKeySpec:
47 """Make a `ddl.ForeignKeySpec` that references the table for the given
48 `Dimension` table.
50 Most callers should use the higher-level `addDimensionForeignKey` function
51 instead.
53 Parameters
54 ----------
55 dimension : `Dimension`
56 The dimension to be referenced. Caller guarantees that it is actually
57 associated with a table.
59 Returns
60 -------
61 spec : `ddl.ForeignKeySpec`
62 A database-agnostic foreign key specification.
63 """
64 source = []
65 target = []
66 for other in dimension.required:
67 if other == dimension:
68 target.append(dimension.primaryKey.name)
69 else:
70 target.append(other.name)
71 source.append(other.name)
72 return ddl.ForeignKeySpec(table=dimension.name, source=tuple(source), target=tuple(target))
75def addDimensionForeignKey(tableSpec: ddl.TableSpec, dimension: Dimension, *,
76 primaryKey: bool, nullable: bool = False, constraint: bool = True
77 ) -> ddl.FieldSpec:
78 """Add a field and possibly a foreign key to a table specification that
79 reference the table for the given `Dimension`.
81 Parameters
82 ----------
83 tableSpec : `ddl.TableSpec`
84 Specification the field and foreign key are to be added to.
85 dimension : `Dimension`
86 Dimension to be referenced. If this dimension has required
87 dependencies, those must have already been added to the table. A field
88 will be added that correspond to this dimension's primary key, and a
89 foreign key constraint will be added only if the dimension is
90 associated with a table of its own.
91 primaryKey : `bool`
92 If `True`, the new field will be added as part of a compound primary
93 key for the table.
94 nullable : `bool`, optional
95 If `False` (default) the new field will be added with a NOT NULL
96 constraint.
97 constraint : `bool`
98 If `False` (`True` is default), just add the field, not the foreign
99 key constraint.
101 Returns
102 -------
103 fieldSpec : `ddl.FieldSpec`
104 Specification for the field just added.
105 """
106 # Add the dependency's primary key field, but use the dimension name for
107 # the field name to make it unique and more meaningful in this table.
108 fieldSpec = copy.copy(dimension.primaryKey)
109 fieldSpec.name = dimension.name
110 fieldSpec.primaryKey = primaryKey
111 fieldSpec.nullable = nullable
112 tableSpec.fields.add(fieldSpec)
113 # Also add a foreign key constraint on the dependency table, but only if
114 # there actually is one and we weren't told not to.
115 if dimension.hasTable() and dimension.viewOf is None and constraint:
116 tableSpec.foreignKeys.append(_makeForeignKeySpec(dimension))
117 return fieldSpec
120def makeDimensionElementTableSpec(element: DimensionElement) -> ddl.TableSpec:
121 """Create a complete table specification for a `DimensionElement`.
123 This combines the foreign key fields from dependencies, unique keys
124 for true `Dimension` instances, metadata fields, and region/timestamp
125 fields for spatial/temporal elements.
127 Most callers should use `DimensionElement.makeTableSpec` or
128 `DimensionUniverse.makeSchemaSpec` instead, which account for elements
129 that have no table or reference another table.
131 Parameters
132 ----------
133 element : `DimensionElement`
134 Element for which to make a table specification.
136 Returns
137 -------
138 spec : `ddl.TableSpec`
139 Database-agnostic specification for a table.
140 """
141 tableSpec = ddl.TableSpec(
142 fields=NamedValueSet(),
143 unique=set(),
144 foreignKeys=[]
145 )
146 # Add the primary key fields of required dimensions. These continue to be
147 # primary keys in the table for this dimension.
148 dependencies = []
149 for dimension in element.required:
150 if dimension != element:
151 addDimensionForeignKey(tableSpec, dimension, primaryKey=True)
152 dependencies.append(dimension.name)
153 else:
154 # A Dimension instance is in its own required dependency graph
155 # (always at the end, because of topological ordering). In this
156 # case we don't want to rename the field.
157 tableSpec.fields.add(element.primaryKey) # type: ignore
158 # Add fields and foreign keys for implied dimensions. These are primary
159 # keys in their own table, but should not be here. As with required
160 # dependencies, we rename the fields with the dimension name.
161 # We use element.implied instead of element.graph.implied because we don't
162 # want *recursive* implied dependencies.
163 for dimension in element.implied:
164 addDimensionForeignKey(tableSpec, dimension, primaryKey=False, nullable=True)
165 # Add non-primary unique keys and unique constraints for them.
166 for fieldSpec in getattr(element, "alternateKeys", ()):
167 tableSpec.fields.add(fieldSpec)
168 tableSpec.unique.add(tuple(dependencies) + (fieldSpec.name,))
169 # Add metadata fields, temporal timespans, and spatial regions.
170 for fieldSpec in element.metadata:
171 tableSpec.fields.add(fieldSpec)
172 if element.spatial is not None:
173 tableSpec.fields.add(REGION_FIELD_SPEC)
174 if element.temporal is not None:
175 for fieldSpec in TIMESPAN_FIELD_SPECS:
176 tableSpec.fields.add(fieldSpec)
177 return tableSpec