Coverage for python/lsst/daf/butler/registry/interfaces/_opaque.py: 82%

42 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-30 01:35 -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 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/>. 

27 

28"""Interfaces for the objects that manage opaque (logical) tables within a 

29`Registry`. 

30""" 

31 

32from __future__ import annotations 

33 

34__all__ = ["OpaqueTableStorage", "OpaqueTableStorageManager"] 

35 

36from abc import ABC, abstractmethod 

37from collections.abc import Iterable, Iterator, Mapping, Sequence 

38from typing import TYPE_CHECKING, Any 

39 

40from ...ddl import TableSpec 

41from ._database import Database, StaticTablesContext 

42from ._versioning import VersionedExtension, VersionTuple 

43 

44if TYPE_CHECKING: 

45 from ...datastore import DatastoreTransaction 

46 

47 

48class OpaqueTableStorage(ABC): 

49 """An interface that manages the records associated with a particular 

50 opaque table in a `Registry`. 

51 

52 Parameters 

53 ---------- 

54 name : `str` 

55 Name of the opaque table. 

56 """ 

57 

58 def __init__(self, name: str): 

59 self.name = name 

60 

61 @abstractmethod 

62 def insert(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None: 

63 """Insert records into the table. 

64 

65 Parameters 

66 ---------- 

67 *data 

68 Each additional positional argument is a dictionary that represents 

69 a single row to be added. 

70 transaction : `DatastoreTransaction`, optional 

71 Transaction object that can be used to enable an explicit rollback 

72 of the insert to be registered. Can be ignored if rollback is 

73 handled via a different mechanism, such as by a database. Can be 

74 `None` if no external transaction is available. 

75 """ 

76 raise NotImplementedError() 

77 

78 @abstractmethod 

79 def ensure(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None: 

80 """Insert records into the table, skipping rows that already exist. 

81 

82 Parameters 

83 ---------- 

84 *data 

85 Each additional positional argument is a dictionary that represents 

86 a single row to be added. 

87 transaction : `DatastoreTransaction`, optional 

88 Transaction object that can be used to enable an explicit rollback 

89 of the insert to be registered. Can be ignored if rollback is 

90 handled via a different mechanism, such as by a database. Can be 

91 `None` if no external transaction is available. 

92 """ 

93 raise NotImplementedError() 

94 

95 @abstractmethod 

96 def replace(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None: 

97 """Insert records into the table, replacing if previously existing 

98 but different. 

99 

100 Parameters 

101 ---------- 

102 *data 

103 Each additional positional argument is a dictionary that represents 

104 a single row to be added. 

105 transaction : `DatastoreTransaction`, optional 

106 Transaction object that can be used to enable an explicit rollback 

107 of the insert to be registered. Can be ignored if rollback is 

108 handled via a different mechanism, such as by a database. Can be 

109 `None` if no external transaction is available. 

110 """ 

111 raise NotImplementedError() 

112 

113 @abstractmethod 

114 def fetch(self, **where: Any) -> Iterator[Mapping[Any, Any]]: 

115 """Retrieve records from an opaque table. 

116 

117 Parameters 

118 ---------- 

119 **where 

120 Additional keyword arguments are interpreted as equality 

121 constraints that restrict the returned rows (combined with AND); 

122 keyword arguments are column names and values are the values they 

123 must have. 

124 

125 Yields 

126 ------ 

127 row : `dict` 

128 A dictionary representing a single result row. 

129 """ 

130 raise NotImplementedError() 

131 

132 @abstractmethod 

133 def fetch_batches( 

134 self, 

135 **where: Any, 

136 ) -> Iterator[Sequence[Mapping]]: 

137 """Retrieve records from an opaque table in batches. 

138 

139 Parameters 

140 ---------- 

141 **where 

142 Same as ``OpaqueTableStorage.fetch``. 

143 

144 Yields 

145 ------ 

146 batch 

147 A batch of mappings representing a series of result rows. 

148 """ 

149 raise NotImplementedError() 

150 

151 @abstractmethod 

152 def delete(self, columns: Iterable[str], *rows: dict) -> None: 

153 """Remove records from an opaque table. 

154 

155 Parameters 

156 ---------- 

157 columns : `~collections.abc.Iterable` of `str` 

158 The names of columns that will be used to constrain the rows to 

159 be deleted; these will be combined via ``AND`` to form the 

160 ``WHERE`` clause of the delete query. 

161 *rows 

162 Positional arguments are the keys of rows to be deleted, as 

163 dictionaries mapping column name to value. The keys in all 

164 dictionaries must be exactly the names in ``columns``. 

165 """ 

166 raise NotImplementedError() 

167 

168 name: str 

169 """The name of the logical table this instance manages (`str`). 

170 """ 

171 

172 

173class OpaqueTableStorageManager(VersionedExtension): 

174 """An interface that manages the opaque tables in a `Registry`. 

175 

176 `OpaqueTableStorageManager` primarily serves as a container and factory for 

177 `OpaqueTableStorage` instances, which each provide access to the records 

178 for a different (logical) opaque table. 

179 

180 Parameters 

181 ---------- 

182 registry_schema_version : `VersionTuple` or `None`, optional 

183 Version of registry schema. 

184 

185 Notes 

186 ----- 

187 Opaque tables are primarily used by `Datastore` instances to manage their 

188 internal data in the same database that hold the `Registry`, but are not 

189 limited to this. 

190 

191 While an opaque table in a multi-layer `Registry` may in fact be the union 

192 of multiple tables in different layers, we expect this to be rare, as 

193 `Registry` layers will typically correspond to different leaf `Datastore` 

194 instances (each with their own opaque table) in a `ChainedDatastore`. 

195 """ 

196 

197 def __init__(self, *, registry_schema_version: VersionTuple | None = None): 

198 super().__init__(registry_schema_version=registry_schema_version) 

199 

200 @classmethod 

201 @abstractmethod 

202 def initialize( 

203 cls, db: Database, context: StaticTablesContext, registry_schema_version: VersionTuple | None = None 

204 ) -> OpaqueTableStorageManager: 

205 """Construct an instance of the manager. 

206 

207 Parameters 

208 ---------- 

209 db : `Database` 

210 Interface to the underlying database engine and namespace. 

211 context : `StaticTablesContext` 

212 Context object obtained from `Database.declareStaticTables`; used 

213 to declare any tables that should always be present in a layer 

214 implemented with this manager. 

215 registry_schema_version : `VersionTuple` or `None` 

216 Schema version of this extension as defined in registry. 

217 

218 Returns 

219 ------- 

220 manager : `OpaqueTableStorageManager` 

221 An instance of a concrete `OpaqueTableStorageManager` subclass. 

222 """ 

223 raise NotImplementedError() 

224 

225 def __getitem__(self, name: str) -> OpaqueTableStorage: 

226 """Interface to `get` that raises `LookupError` instead of returning 

227 `None` on failure. 

228 """ 

229 r = self.get(name) 

230 if r is None: 

231 raise LookupError(f"No logical table with name '{name}' found.") 

232 return r 

233 

234 @abstractmethod 

235 def get(self, name: str) -> OpaqueTableStorage | None: 

236 """Return an object that provides access to the records associated with 

237 an opaque logical table. 

238 

239 Parameters 

240 ---------- 

241 name : `str` 

242 Name of the logical table. 

243 

244 Returns 

245 ------- 

246 records : `OpaqueTableStorage` or `None` 

247 The object representing the records for the given table in this 

248 layer, or `None` if there are no records for that table in this 

249 layer. 

250 

251 Notes 

252 ----- 

253 Opaque tables must be registered with the layer (see `register`) by 

254 the same client before they can safely be retrieved with `get`. 

255 Unlike most other manager classes, the set of opaque tables cannot be 

256 obtained from an existing data repository. 

257 """ 

258 raise NotImplementedError() 

259 

260 @abstractmethod 

261 def register(self, name: str, spec: TableSpec) -> OpaqueTableStorage: 

262 """Ensure that this layer can hold records for the given opaque logical 

263 table, creating new tables as necessary. 

264 

265 Parameters 

266 ---------- 

267 name : `str` 

268 Name of the logical table. 

269 spec : `TableSpec` 

270 Schema specification for the table to be created. 

271 

272 Returns 

273 ------- 

274 records : `OpaqueTableStorage` 

275 The object representing the records for the given element in this 

276 layer. 

277 

278 Notes 

279 ----- 

280 This operation may not be invoked within a transaction context block. 

281 """ 

282 raise NotImplementedError() 

283 

284 @abstractmethod 

285 def clone(self, db: Database) -> OpaqueTableStorageManager: 

286 """Make an independent copy of this manager instance bound to a new 

287 `Database` instance. 

288 

289 Parameters 

290 ---------- 

291 db : `Database` 

292 New `Database` object to use when instantiating the manager. 

293 

294 Returns 

295 ------- 

296 instance : `OpaqueTableStorageManager` 

297 New manager instance with the same configuration as this instance, 

298 but bound to a new Database object. 

299 """ 

300 raise NotImplementedError()