Coverage for python/lsst/daf/butler/registry/dimensions/query.py: 96%

42 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-06 10:52 +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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27from __future__ import annotations 

28 

29__all__ = ["QueryDimensionRecordStorage"] 

30 

31from collections.abc import Mapping 

32from typing import TYPE_CHECKING, Any 

33 

34import sqlalchemy 

35from lsst.daf.relation import Relation 

36 

37from ..._column_tags import DimensionKeyColumnTag 

38from ..._named import NamedKeyMapping 

39from ...dimensions import ( 

40 DatabaseDimension, 

41 DatabaseDimensionElement, 

42 DataCoordinate, 

43 DimensionRecord, 

44 GovernorDimension, 

45) 

46from ..interfaces import ( 

47 Database, 

48 DatabaseDimensionRecordStorage, 

49 GovernorDimensionRecordStorage, 

50 StaticTablesContext, 

51) 

52 

53if TYPE_CHECKING: 

54 from .. import queries 

55 

56 

57class QueryDimensionRecordStorage(DatabaseDimensionRecordStorage): 

58 """A read-only record storage implementation backed by SELECT query. 

59 

60 At present, the only query this class supports is a SELECT DISTINCT over 

61 the table for some other dimension that has this dimension as an implied 

62 dependency. For example, we can use this class to provide access to the 

63 set of ``band`` names referenced by any ``physical_filter``. 

64 

65 Parameters 

66 ---------- 

67 db : `Database` 

68 Interface to the database engine and namespace that will hold these 

69 dimension records. 

70 element : `DatabaseDimensionElement` 

71 The element whose records this storage will manage. 

72 view_target : `DatabaseDimensionRecordStorage` 

73 Storage object for the element this target's storage is a view of. 

74 """ 

75 

76 def __init__( 

77 self, db: Database, element: DatabaseDimensionElement, view_target: DatabaseDimensionRecordStorage 

78 ): 

79 assert isinstance( 

80 element, DatabaseDimension 

81 ), "An element cannot be a dependency unless it is a dimension." 

82 self._db = db 

83 self._element = element 

84 self._target = view_target 

85 if element.name not in self._target.element.minimal_group: 

86 raise NotImplementedError("Query-backed dimension must be a dependency of its target.") 

87 if element.metadata: 

88 raise NotImplementedError("Cannot use query to back dimension with metadata.") 

89 if element.implied: 

90 raise NotImplementedError("Cannot use query to back dimension with implied dependencies.") 

91 if element.alternateKeys: 

92 raise NotImplementedError("Cannot use query to back dimension with alternate unique keys.") 

93 if element.spatial is not None: 

94 raise NotImplementedError("Cannot use query to back spatial dimension.") 

95 if element.temporal is not None: 

96 raise NotImplementedError("Cannot use query to back temporal dimension.") 

97 

98 @classmethod 

99 def initialize( 

100 cls, 

101 db: Database, 

102 element: DatabaseDimensionElement, 

103 *, 

104 context: StaticTablesContext | None = None, 

105 config: Mapping[str, Any], 

106 governors: NamedKeyMapping[GovernorDimension, GovernorDimensionRecordStorage], 

107 view_target: DatabaseDimensionRecordStorage | None = None, 

108 ) -> DatabaseDimensionRecordStorage: 

109 # Docstring inherited from DatabaseDimensionRecordStorage. 

110 assert view_target is not None, f"Storage for '{element}' is a view." 

111 return cls(db, element, view_target) 

112 

113 @property 

114 def element(self) -> DatabaseDimension: 

115 # Docstring inherited from DimensionRecordStorage.element. 

116 return self._element 

117 

118 def clearCaches(self) -> None: 

119 # Docstring inherited from DimensionRecordStorage.clearCaches. 

120 pass 

121 

122 def make_relation(self, context: queries.SqlQueryContext) -> Relation: 

123 # Docstring inherited from DimensionRecordStorage. 

124 columns = DimensionKeyColumnTag.generate([self.element.name]) 

125 return ( 

126 self._target.make_relation(context) 

127 .with_only_columns( 

128 frozenset(columns), 

129 preferred_engine=context.preferred_engine, 

130 require_preferred_engine=True, 

131 ) 

132 .without_duplicates() 

133 ) 

134 

135 def insert(self, *records: DimensionRecord, replace: bool = False, skip_existing: bool = False) -> None: 

136 # Docstring inherited from DimensionRecordStorage.insert. 

137 raise TypeError( 

138 f"Cannot insert {self.element.name} records, define as part of {self._target.element} instead." 

139 ) 

140 

141 def sync(self, record: DimensionRecord, update: bool = False) -> bool: 

142 # Docstring inherited from DimensionRecordStorage.sync. 

143 raise TypeError( 

144 f"Cannot sync {self.element.name} records, define as part of {self._target.element} instead." 

145 ) 

146 

147 def fetch_one(self, data_id: DataCoordinate, context: queries.SqlQueryContext) -> DimensionRecord | None: 

148 # Docstring inherited from DimensionRecordStorage. 

149 # Given the restrictions imposed at construction, we know there's 

150 # nothing to actually fetch: everything we need is in the data ID. 

151 return self.element.RecordClass(**data_id.required) 

152 

153 def digestTables(self) -> list[sqlalchemy.schema.Table]: 

154 # Docstring inherited from DimensionRecordStorage.digestTables. 

155 return []