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) 

26 

27import copy 

28 

29from typing import Tuple, Type, TYPE_CHECKING 

30 

31from .. import ddl 

32from ..named import NamedValueSet 

33from .._topology import SpatialRegionDatabaseRepresentation 

34from ..timespan import TimespanDatabaseRepresentation 

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 

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

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

42 `Dimension` table. 

43 

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

45 instead. 

46 

47 Parameters 

48 ---------- 

49 dimension : `Dimension` 

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

51 associated with a table. 

52 

53 Returns 

54 ------- 

55 spec : `ddl.ForeignKeySpec` 

56 A database-agnostic foreign key specification. 

57 """ 

58 source = [] 

59 target = [] 

60 for other in dimension.required: 

61 if other == dimension: 

62 target.append(dimension.primaryKey.name) 

63 else: 

64 target.append(other.name) 

65 source.append(other.name) 

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

67 

68 

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

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

71 ) -> ddl.FieldSpec: 

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

73 reference the table for the given `Dimension`. 

74 

75 Parameters 

76 ---------- 

77 tableSpec : `ddl.TableSpec` 

78 Specification the field and foreign key are to be added to. 

79 dimension : `Dimension` 

80 Dimension to be referenced. If this dimension has required 

81 dependencies, those must have already been added to the table. A field 

82 will be added that correspond to this dimension's primary key, and a 

83 foreign key constraint will be added only if the dimension is 

84 associated with a table of its own. 

85 primaryKey : `bool` 

86 If `True`, the new field will be added as part of a compound primary 

87 key for the table. 

88 nullable : `bool`, optional 

89 If `False` (default) the new field will be added with a NOT NULL 

90 constraint. 

91 constraint : `bool` 

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

93 key constraint. 

94 

95 Returns 

96 ------- 

97 fieldSpec : `ddl.FieldSpec` 

98 Specification for the field just added. 

99 """ 

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

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

102 fieldSpec = copy.copy(dimension.primaryKey) 

103 fieldSpec.name = dimension.name 

104 fieldSpec.primaryKey = primaryKey 

105 fieldSpec.nullable = nullable 

106 tableSpec.fields.add(fieldSpec) 

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

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

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

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

111 return fieldSpec 

112 

113 

114class DimensionElementFields: 

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

116 provides a categorized view of its fields. 

117 

118 Parameters 

119 ---------- 

120 element : `DimensionElement` 

121 Element for which to make a table specification. 

122 

123 Notes 

124 ----- 

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

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

127 fields for spatial/temporal elements. 

128 

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

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

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

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

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

134 table. 

135 """ 

136 def __init__(self, element: DimensionElement): 

137 self.element = element 

138 self._tableSpec = ddl.TableSpec(fields=()) 

139 # Add the primary key fields of required dimensions. These continue to 

140 # be primary keys in the table for this dimension. 

141 self.required = NamedValueSet() 

142 self.dimensions = NamedValueSet() 

143 self.facts = NamedValueSet() 

144 self.standard = NamedValueSet() 

145 dependencies = [] 

146 for dimension in element.required: 

147 if dimension != element: 

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

149 dependencies.append(fieldSpec.name) 

150 else: 

151 fieldSpec = element.primaryKey # type: ignore 

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

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

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

155 self._tableSpec.fields.add(fieldSpec) 

156 self.required.add(fieldSpec) 

157 self.dimensions.add(fieldSpec) 

158 self.standard.add(fieldSpec) 

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

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

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

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

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

164 self.implied = NamedValueSet() 

165 for dimension in element.implied: 

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

167 self.implied.add(fieldSpec) 

168 self.dimensions.add(fieldSpec) 

169 self.standard.add(fieldSpec) 

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

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

172 self._tableSpec.fields.add(fieldSpec) 

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

174 self.standard.add(fieldSpec) 

175 self.facts.add(fieldSpec) 

176 # Add other metadata fields. 

177 for fieldSpec in element.metadata: 

178 self._tableSpec.fields.add(fieldSpec) 

179 self.standard.add(fieldSpec) 

180 self.facts.add(fieldSpec) 

181 names = list(self.standard.names) 

182 # Add fields for regions and/or timespans. 

183 if element.spatial is not None: 

184 names.append(SpatialRegionDatabaseRepresentation.NAME) 

185 if element.temporal is not None: 

186 names.append(TimespanDatabaseRepresentation.NAME) 

187 self.names = tuple(names) 

188 

189 def makeTableSpec( 

190 self, 

191 RegionReprClass: Type[SpatialRegionDatabaseRepresentation], 

192 TimespanReprClass: Type[TimespanDatabaseRepresentation], 

193 ) -> ddl.TableSpec: 

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

195 records of this element. 

196 

197 Parameters 

198 ---------- 

199 RegionReprClass : `type` [ `SpatialRegionDatabaseRepresentation` ] 

200 Class object that specifies how spatial regions are represented in 

201 the database. 

202 TimespanReprClass : `type` [ `TimespanDatabaseRepresentation` ] 

203 Class object that specifies how timespans are represented in the 

204 database. 

205 

206 Returns 

207 ------- 

208 spec : `ddl.TableSpec` 

209 Specification for a table. 

210 """ 

211 if self.element.temporal is not None or self.element.spatial is not None: 

212 spec = ddl.TableSpec( 

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

214 unique=self._tableSpec.unique, 

215 indexes=self._tableSpec.indexes, 

216 foreignKeys=self._tableSpec.foreignKeys, 

217 ) 

218 if self.element.spatial is not None: 

219 spec.fields.update(RegionReprClass.makeFieldSpecs(nullable=True)) 

220 if self.element.temporal is not None: 

221 spec.fields.update(TimespanReprClass.makeFieldSpecs(nullable=True)) 

222 else: 

223 spec = self._tableSpec 

224 return spec 

225 

226 def __str__(self) -> str: 

227 lines = [f"{self.element.name}: "] 

228 lines.extend(f" {field.name}: {field.getPythonType().__name__}" for field in self.standard) 

229 if self.element.spatial is not None: 

230 lines.append(" region: lsst.sphgeom.Region") 

231 if self.element.temporal is not None: 

232 lines.append(" timespan: lsst.daf.butler.Timespan") 

233 return "\n".join(lines) 

234 

235 element: DimensionElement 

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

237 """ 

238 

239 required: NamedValueSet[ddl.FieldSpec] 

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

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

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

243 """ 

244 

245 implied: NamedValueSet[ddl.FieldSpec] 

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

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

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

249 """ 

250 

251 dimensions: NamedValueSet[ddl.FieldSpec] 

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

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

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

255 """ 

256 

257 facts: NamedValueSet[ddl.FieldSpec] 

258 """The standard fields of this table that do not correspond to dimensions 

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

260 

261 This is equivalent to ``standard - dimensions`` (but possibly in a 

262 different order). 

263 """ 

264 

265 standard: NamedValueSet[ddl.FieldSpec] 

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

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

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

269 """ 

270 

271 names: Tuple[str, ...] 

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

273 

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

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

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

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

278 instances) will always contain exactly these fields. 

279 """