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 ..utils 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) -> ddl.FieldSpec:
77 """Add a field and possibly a foreign key to a table specification that
78 reference the table for the given `Dimension`.
80 Parameters
81 ----------
82 tableSpec : `ddl.TableSpec`
83 Specification the field and foreign key are to be added to.
84 dimension : `Dimension`
85 Dimension to be referenced. If this dimension has required
86 dependencies, those must have already been added to the table. A field
87 will be added that correspond to this dimension's primary key, and a
88 foreign key constraint will be added only if the dimension is
89 associated with a table of its own.
90 primaryKey : `bool`
91 If `True`, the new field will be added as part of a compound primary
92 key for the table.
93 nullable : `bool`, optional
94 If `False` (default) the new field will be added with a NOT NULL
95 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.
111 if dimension.hasTable() and dimension.viewOf is None:
112 tableSpec.foreignKeys.append(_makeForeignKeySpec(dimension))
113 return fieldSpec
116def makeDimensionElementTableSpec(element: DimensionElement) -> ddl.TableSpec:
117 """Create a complete table specification for a `DimensionElement`.
119 This combines the foreign key fields from dependencies, unique keys
120 for true `Dimension` instances, metadata fields, and region/timestamp
121 fields for spatial/temporal elements.
123 Most callers should use `DimensionElement.makeTableSpec` or
124 `DimensionUniverse.makeSchemaSpec` instead, which account for elements
125 that have no table or reference another table.
127 Parameters
128 ----------
129 element : `DimensionElement`
130 Element for which to make a table specification.
132 Returns
133 -------
134 spec : `ddl.TableSpec`
135 Database-agnostic specification for a table.
136 """
137 tableSpec = ddl.TableSpec(
138 fields=NamedValueSet(),
139 unique=set(),
140 foreignKeys=[]
141 )
142 # Add the primary key fields of required dimensions. These continue to be
143 # primary keys in the table for this dimension.
144 dependencies = []
145 for dimension in element.required:
146 if dimension != element:
147 addDimensionForeignKey(tableSpec, dimension, primaryKey=True)
148 dependencies.append(dimension.name)
149 else:
150 # A Dimension instance is in its own required dependency graph
151 # (always at the end, because of topological ordering). In this
152 # case we don't want to rename the field.
153 tableSpec.fields.add(element.primaryKey)
154 # Add fields and foreign keys for implied dimensions. These are primary
155 # keys in their own table, but should not be here. As with required
156 # dependencies, we rename the fields with the dimension name.
157 # We use element.implied instead of element.graph.implied because we don't
158 # want *recursive* implied dependencies.
159 for dimension in element.implied:
160 addDimensionForeignKey(tableSpec, dimension, primaryKey=False, nullable=True)
161 # Add non-primary unique keys and unique constraints for them.
162 for fieldSpec in getattr(element, "alternateKeys", ()):
163 tableSpec.fields.add(fieldSpec)
164 tableSpec.unique.add(tuple(dependencies) + (fieldSpec.name,))
165 # Add metadata fields, temporal timespans, and spatial regions.
166 for fieldSpec in element.metadata:
167 tableSpec.fields.add(fieldSpec)
168 if element.spatial is not None:
169 tableSpec.fields.add(REGION_FIELD_SPEC)
170 if element.temporal is not None:
171 for fieldSpec in TIMESPAN_FIELD_SPECS:
172 tableSpec.fields.add(fieldSpec)
173 return tableSpec