Hide keyboard shortcuts

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 

22 

23__all__ = ( 

24 "addDimensionForeignKey", 

25 "REGION_FIELD_SPEC", 

26) 

27 

28import copy 

29 

30from typing import Tuple, Type, TYPE_CHECKING 

31 

32from .. import ddl 

33from ..named import NamedValueSet 

34from ..timespan import DatabaseTimespanRepresentation 

35 

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 

38 

39 

40# Most regions are small (they're quadrilaterals), but visit ones can be quite 

41# large because they have a complicated boundary. For HSC, about ~1400 bytes. 

42REGION_FIELD_SPEC = ddl.FieldSpec(name="region", nbytes=2048, dtype=ddl.Base64Region) 

43 

44 

45def _makeForeignKeySpec(dimension: Dimension) -> ddl.ForeignKeySpec: 

46 """Make a `ddl.ForeignKeySpec` that references the table for the given 

47 `Dimension` table. 

48 

49 Most callers should use the higher-level `addDimensionForeignKey` function 

50 instead. 

51 

52 Parameters 

53 ---------- 

54 dimension : `Dimension` 

55 The dimension to be referenced. Caller guarantees that it is actually 

56 associated with a table. 

57 

58 Returns 

59 ------- 

60 spec : `ddl.ForeignKeySpec` 

61 A database-agnostic foreign key specification. 

62 """ 

63 source = [] 

64 target = [] 

65 for other in dimension.required: 

66 if other == dimension: 

67 target.append(dimension.primaryKey.name) 

68 else: 

69 target.append(other.name) 

70 source.append(other.name) 

71 return ddl.ForeignKeySpec(table=dimension.name, source=tuple(source), target=tuple(target)) 

72 

73 

74def addDimensionForeignKey(tableSpec: ddl.TableSpec, dimension: Dimension, *, 

75 primaryKey: bool, nullable: bool = False, constraint: bool = True 

76 ) -> ddl.FieldSpec: 

77 """Add a field and possibly a foreign key to a table specification that 

78 reference the table for the given `Dimension`. 

79 

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. 

96 constraint : `bool` 

97 If `False` (`True` is default), just add the field, not the foreign 

98 key constraint. 

99 

100 Returns 

101 ------- 

102 fieldSpec : `ddl.FieldSpec` 

103 Specification for the field just added. 

104 """ 

105 # Add the dependency's primary key field, but use the dimension name for 

106 # the field name to make it unique and more meaningful in this table. 

107 fieldSpec = copy.copy(dimension.primaryKey) 

108 fieldSpec.name = dimension.name 

109 fieldSpec.primaryKey = primaryKey 

110 fieldSpec.nullable = nullable 

111 tableSpec.fields.add(fieldSpec) 

112 # Also add a foreign key constraint on the dependency table, but only if 

113 # there actually is one and we weren't told not to. 

114 if dimension.hasTable() and dimension.viewOf is None and constraint: 

115 tableSpec.foreignKeys.append(_makeForeignKeySpec(dimension)) 

116 return fieldSpec 

117 

118 

119class DimensionElementFields: 

120 """An object that constructs the table schema for a `DimensionElement` and 

121 provides a categorized view of its fields. 

122 

123 Parameters 

124 ---------- 

125 element : `DimensionElement` 

126 Element for which to make a table specification. 

127 

128 Notes 

129 ----- 

130 This combines the foreign key fields from dependencies, unique keys 

131 for true `Dimension` instances, metadata fields, and region/timestamp 

132 fields for spatial/temporal elements. 

133 

134 Callers should use `DimensionUniverse.makeSchemaSpec` if they want to 

135 account for elements that have no table or reference another table; this 

136 class simply creates a specification for the table an element _would_ have 

137 without checking whether it does have one. That can be useful in contexts 

138 (e.g. `DimensionRecord`) where we want to simulate the existence of such a 

139 table. 

140 """ 

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.standard = NamedValueSet() 

149 dependencies = [] 

150 for dimension in element.required: 

151 if dimension != element: 

152 fieldSpec = addDimensionForeignKey(self._tableSpec, dimension, primaryKey=True) 

153 dependencies.append(fieldSpec.name) 

154 else: 

155 fieldSpec = element.primaryKey # type: ignore 

156 # A Dimension instance is in its own required dependency graph 

157 # (always at the end, because of topological ordering). In 

158 # this case we don't want to rename the field. 

159 self._tableSpec.fields.add(fieldSpec) 

160 self.required.add(fieldSpec) 

161 self.dimensions.add(fieldSpec) 

162 self.standard.add(fieldSpec) 

163 # Add fields and foreign keys for implied dimensions. These are 

164 # primary keys in their own table, but should not be here. As with 

165 # required dependencies, we rename the fields with the dimension name. 

166 # We use element.implied instead of element.graph.implied because we 

167 # don't want *recursive* implied dependencies. 

168 self.implied = NamedValueSet() 

169 for dimension in element.implied: 

170 fieldSpec = addDimensionForeignKey(self._tableSpec, dimension, primaryKey=False, nullable=True) 

171 self.implied.add(fieldSpec) 

172 self.dimensions.add(fieldSpec) 

173 self.standard.add(fieldSpec) 

174 # Add non-primary unique keys and unique constraints for them. 

175 for fieldSpec in getattr(element, "alternateKeys", ()): 

176 self._tableSpec.fields.add(fieldSpec) 

177 self._tableSpec.unique.add(tuple(dependencies) + (fieldSpec.name,)) 

178 self.standard.add(fieldSpec) 

179 # Add other metadata fields. 

180 for fieldSpec in element.metadata: 

181 self._tableSpec.fields.add(fieldSpec) 

182 self.standard.add(fieldSpec) 

183 names = list(self.standard.names) 

184 # Add fields for regions and/or timespans. 

185 if element.spatial is not None: 

186 self._tableSpec.fields.add(REGION_FIELD_SPEC) 

187 names.append(REGION_FIELD_SPEC.name) 

188 if element.temporal is not None: 

189 names.append(DatabaseTimespanRepresentation.NAME) 

190 self.names = tuple(names) 

191 

192 def makeTableSpec(self, tsRepr: Type[DatabaseTimespanRepresentation]) -> ddl.TableSpec: 

193 """Construct a complete specification for a table that could hold the 

194 records of this element. 

195 

196 Parameters 

197 ---------- 

198 tsRepr : `type` (`DatabaseTimespanRepresentation` subclass) 

199 Class object that specifies how timespans are represented in the 

200 database. 

201 

202 Returns 

203 ------- 

204 spec : `ddl.TableSpec` 

205 Specification for a table. 

206 """ 

207 if self.element.temporal is not None: 

208 spec = ddl.TableSpec( 

209 fields=NamedValueSet(self._tableSpec.fields), 

210 unique=self._tableSpec.unique, 

211 indexes=self._tableSpec.indexes, 

212 foreignKeys=self._tableSpec.foreignKeys, 

213 ) 

214 for fieldSpec in tsRepr.makeFieldSpecs(nullable=True): 

215 spec.fields.add(fieldSpec) 

216 else: 

217 spec = self._tableSpec 

218 return spec 

219 

220 element: DimensionElement 

221 """The dimension element these fields correspond to (`DimensionElement`). 

222 """ 

223 

224 required: NamedValueSet[ddl.FieldSpec] 

225 """The fields of this table that correspond to the element's required 

226 dimensions, in that order, i.e. `DimensionElement.required` 

227 (`NamedValueSet` [ `ddl.FieldSpec` ]). 

228 """ 

229 

230 implied: NamedValueSet[ddl.FieldSpec] 

231 """The fields of this table that correspond to the element's implied 

232 dimensions, in that order, i.e. `DimensionElement.implied` 

233 (`NamedValueSet` [ `ddl.FieldSpec` ]). 

234 """ 

235 

236 dimensions: NamedValueSet[ddl.FieldSpec] 

237 """The fields of this table that correspond to the element's direct 

238 required and implied dimensions, in that order, i.e. 

239 `DimensionElement.dimensions` (`NamedValueSet` [ `ddl.FieldSpec` ]). 

240 """ 

241 

242 standard: NamedValueSet[ddl.FieldSpec] 

243 """All standard fields that are expected to have the same form in all 

244 databases; this is all fields other than those that represent a region 

245 and/or timespan (`NamedValueSet` [ `ddl.FieldSpec` ]). 

246 """ 

247 

248 names: Tuple[str, ...] 

249 """The names of all fields in the specification (`tuple` [ `str` ]). 

250 

251 This includes "region" and/or "timespan" if `element` is spatial and/or 

252 temporal (respectively). The actual database representation of these 

253 quantities may involve multiple fields (or even fields only on a different 

254 table), but the Python representation of those rows (i.e. `DimensionRecord` 

255 instances) will always contain exactly these fields. 

256 """