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 "makeDimensionElementTableSpec", 

26 "REGION_FIELD_SPEC", 

27) 

28 

29import copy 

30 

31from typing import TYPE_CHECKING 

32 

33from .. import ddl 

34from ..named import NamedValueSet 

35from ..timespan import TIMESPAN_FIELD_SPECS 

36 

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 

39 

40 

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) 

44 

45 

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

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

48 `Dimension` table. 

49 

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

51 instead. 

52 

53 Parameters 

54 ---------- 

55 dimension : `Dimension` 

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

57 associated with a table. 

58 

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)) 

73 

74 

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

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

77 ) -> ddl.FieldSpec: 

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

79 reference the table for the given `Dimension`. 

80 

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 constraint : `bool` 

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

99 key constraint. 

100 

101 Returns 

102 ------- 

103 fieldSpec : `ddl.FieldSpec` 

104 Specification for the field just added. 

105 """ 

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

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

108 fieldSpec = copy.copy(dimension.primaryKey) 

109 fieldSpec.name = dimension.name 

110 fieldSpec.primaryKey = primaryKey 

111 fieldSpec.nullable = nullable 

112 tableSpec.fields.add(fieldSpec) 

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

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

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

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

117 return fieldSpec 

118 

119 

120def makeDimensionElementTableSpec(element: DimensionElement) -> ddl.TableSpec: 

121 """Create a complete table specification for a `DimensionElement`. 

122 

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

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

125 fields for spatial/temporal elements. 

126 

127 Most callers should use `DimensionElement.makeTableSpec` or 

128 `DimensionUniverse.makeSchemaSpec` instead, which account for elements 

129 that have no table or reference another table. 

130 

131 Parameters 

132 ---------- 

133 element : `DimensionElement` 

134 Element for which to make a table specification. 

135 

136 Returns 

137 ------- 

138 spec : `ddl.TableSpec` 

139 Database-agnostic specification for a table. 

140 """ 

141 tableSpec = ddl.TableSpec( 

142 fields=NamedValueSet(), 

143 unique=set(), 

144 foreignKeys=[] 

145 ) 

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

147 # primary keys in the table for this dimension. 

148 dependencies = [] 

149 for dimension in element.required: 

150 if dimension != element: 

151 addDimensionForeignKey(tableSpec, dimension, primaryKey=True) 

152 dependencies.append(dimension.name) 

153 else: 

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

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

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

157 tableSpec.fields.add(element.primaryKey) # type: ignore 

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

159 # keys in their own table, but should not be here. As with required 

160 # dependencies, we rename the fields with the dimension name. 

161 # We use element.implied instead of element.graph.implied because we don't 

162 # want *recursive* implied dependencies. 

163 for dimension in element.implied: 

164 addDimensionForeignKey(tableSpec, dimension, primaryKey=False, nullable=True) 

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

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

167 tableSpec.fields.add(fieldSpec) 

168 tableSpec.unique.add(tuple(dependencies) + (fieldSpec.name,)) 

169 # Add metadata fields, temporal timespans, and spatial regions. 

170 for fieldSpec in element.metadata: 

171 tableSpec.fields.add(fieldSpec) 

172 if element.spatial is not None: 

173 tableSpec.fields.add(REGION_FIELD_SPEC) 

174 if element.temporal is not None: 

175 for fieldSpec in TIMESPAN_FIELD_SPECS: 

176 tableSpec.fields.add(fieldSpec) 

177 return tableSpec