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

97 Returns 

98 ------- 

99 fieldSpec : `ddl.FieldSpec` 

100 Specification for the field just added. 

101 """ 

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

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

104 fieldSpec = copy.copy(dimension.primaryKey) 

105 fieldSpec.name = dimension.name 

106 fieldSpec.primaryKey = primaryKey 

107 fieldSpec.nullable = nullable 

108 tableSpec.fields.add(fieldSpec) 

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

110 # there actually is one. 

111 if dimension.hasTable() and dimension.viewOf is None: 

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

113 return fieldSpec 

114 

115 

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

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

118 

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

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

121 fields for spatial/temporal elements. 

122 

123 Most callers should use `DimensionElement.makeTableSpec` or 

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

125 that have no table or reference another table. 

126 

127 Parameters 

128 ---------- 

129 element : `DimensionElement` 

130 Element for which to make a table specification. 

131 

132 Returns 

133 ------- 

134 spec : `ddl.TableSpec` 

135 Database-agnostic specification for a table. 

136 """ 

137 tableSpec = ddl.TableSpec( 

138 fields=NamedValueSet(), 

139 unique=set(), 

140 foreignKeys=[] 

141 ) 

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

143 # primary keys in the table for this dimension. 

144 dependencies = [] 

145 for dimension in element.required: 

146 if dimension != element: 

147 addDimensionForeignKey(tableSpec, dimension, primaryKey=True) 

148 dependencies.append(dimension.name) 

149 else: 

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

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

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

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

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

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

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

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

158 # want *recursive* implied dependencies. 

159 for dimension in element.implied: 

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

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

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

163 tableSpec.fields.add(fieldSpec) 

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

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

166 for fieldSpec in element.metadata: 

167 tableSpec.fields.add(fieldSpec) 

168 if element.spatial is not None: 

169 tableSpec.fields.add(REGION_FIELD_SPEC) 

170 if element.temporal is not None: 

171 for fieldSpec in TIMESPAN_FIELD_SPECS: 

172 tableSpec.fields.add(fieldSpec) 

173 return tableSpec