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

25 "addDimensionForeignKey", 

26 "makeElementTableSpec", 

27 "REGION_FIELD_SPEC", 

28) 

29 

30import copy 

31 

32from typing import TYPE_CHECKING 

33 

34from .. import ddl 

35from ..utils import NamedValueSet 

36from ..timespan import TIMESPAN_FIELD_SPECS 

37 

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

39 from .elements import DimensionElement, Dimension 

40 

41 

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

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

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

45 

46 

47def makeForeignKeySpec(dimension: Dimension) -> ddl.ForeignKeySpec: 

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

49 `Dimension` table. 

50 

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

52 instead. 

53 

54 Parameters 

55 ---------- 

56 dimension : `Dimension` 

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

58 associated with a table. 

59 

60 Returns 

61 ------- 

62 spec : `ddl.ForeignKeySpec` 

63 A database-agnostic foreign key specification. 

64 """ 

65 source = [] 

66 target = [] 

67 for other in dimension.required: 

68 if other == dimension: 

69 target.append(dimension.primaryKey.name) 

70 else: 

71 target.append(other.name) 

72 source.append(other.name) 

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

74 

75 

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

77 primaryKey: bool, nullable: bool = False): 

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

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

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

100 fieldSpec = copy.copy(dimension.primaryKey) 

101 fieldSpec.name = dimension.name 

102 fieldSpec.primaryKey = primaryKey 

103 fieldSpec.nullable = nullable 

104 tableSpec.fields.add(fieldSpec) 

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

106 # there actually is one. 

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

108 tableSpec.foreignKeys.append(makeForeignKeySpec(dimension)) 

109 

110 

111def makeElementTableSpec(element: DimensionElement) -> ddl.TableSpec: 

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

113 

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

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

116 fields for spatial/temporal elements. 

117 

118 Most callers should use `DimensionElement.makeTableSpec` or 

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

120 that have no table or reference another table. 

121 

122 Parameters 

123 ---------- 

124 element : `DimensionElement` 

125 Element for which to make a table specification. 

126 

127 Returns 

128 ------- 

129 spec : `ddl.TableSpec` 

130 Database-agnostic specification for a table. 

131 """ 

132 tableSpec = ddl.TableSpec( 

133 fields=NamedValueSet(), 

134 unique=set(), 

135 foreignKeys=[] 

136 ) 

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

138 # primary keys in the table for this dimension. 

139 dependencies = [] 

140 for dimension in element.required: 

141 if dimension != element: 

142 addDimensionForeignKey(tableSpec, dimension, primaryKey=True) 

143 dependencies.append(dimension.name) 

144 else: 

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

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

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

148 tableSpec.fields.add(element.primaryKey) 

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

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

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

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

153 # want *recursive* implied dependencies. 

154 for dimension in element.implied: 

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

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

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

158 tableSpec.fields.add(fieldSpec) 

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

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

161 for fieldSpec in element.metadata: 

162 tableSpec.fields.add(fieldSpec) 

163 if element.spatial is not None: 

164 tableSpec.fields.add(REGION_FIELD_SPEC) 

165 if element.temporal is not None: 

166 for fieldSpec in TIMESPAN_FIELD_SPECS: 

167 tableSpec.fields.add(fieldSpec) 

168 return tableSpec