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 "makeForeignKeySpec",
25 "addDimensionForeignKey",
26 "makeElementTableSpec",
27 "REGION_FIELD_SPEC",
28)
30import copy
32from typing import TYPE_CHECKING
34from .. import ddl
35from ..utils import NamedValueSet
36from ..timespan import TIMESPAN_FIELD_SPECS
38if TYPE_CHECKING: # Imports needed only for type annotations; may be circular. 38 ↛ 39line 38 didn't jump to line 39, because the condition on line 38 was never true
39 from .elements import DimensionElement, Dimension
42# Most regions are small (they're quadrilaterals), but visit ones can be quite
43# large because they have a complicated boundary. For HSC, about ~1400 bytes.
44REGION_FIELD_SPEC = ddl.FieldSpec(name="region", nbytes=2048, dtype=ddl.Base64Region)
47def makeForeignKeySpec(dimension: Dimension) -> ddl.ForeignKeySpec:
48 """Make a `ddl.ForeignKeySpec` that references the table for the given
49 `Dimension` table.
51 Most callers should use the higher-level `addDimensionForeignKey` function
52 instead.
54 Parameters
55 ----------
56 dimension : `Dimension`
57 The dimension to be referenced. Caller guarantees that it is actually
58 associated with a table.
60 Returns
61 -------
62 spec : `ddl.ForeignKeySpec`
63 A database-agnostic foreign key specification.
64 """
65 source = []
66 target = []
67 for other in dimension.graph.required:
68 if other == dimension:
69 target.append(dimension.primaryKey.name)
70 else:
71 target.append(other.name)
72 source.append(other.name)
73 return ddl.ForeignKeySpec(table=dimension.name, source=tuple(source), target=tuple(target))
76def addDimensionForeignKey(tableSpec: ddl.TableSpec, dimension: Dimension, *,
77 primaryKey: bool, nullable: bool = False):
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 """
98 # Add the dependency's primary key field, but use the dimension name for
99 # the field name to make it unique and more meaningful in this table.
100 fieldSpec = copy.copy(dimension.primaryKey)
101 fieldSpec.name = dimension.name
102 fieldSpec.primaryKey = primaryKey
103 fieldSpec.nullable = nullable
104 tableSpec.fields.add(fieldSpec)
105 # Also add a foreign key constraint on the dependency table, but only if
106 # there actually is one.
107 if dimension.hasTable() and dimension.viewOf is None:
108 tableSpec.foreignKeys.append(makeForeignKeySpec(dimension))
111def makeElementTableSpec(element: DimensionElement) -> ddl.TableSpec:
112 """Create a complete table specification for a `DimensionElement`.
114 This combines the foreign key fields from dependencies, unique keys
115 for true `Dimension` instances, metadata fields, and region/timestamp
116 fields for spatial/temporal elements.
118 Most callers should use `DimensionElement.makeTableSpec` or
119 `DimensionUniverse.makeSchemaSpec` instead, which account for elements
120 that have no table or reference another table.
122 Parameters
123 ----------
124 element : `DimensionElement`
125 Element for which to make a table specification.
127 Returns
128 -------
129 spec : `ddl.TableSpec`
130 Database-agnostic specification for a table.
131 """
132 tableSpec = ddl.TableSpec(
133 fields=NamedValueSet(),
134 unique=set(),
135 foreignKeys=[]
136 )
137 # Add the primary key fields of required dimensions. These continue to be
138 # primary keys in the table for this dimension.
139 dependencies = []
140 for dimension in element.graph.required:
141 if dimension != element:
142 addDimensionForeignKey(tableSpec, dimension, primaryKey=True)
143 dependencies.append(dimension.name)
144 else:
145 # A Dimension instance is in its own required dependency graph
146 # (always at the end, because of topological ordering). In this
147 # case we don't want to rename the field.
148 tableSpec.fields.add(element.primaryKey)
149 # Add fields and foreign keys for implied dimensions. These are primary
150 # keys in their own table, but should not be here. As with required
151 # dependencies, we rename the fields with the dimension name.
152 # We use element.implied instead of element.graph.implied because we don't
153 # want *recursive* implied dependencies.
154 for dimension in element.implied:
155 addDimensionForeignKey(tableSpec, dimension, primaryKey=False, nullable=True)
156 # Add non-primary unique keys and unique constraints for them.
157 for fieldSpec in getattr(element, "alternateKeys", ()):
158 tableSpec.fields.add(fieldSpec)
159 tableSpec.unique.add(tuple(dependencies) + (fieldSpec.name,))
160 # Add metadata fields, temporal timespans, and spatial regions.
161 for fieldSpec in element.metadata:
162 tableSpec.fields.add(fieldSpec)
163 if element.spatial:
164 tableSpec.fields.add(REGION_FIELD_SPEC)
165 if element.temporal:
166 for fieldSpec in TIMESPAN_FIELD_SPECS:
167 tableSpec.fields.add(fieldSpec)
168 return tableSpec