Coverage for python/lsst/daf/butler/core/dimensions/_schema.py: 24%

98 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-19 12:13 +0000

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__ = ("addDimensionForeignKey",) 

24 

25import copy 

26from typing import TYPE_CHECKING, Tuple, Type 

27 

28from .. import ddl 

29from .._topology import SpatialRegionDatabaseRepresentation 

30from ..named import NamedValueSet 

31from ..timespan import TimespanDatabaseRepresentation 

32 

33if TYPE_CHECKING: # Imports needed only for type annotations; may be circular. 33 ↛ 34line 33 didn't jump to line 34, because the condition on line 33 was never true

34 from ._elements import Dimension, DimensionElement 

35 

36 

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

38 """Make a `ddl.ForeignKeySpec`. 

39 

40 This will reference the table for the given `Dimension` table. 

41 

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

43 instead. 

44 

45 Parameters 

46 ---------- 

47 dimension : `Dimension` 

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

49 associated with a table. 

50 

51 Returns 

52 ------- 

53 spec : `ddl.ForeignKeySpec` 

54 A database-agnostic foreign key specification. 

55 """ 

56 source = [] 

57 target = [] 

58 for other in dimension.required: 

59 if other == dimension: 

60 target.append(dimension.primaryKey.name) 

61 else: 

62 target.append(other.name) 

63 source.append(other.name) 

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

65 

66 

67def addDimensionForeignKey( 

68 tableSpec: ddl.TableSpec, 

69 dimension: Dimension, 

70 *, 

71 primaryKey: bool, 

72 nullable: bool = False, 

73 constraint: bool = True, 

74) -> ddl.FieldSpec: 

75 """Add a field and possibly a foreign key to a table specification. 

76 

77 The field will reference the table for the given `Dimension`. 

78 

79 Parameters 

80 ---------- 

81 tableSpec : `ddl.TableSpec` 

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

83 dimension : `Dimension` 

84 Dimension to be referenced. If this dimension has required 

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

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

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

88 associated with a table of its own. 

89 primaryKey : `bool` 

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

91 key for the table. 

92 nullable : `bool`, optional 

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

94 constraint. 

95 constraint : `bool` 

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

97 key constraint. 

98 

99 Returns 

100 ------- 

101 fieldSpec : `ddl.FieldSpec` 

102 Specification for the field just added. 

103 """ 

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

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

106 fieldSpec = copy.copy(dimension.primaryKey) 

107 fieldSpec.name = dimension.name 

108 fieldSpec.primaryKey = primaryKey 

109 fieldSpec.nullable = nullable 

110 tableSpec.fields.add(fieldSpec) 

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

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

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

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

115 return fieldSpec 

116 

117 

118class DimensionElementFields: 

119 """Class for constructing table schemas for `DimensionElement`. 

120 

121 This creates an object that constructs the table schema for a 

122 `DimensionElement` and provides a categorized view of its fields. 

123 

124 Parameters 

125 ---------- 

126 element : `DimensionElement` 

127 Element for which to make a table specification. 

128 

129 Notes 

130 ----- 

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

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

133 fields for spatial/temporal elements. 

134 

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

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

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

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

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

140 table. 

141 """ 

142 

143 def __init__(self, element: DimensionElement): 

144 self.element = element 

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

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

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

148 self.required = NamedValueSet() 

149 self.dimensions = NamedValueSet() 

150 self.facts = NamedValueSet() 

151 self.standard = NamedValueSet() 

152 dependencies = [] 

153 for dimension in element.required: 

154 if dimension != element: 

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

156 dependencies.append(fieldSpec.name) 

157 else: 

158 fieldSpec = element.primaryKey # type: ignore 

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

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

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

162 self._tableSpec.fields.add(fieldSpec) 

163 self.required.add(fieldSpec) 

164 self.dimensions.add(fieldSpec) 

165 self.standard.add(fieldSpec) 

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

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

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

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

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

171 self.implied = NamedValueSet() 

172 for dimension in element.implied: 

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

174 self.implied.add(fieldSpec) 

175 self.dimensions.add(fieldSpec) 

176 self.standard.add(fieldSpec) 

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

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

179 self._tableSpec.fields.add(fieldSpec) 

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

181 self.standard.add(fieldSpec) 

182 self.facts.add(fieldSpec) 

183 # Add other metadata fields. 

184 for fieldSpec in element.metadata: 

185 self._tableSpec.fields.add(fieldSpec) 

186 self.standard.add(fieldSpec) 

187 self.facts.add(fieldSpec) 

188 names = list(self.standard.names) 

189 # Add fields for regions and/or timespans. 

190 if element.spatial is not None: 

191 names.append(SpatialRegionDatabaseRepresentation.NAME) 

192 if element.temporal is not None: 

193 names.append(TimespanDatabaseRepresentation.NAME) 

194 self.names = tuple(names) 

195 

196 def makeTableSpec( 

197 self, 

198 RegionReprClass: Type[SpatialRegionDatabaseRepresentation], 

199 TimespanReprClass: Type[TimespanDatabaseRepresentation], 

200 ) -> ddl.TableSpec: 

201 """Construct a complete specification for a table. 

202 

203 The table could hold the records of this element. 

204 

205 Parameters 

206 ---------- 

207 RegionReprClass : `type` [ `SpatialRegionDatabaseRepresentation` ] 

208 Class object that specifies how spatial regions are represented in 

209 the database. 

210 TimespanReprClass : `type` [ `TimespanDatabaseRepresentation` ] 

211 Class object that specifies how timespans are represented in the 

212 database. 

213 

214 Returns 

215 ------- 

216 spec : `ddl.TableSpec` 

217 Specification for a table. 

218 """ 

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

220 spec = ddl.TableSpec( 

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

222 unique=self._tableSpec.unique, 

223 indexes=self._tableSpec.indexes, 

224 foreignKeys=self._tableSpec.foreignKeys, 

225 ) 

226 if self.element.spatial is not None: 

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

228 if self.element.temporal is not None: 

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

230 else: 

231 spec = self._tableSpec 

232 return spec 

233 

234 def __str__(self) -> str: 

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

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

237 if self.element.spatial is not None: 

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

239 if self.element.temporal is not None: 

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

241 return "\n".join(lines) 

242 

243 element: DimensionElement 

244 """The dimension element these fields correspond to. 

245 

246 (`DimensionElement`) 

247 """ 

248 

249 required: NamedValueSet[ddl.FieldSpec] 

250 """The required dimension fields of this table. 

251 

252 They correspond to the element's required 

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

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

255 """ 

256 

257 implied: NamedValueSet[ddl.FieldSpec] 

258 """The implied dimension fields of this table. 

259 

260 They correspond to the element's implied 

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

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

263 """ 

264 

265 dimensions: NamedValueSet[ddl.FieldSpec] 

266 """The direct and implied dimension fields of this table. 

267 

268 They correspond to the element's direct 

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

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

271 """ 

272 

273 facts: NamedValueSet[ddl.FieldSpec] 

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

275 

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

277 

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

279 different order). 

280 """ 

281 

282 standard: NamedValueSet[ddl.FieldSpec] 

283 """All standard fields that are expected to have the same form. 

284 

285 They are expected to have the same form in all 

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

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

288 """ 

289 

290 names: Tuple[str, ...] 

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

292 

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

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

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

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

297 instances) will always contain exactly these fields. 

298 """