Coverage for python/lsst/daf/butler/registry/interfaces/_dimensions.py: 95%

37 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-11 01:27 -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/>. 

27from __future__ import annotations 

28 

29__all__ = ("DimensionRecordStorageManager",) 

30 

31from abc import abstractmethod 

32from collections.abc import Iterable, Mapping, Set 

33from typing import TYPE_CHECKING, Any 

34 

35from lsst.daf.relation import Join, Relation 

36 

37from ...dimensions import ( 

38 DataCoordinate, 

39 DataIdValue, 

40 DimensionElement, 

41 DimensionGroup, 

42 DimensionRecord, 

43 DimensionRecordSet, 

44 DimensionUniverse, 

45) 

46from ...dimensions.record_cache import DimensionRecordCache 

47from ._versioning import VersionedExtension, VersionTuple 

48 

49if TYPE_CHECKING: 

50 from ...direct_query_driver import ( # Future query system (direct,server). 

51 Postprocessing, 

52 SqlJoinsBuilder, 

53 SqlSelectBuilder, 

54 ) 

55 from ...queries.tree import AnyDatasetType, Predicate # Future query system (direct,client,server). 

56 from .. import queries # Old Registry.query* system. 

57 from ._database import Database, StaticTablesContext 

58 

59 

60class DimensionRecordStorageManager(VersionedExtension): 

61 """An interface for managing the dimension records in a `Registry`. 

62 

63 `DimensionRecordStorageManager` primarily serves as a container and factory 

64 for `DimensionRecordStorage` instances, which each provide access to the 

65 records for a different `DimensionElement`. 

66 

67 Parameters 

68 ---------- 

69 universe : `DimensionUniverse` 

70 Universe of all dimensions and dimension elements known to the 

71 `Registry`. 

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

73 Version of registry schema. 

74 

75 Notes 

76 ----- 

77 In a multi-layer `Registry`, many dimension elements will only have 

78 records in one layer (often the base layer). The union of the records 

79 across all layers forms the logical table for the full `Registry`. 

80 """ 

81 

82 def __init__(self, *, universe: DimensionUniverse, registry_schema_version: VersionTuple | None = None): 

83 super().__init__(registry_schema_version=registry_schema_version) 

84 self.universe = universe 

85 

86 @abstractmethod 

87 def clone(self, db: Database) -> DimensionRecordStorageManager: 

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

89 `Database` instance. 

90 

91 Parameters 

92 ---------- 

93 db : `Database` 

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

95 

96 Returns 

97 ------- 

98 instance : `DatasetRecordStorageManager` 

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

100 but bound to a new Database object. 

101 """ 

102 raise NotImplementedError() 

103 

104 @classmethod 

105 @abstractmethod 

106 def initialize( 

107 cls, 

108 db: Database, 

109 context: StaticTablesContext, 

110 *, 

111 universe: DimensionUniverse, 

112 registry_schema_version: VersionTuple | None = None, 

113 ) -> DimensionRecordStorageManager: 

114 """Construct an instance of the manager. 

115 

116 Parameters 

117 ---------- 

118 db : `Database` 

119 Interface to the underlying database engine and namespace. 

120 context : `StaticTablesContext` 

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

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

123 implemented with this manager. 

124 universe : `DimensionUniverse` 

125 Universe graph containing dimensions known to this `Registry`. 

126 registry_schema_version : `VersionTuple` or `None` 

127 Schema version of this extension as defined in registry. 

128 

129 Returns 

130 ------- 

131 manager : `DimensionRecordStorageManager` 

132 An instance of a concrete `DimensionRecordStorageManager` subclass. 

133 """ 

134 raise NotImplementedError() 

135 

136 def fetch_cache_dict(self) -> dict[str, DimensionRecordSet]: 

137 """Return a `dict` that can back a `DimensionRecordSet`. 

138 

139 This method is intended as the ``fetch`` callback argument to 

140 `DimensionRecordCache`, in contexts where direct SQL queries are 

141 possible. 

142 """ 

143 raise NotImplementedError() 

144 

145 @abstractmethod 

146 def insert( 

147 self, 

148 element: DimensionElement, 

149 *records: DimensionRecord, 

150 replace: bool = False, 

151 skip_existing: bool = False, 

152 ) -> None: 

153 """Insert one or more records into storage. 

154 

155 Parameters 

156 ---------- 

157 element : `DimensionElement` 

158 Dimension element that provides the definition for records. 

159 *records : `DimensionRecord` 

160 One or more instances of the `DimensionRecord` subclass for the 

161 element this storage is associated with. 

162 replace : `bool`, optional 

163 If `True` (`False` is default), replace existing records in the 

164 database if there is a conflict. 

165 skip_existing : `bool`, optional 

166 If `True` (`False` is default), skip insertion if a record with 

167 the same primary key values already exists. 

168 

169 Raises 

170 ------ 

171 TypeError 

172 Raised if the element does not support record insertion. 

173 sqlalchemy.exc.IntegrityError 

174 Raised if one or more records violate database integrity 

175 constraints. 

176 """ 

177 raise NotImplementedError() 

178 

179 @abstractmethod 

180 def sync(self, record: DimensionRecord, update: bool = False) -> bool | dict[str, Any]: 

181 """Synchronize a record with the database, inserting it only if it does 

182 not exist and comparing values if it does. 

183 

184 Parameters 

185 ---------- 

186 record : `DimensionRecord` 

187 An instance of the `DimensionRecord` subclass for the 

188 element this storage is associated with. 

189 update : `bool`, optional 

190 If `True` (`False` is default), update the existing record in the 

191 database if there is a conflict. 

192 

193 Returns 

194 ------- 

195 inserted_or_updated : `bool` or `dict` 

196 `True` if a new row was inserted, `False` if no changes were 

197 needed, or a `dict` mapping updated column names to their old 

198 values if an update was performed (only possible if 

199 ``update=True``). 

200 

201 Raises 

202 ------ 

203 DatabaseConflictError 

204 Raised if the record exists in the database (according to primary 

205 key lookup) but is inconsistent with the given one. 

206 TypeError 

207 Raised if the element does not support record synchronization. 

208 sqlalchemy.exc.IntegrityError 

209 Raised if one or more records violate database integrity 

210 constraints. 

211 """ 

212 raise NotImplementedError() 

213 

214 @abstractmethod 

215 def fetch_one( 

216 self, 

217 element_name: str, 

218 data_id: DataCoordinate, 

219 cache: DimensionRecordCache, 

220 ) -> DimensionRecord | None: 

221 """Retrieve a single record from storage. 

222 

223 Parameters 

224 ---------- 

225 element_name : `str` 

226 Name of the dimension element for the record to fetch. 

227 data_id : `DataCoordinate` 

228 Data ID of the record to fetch. Implied dimensions do not need to 

229 be present. 

230 cache : `DimensionRecordCache` 

231 Cache to look in first. 

232 

233 Returns 

234 ------- 

235 record : `DimensionRecord` or `None` 

236 Fetched record, or *possibly* `None` if there was no match for the 

237 given data ID. 

238 """ 

239 raise NotImplementedError() 

240 

241 @abstractmethod 

242 def save_dimension_group(self, group: DimensionGroup) -> int: 

243 """Save a `DimensionGroup` definition to the database, allowing it to 

244 be retrieved later via the returned key. 

245 

246 If this dimension group has already been saved, this method just 

247 returns the key already associated with it. 

248 

249 Parameters 

250 ---------- 

251 group : `DimensionGroup` 

252 Set of dimensions to save. 

253 

254 Returns 

255 ------- 

256 key : `int` 

257 Integer used as the unique key for this `DimensionGroup` in the 

258 database. 

259 

260 Raises 

261 ------ 

262 TransactionInterruption 

263 Raised if this operation is invoked within a `Database.transaction` 

264 context. 

265 """ 

266 raise NotImplementedError() 

267 

268 @abstractmethod 

269 def load_dimension_group(self, key: int) -> DimensionGroup: 

270 """Retrieve a `DimensionGroup` that was previously saved in the 

271 database. 

272 

273 Parameters 

274 ---------- 

275 key : `int` 

276 Integer used as the unique key for this `DimensionGroup` in the 

277 database. 

278 

279 Returns 

280 ------- 

281 dimensions : `DimensionGroup` 

282 Retrieved dimensions. 

283 

284 Raises 

285 ------ 

286 KeyError 

287 Raised if the given key cannot be found in the database. 

288 """ 

289 raise NotImplementedError() 

290 

291 @abstractmethod 

292 def join( 

293 self, 

294 element_name: str, 

295 target: Relation, 

296 join: Join, 

297 context: queries.SqlQueryContext, 

298 ) -> Relation: 

299 """Join this dimension element's records to a relation. 

300 

301 Parameters 

302 ---------- 

303 element_name : `str` 

304 Name of the dimension element whose relation should be joined in. 

305 target : `~lsst.daf.relation.Relation` 

306 Existing relation to join to. Implementations may require that 

307 this relation already include dimension key columns for this 

308 dimension element and assume that dataset or spatial join relations 

309 that might provide these will be included in the relation tree 

310 first. 

311 join : `~lsst.daf.relation.Join` 

312 Join operation to use when the implementation is an actual join. 

313 When a true join is being simulated by other relation operations, 

314 this objects `~lsst.daf.relation.Join.min_columns` and 

315 `~lsst.daf.relation.Join.max_columns` should still be respected. 

316 context : `.queries.SqlQueryContext` 

317 Object that manages relation engines and database-side state (e.g. 

318 temporary tables) for the query. 

319 

320 Returns 

321 ------- 

322 joined : `~lsst.daf.relation.Relation` 

323 New relation that includes this relation's dimension key and record 

324 columns, as well as all columns in ``target``, with rows 

325 constrained to those for which this element's dimension key values 

326 exist in the registry and rows already exist in ``target``. 

327 """ 

328 raise NotImplementedError() 

329 

330 @abstractmethod 

331 def make_joins_builder(self, element: DimensionElement, fields: Set[str]) -> SqlJoinsBuilder: 

332 """Make a `..direct_query_driver.SqlJoinsBuilder` that represents a 

333 dimension element table. 

334 

335 Parameters 

336 ---------- 

337 element : `DimensionElement` 

338 Dimension element the table corresponds to. 

339 fields : `~collections.abc.Set` [ `str` ] 

340 Names of fields to make available in the builder. These can be any 

341 metadata or alternate key field in the element's schema, including 

342 the special ``region`` and ``timespan`` fields. Dimension keys in 

343 the element's schema are always included. 

344 

345 Returns 

346 ------- 

347 builder : `..direct_query_driver.SqlJoinsBuilder` 

348 A query-construction object representing a table or subquery. This 

349 is guaranteed to have rows that are unique over dimension keys and 

350 all possible key values for this dimension, so joining in a 

351 dimension element table: 

352 

353 - never introduces duplicates into the query's result rows; 

354 - never restricts the query's rows *except* to ensure 

355 required-implied relationships are followed. 

356 """ 

357 raise NotImplementedError() 

358 

359 @abstractmethod 

360 def process_query_overlaps( 

361 self, 

362 dimensions: DimensionGroup, 

363 predicate: Predicate, 

364 join_operands: Iterable[DimensionGroup], 

365 calibration_dataset_types: Set[str | AnyDatasetType], 

366 allow_duplicates: bool, 

367 constraint_data_id: Mapping[str, DataIdValue], 

368 ) -> tuple[Predicate, SqlSelectBuilder, Postprocessing]: 

369 """Process a query's WHERE predicate and dimensions to handle spatial 

370 and temporal overlaps. 

371 

372 Parameters 

373 ---------- 

374 dimensions : `..dimensions.DimensionGroup` 

375 Full dimensions of all tables to be joined into the query (even if 

376 they are not included in the query results). 

377 predicate : `..queries.tree.Predicate` 

378 Boolean column expression that may contain user-provided spatial 

379 and/or temporal overlaps intermixed with other constraints. 

380 join_operands : `~collections.abc.Iterable` [ \ 

381 `..dimensions.DimensionGroup` ] 

382 Dimensions of tables or subqueries that are already going to be 

383 joined into the query that may establish their own spatial or 

384 temporal relationships (e.g. a dataset search with both ``visit`` 

385 and ``patch`` dimensions). 

386 calibration_dataset_types : `~collections.abc.Set` [ `str` or \ 

387 `..queries.tree.AnyDatasetType` ] 

388 The names of dataset types that have been joined into the query via 

389 a search that includes at least one calibration collection. 

390 allow_duplicates : `bool` 

391 If set to `True` then query will be allowed to return non-distinct 

392 rows. 

393 constraint_data_id : `~collections.abc.Mapping` [`str`, `int` | `str`] 

394 Dimension values that are known to be common to all rows in the 

395 query result set. 

396 

397 Returns 

398 ------- 

399 predicate : `..queries.tree.Predicate` 

400 A version of the given predicate that preserves the overall 

401 behavior of the filter while possibly rewriting overlap expressions 

402 that have been partially moved into ``builder`` as some combination 

403 of new nested predicates, joins, and postprocessing. 

404 builder : `..direct_query_driver.SqlSelectBuilder` 

405 A query-construction helper object that includes any initial joins 

406 and postprocessing needed to handle overlap expression extracted 

407 from the original predicate. 

408 postprocessing : `Postprocessing` 

409 Struct representing post-query processing to be done in Python. 

410 

411 Notes 

412 ----- 

413 Implementations must delegate to `.queries.overlaps.OverlapsVisitor` 

414 (possibly by subclassing it) to ensure "automatic" spatial and temporal 

415 joins are added consistently by all query-construction implementations. 

416 """ 

417 raise NotImplementedError() 

418 

419 universe: DimensionUniverse 

420 """Universe of all dimensions and dimension elements known to the 

421 `Registry` (`DimensionUniverse`). 

422 """