Coverage for python/lsst/daf/butler/registry/dimensions/overlaps.py: 94%

58 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-04-04 02:05 -0700

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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("CrossFamilyDimensionOverlapStorage",) 

25 

26import logging 

27from collections.abc import Iterable, Mapping, Set 

28from typing import TYPE_CHECKING 

29 

30import sqlalchemy 

31from lsst.daf.relation import Relation 

32 

33from ...core import DatabaseDimensionElement, addDimensionForeignKey, ddl 

34from ..interfaces import ( 

35 Database, 

36 DatabaseDimensionOverlapStorage, 

37 DatabaseDimensionRecordStorage, 

38 GovernorDimensionRecordStorage, 

39 StaticTablesContext, 

40) 

41 

42if TYPE_CHECKING: 

43 from .. import queries 

44 

45 

46_LOG = logging.getLogger(__name__) 

47 

48 

49class CrossFamilyDimensionOverlapStorage(DatabaseDimensionOverlapStorage): 

50 """Basic implementation of materialized overlaps between 

51 otherwise-unrelated dimension elements. 

52 

53 New instances should be constructed by calling `initialize`, not by calling 

54 the constructor directly. 

55 

56 Parameters 

57 ---------- 

58 db : `Database` 

59 Interface to the underlying database engine and namespace. 

60 elementStorage : `tuple` [ `DatabaseDimensionRecordStorage` ] 

61 Storage objects for the elements this object will relate. 

62 governorStorage : `tuple` [ `GovernorDimensionRecordStorage` ] 

63 Storage objects for the governor dimensions of the elements this 

64 object will relate. 

65 summaryTable : `sqlalchemy.schema.Table` 

66 Table that records which combinations of governor dimension values 

67 have materialized overlap rows. 

68 overlapTable : `sqlalchemy.schema.Table` 

69 Table containing the actual materialized overlap rows. 

70 

71 Notes 

72 ----- 

73 At present, this class (like its ABC) is just a stub that creates the 

74 tables it will use, but does nothing else. 

75 """ 

76 

77 def __init__( 

78 self, 

79 db: Database, 

80 elementStorage: tuple[DatabaseDimensionRecordStorage, DatabaseDimensionRecordStorage], 

81 governorStorage: tuple[GovernorDimensionRecordStorage, GovernorDimensionRecordStorage], 

82 summaryTable: sqlalchemy.schema.Table, 

83 overlapTable: sqlalchemy.schema.Table, 

84 ): 

85 self._db = db 

86 self._elementStorage = elementStorage 

87 self._governorStorage = governorStorage 

88 self._summaryTable = summaryTable 

89 self._overlapTable = overlapTable 

90 

91 @classmethod 

92 def initialize( 

93 cls, 

94 db: Database, 

95 elementStorage: tuple[DatabaseDimensionRecordStorage, DatabaseDimensionRecordStorage], 

96 governorStorage: tuple[GovernorDimensionRecordStorage, GovernorDimensionRecordStorage], 

97 context: StaticTablesContext | None = None, 

98 ) -> DatabaseDimensionOverlapStorage: 

99 # Docstring inherited from DatabaseDimensionOverlapStorage. 

100 if context is not None: 100 ↛ 103line 100 didn't jump to line 103, because the condition on line 100 was never false

101 op = context.addTable 

102 else: 

103 op = db.ensureTableExists 

104 elements = (elementStorage[0].element, elementStorage[1].element) 

105 summaryTable = op( 

106 cls._SUMMARY_TABLE_NAME_SPEC.format(*elements), 

107 cls._makeSummaryTableSpec(elements), 

108 ) 

109 overlapTable = op( 

110 cls._OVERLAP_TABLE_NAME_SPEC.format(*elements), 

111 cls._makeOverlapTableSpec(elements), 

112 ) 

113 return cls( 

114 db, 

115 elementStorage, 

116 governorStorage, 

117 summaryTable=summaryTable, 

118 overlapTable=overlapTable, 

119 ) 

120 

121 @property 

122 def elements(self) -> tuple[DatabaseDimensionElement, DatabaseDimensionElement]: 

123 # Docstring inherited from DatabaseDimensionOverlapStorage. 

124 return (self._elementStorage[0].element, self._elementStorage[1].element) 

125 

126 def digestTables(self) -> Iterable[sqlalchemy.schema.Table]: 

127 # Docstring inherited from DatabaseDimensionOverlapStorage. 

128 return [self._summaryTable, self._overlapTable] 

129 

130 _SUMMARY_TABLE_NAME_SPEC = "{0.name}_{1.name}_overlap_summary" 

131 

132 @classmethod 

133 def _makeSummaryTableSpec( 

134 cls, elements: tuple[DatabaseDimensionElement, DatabaseDimensionElement] 

135 ) -> ddl.TableSpec: 

136 """Create a specification for the table that records which combinations 

137 of skypix dimension and governor value have materialized overlaps. 

138 

139 Parameters 

140 ---------- 

141 elements : `tuple` [ `DatabaseDimensionElement` ] 

142 Dimension elements whose overlaps are to be managed. 

143 

144 Returns 

145 ------- 

146 tableSpec : `ddl.TableSpec` 

147 Table specification. 

148 """ 

149 assert elements[0].spatial is not None and elements[1].spatial is not None 

150 assert elements[0].spatial.governor != elements[1].spatial.governor 

151 tableSpec = ddl.TableSpec(fields=[]) 

152 addDimensionForeignKey(tableSpec, elements[0].spatial.governor, primaryKey=True) 

153 addDimensionForeignKey(tableSpec, elements[1].spatial.governor, primaryKey=True) 

154 return tableSpec 

155 

156 _OVERLAP_TABLE_NAME_SPEC = "{0.name}_{1.name}_overlap" 

157 

158 @classmethod 

159 def _makeOverlapTableSpec( 

160 cls, elements: tuple[DatabaseDimensionElement, DatabaseDimensionElement] 

161 ) -> ddl.TableSpec: 

162 """Create a specification for the table that holds materialized 

163 overlap rows. 

164 

165 Parameters 

166 ---------- 

167 elements : `tuple` [ `DatabaseDimensionElement` ] 

168 Dimension elements whose overlaps are to be managed. 

169 

170 Returns 

171 ------- 

172 tableSpec : `ddl.TableSpec` 

173 Table specification. 

174 """ 

175 assert elements[0].graph.required.isdisjoint(elements[1].graph.required) 

176 tableSpec = ddl.TableSpec(fields=[]) 

177 # Add governor dimensions first, so they appear first in the primary 

178 # key; we may often (in the future, perhaps always) know these at 

179 # query-generation time. 

180 for element in elements: 

181 assert element.spatial is not None 

182 addDimensionForeignKey(tableSpec, element.spatial.governor, primaryKey=True) 

183 # Add remaining dimension keys. 

184 for element in elements: 

185 assert element.spatial is not None 

186 for dimension in element.required: 

187 if dimension != element.spatial.governor: 

188 addDimensionForeignKey(tableSpec, dimension, primaryKey=True) 

189 return tableSpec 

190 

191 def clearCaches(self) -> None: 

192 # Docstring inherited from DatabaseDimensionOverlapStorage. 

193 pass 

194 

195 def make_relation( 

196 self, 

197 context: queries.SqlQueryContext, 

198 governor_constraints: Mapping[str, Set[str]], 

199 ) -> Relation | None: 

200 # Docstring inherited. 

201 return None