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

111 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-02 09:50 +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 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 "DatabaseDimensionOverlapStorage", 

25 "DatabaseDimensionRecordStorage", 

26 "DimensionRecordStorage", 

27 "DimensionRecordStorageManager", 

28 "GovernorDimensionRecordStorage", 

29 "SkyPixDimensionRecordStorage", 

30) 

31 

32from abc import ABC, abstractmethod 

33from collections.abc import Callable, Iterable, Mapping, Set 

34from typing import TYPE_CHECKING, Any, Tuple, Union 

35 

36import sqlalchemy 

37from lsst.daf.relation import Join, Relation, sql 

38 

39from ...core import ( 

40 ColumnTypeInfo, 

41 DatabaseDimensionElement, 

42 DataCoordinate, 

43 DimensionElement, 

44 DimensionGraph, 

45 DimensionRecord, 

46 DimensionUniverse, 

47 GovernorDimension, 

48 LogicalColumn, 

49 SkyPixDimension, 

50) 

51from ...core.named import NamedKeyMapping 

52from ._versioning import VersionedExtension, VersionTuple 

53 

54if TYPE_CHECKING: 

55 from .. import queries 

56 from ._database import Database, StaticTablesContext 

57 

58 

59OverlapSide = Union[SkyPixDimension, Tuple[DatabaseDimensionElement, str]] 

60 

61 

62class DimensionRecordStorage(ABC): 

63 """An abstract base class that represents a way of storing the records 

64 associated with a single `DimensionElement`. 

65 

66 Concrete `DimensionRecordStorage` instances should generally be constructed 

67 via a call to `setupDimensionStorage`, which selects the appropriate 

68 subclass for each element according to its configuration. 

69 

70 All `DimensionRecordStorage` methods are pure abstract, even though in some 

71 cases a reasonable default implementation might be possible, in order to 

72 better guarantee all methods are correctly overridden. All of these 

73 potentially-defaultable implementations are extremely trivial, so asking 

74 subclasses to provide them is not a significant burden. 

75 """ 

76 

77 @property 

78 @abstractmethod 

79 def element(self) -> DimensionElement: 

80 """The element whose records this instance managers 

81 (`DimensionElement`). 

82 """ 

83 raise NotImplementedError() 

84 

85 @abstractmethod 

86 def clearCaches(self) -> None: 

87 """Clear any in-memory caches held by the storage instance. 

88 

89 This is called by `Registry` when transactions are rolled back, to 

90 avoid in-memory caches from ever containing records that are not 

91 present in persistent storage. 

92 """ 

93 raise NotImplementedError() 

94 

95 @abstractmethod 

96 def join( 

97 self, 

98 target: Relation, 

99 join: Join, 

100 context: queries.SqlQueryContext, 

101 ) -> Relation: 

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

103 

104 Parameters 

105 ---------- 

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

107 Existing relation to join to. Implementations may require that 

108 this relation already include dimension key columns for this 

109 dimension element and assume that dataset or spatial join relations 

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

111 first. 

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

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

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

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

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

117 context : `.queries.SqlQueryContext` 

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

119 temporary tables) for the query. 

120 

121 Returns 

122 ------- 

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

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

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

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

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

128 """ 

129 raise NotImplementedError() 

130 

131 @abstractmethod 

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

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

134 

135 Parameters 

136 ---------- 

137 records 

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

139 element this storage is associated with. 

140 replace: `bool`, optional 

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

142 database if there is a conflict. 

143 skip_existing : `bool`, optional 

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

145 the same primary key values already exists. 

146 

147 Raises 

148 ------ 

149 TypeError 

150 Raised if the element does not support record insertion. 

151 sqlalchemy.exc.IntegrityError 

152 Raised if one or more records violate database integrity 

153 constraints. 

154 

155 Notes 

156 ----- 

157 As `insert` is expected to be called only by a `Registry`, we rely 

158 on `Registry` to provide transactionality, both by using a SQLALchemy 

159 connection shared with the `Registry` and by relying on it to call 

160 `clearCaches` when rolling back transactions. 

161 """ 

162 raise NotImplementedError() 

163 

164 @abstractmethod 

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

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

167 not exist and comparing values if it does. 

168 

169 Parameters 

170 ---------- 

171 record : `DimensionRecord`. 

172 An instance of the `DimensionRecord` subclass for the 

173 element this storage is associated with. 

174 update: `bool`, optional 

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

176 database if there is a conflict. 

177 

178 Returns 

179 ------- 

180 inserted_or_updated : `bool` or `dict` 

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

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

183 values if an update was performed (only possible if 

184 ``update=True``). 

185 

186 Raises 

187 ------ 

188 DatabaseConflictError 

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

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

191 TypeError 

192 Raised if the element does not support record synchronization. 

193 sqlalchemy.exc.IntegrityError 

194 Raised if one or more records violate database integrity 

195 constraints. 

196 """ 

197 raise NotImplementedError() 

198 

199 @abstractmethod 

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

201 """Retrieve a single record from storage. 

202 

203 Parameters 

204 ---------- 

205 data_id : `DataCoordinate` 

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

207 be present. 

208 context : `.queries.SqlQueryContext` 

209 Context to be used to execute queries when no cached result is 

210 available. 

211 

212 Returns 

213 ------- 

214 record : `DimensionRecord` or `None` 

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

216 given data ID. 

217 """ 

218 raise NotImplementedError() 

219 

220 def get_record_cache( 

221 self, context: queries.SqlQueryContext 

222 ) -> Mapping[DataCoordinate, DimensionRecord] | None: 

223 """Return a local cache of all `DimensionRecord` objects for this 

224 element, fetching it if necessary. 

225 

226 Implementations that never cache records should return `None`. 

227 

228 Parameters 

229 ---------- 

230 context : `.queries.SqlQueryContext` 

231 Context to be used to execute queries when no cached result is 

232 available. 

233 

234 Returns 

235 ------- 

236 cache : `Mapping` [ `DataCoordinate`, `DimensionRecord` ] or `None` 

237 Mapping from data ID to dimension record, or `None`. 

238 """ 

239 return None 

240 

241 @abstractmethod 

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

243 """Return tables used for schema digest. 

244 

245 Returns 

246 ------- 

247 tables : `list` [ `sqlalchemy.schema.Table` ] 

248 Possibly empty list of tables for schema digest calculations. 

249 """ 

250 raise NotImplementedError() 

251 

252 def _build_sql_payload( 

253 self, 

254 from_clause: sqlalchemy.sql.FromClause, 

255 column_types: ColumnTypeInfo, 

256 ) -> sql.Payload[LogicalColumn]: 

257 """Construct a `lsst.daf.relation.sql.Payload` for a dimension table. 

258 

259 This is a conceptually "protected" helper method for use by subclass 

260 `make_relation` implementations. 

261 

262 Parameters 

263 ---------- 

264 from_clause : `sqlalchemy.sql.FromClause` 

265 SQLAlchemy table or subquery to select from. 

266 column_types : `ColumnTypeInfo` 

267 Struct with information about column types that depend on registry 

268 configuration. 

269 

270 Returns 

271 ------- 

272 payload : `lsst.daf.relation.sql.Payload` 

273 Relation SQL "payload" struct, containing a SQL FROM clause, 

274 columns, and optional WHERE clause. 

275 """ 

276 payload = sql.Payload[LogicalColumn](from_clause) 

277 for tag, field_name in self.element.RecordClass.fields.columns.items(): 

278 if field_name == "timespan": 

279 payload.columns_available[tag] = column_types.timespan_cls.from_columns( 

280 from_clause.columns, name=field_name 

281 ) 

282 else: 

283 payload.columns_available[tag] = from_clause.columns[field_name] 

284 return payload 

285 

286 

287class GovernorDimensionRecordStorage(DimensionRecordStorage): 

288 """Intermediate interface for `DimensionRecordStorage` objects that provide 

289 storage for `GovernorDimension` instances. 

290 """ 

291 

292 @classmethod 

293 @abstractmethod 

294 def initialize( 

295 cls, 

296 db: Database, 

297 dimension: GovernorDimension, 

298 *, 

299 context: StaticTablesContext | None = None, 

300 config: Mapping[str, Any], 

301 ) -> GovernorDimensionRecordStorage: 

302 """Construct an instance of this class using a standardized interface. 

303 

304 Parameters 

305 ---------- 

306 db : `Database` 

307 Interface to the underlying database engine and namespace. 

308 dimension : `GovernorDimension` 

309 Dimension the new instance will manage records for. 

310 context : `StaticTablesContext`, optional 

311 If provided, an object to use to create any new tables. If not 

312 provided, ``db.ensureTableExists`` should be used instead. 

313 config : `Mapping` 

314 Extra configuration options specific to the implementation. 

315 

316 Returns 

317 ------- 

318 storage : `GovernorDimensionRecordStorage` 

319 A new `GovernorDimensionRecordStorage` subclass instance. 

320 """ 

321 raise NotImplementedError() 

322 

323 @property 

324 @abstractmethod 

325 def element(self) -> GovernorDimension: 

326 # Docstring inherited from DimensionRecordStorage. 

327 raise NotImplementedError() 

328 

329 @property 

330 @abstractmethod 

331 def table(self) -> sqlalchemy.schema.Table: 

332 """The SQLAlchemy table that backs this dimension 

333 (`sqlalchemy.schema.Table`). 

334 """ 

335 raise NotImplementedError() 

336 

337 def join( 

338 self, 

339 target: Relation, 

340 join: Join, 

341 context: queries.SqlQueryContext, 

342 ) -> Relation: 

343 # Docstring inherited. 

344 # We use Join.partial(...).apply(...) instead of Join.apply(..., ...) 

345 # for the "backtracking" insertion capabilities of the former; more 

346 # specifically, if `target` is a tree that starts with SQL relations 

347 # and ends with iteration-engine operations (e.g. region-overlap 

348 # postprocessing), this will try to perform the join upstream in the 

349 # SQL engine before the transfer to iteration. 

350 return join.partial(self.make_relation(context)).apply(target) 

351 

352 @abstractmethod 

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

354 """Return a relation that represents this dimension element's table. 

355 

356 This is used to provide an implementation for 

357 `DimensionRecordStorage.join`, and is also callable in its own right. 

358 

359 Parameters 

360 ---------- 

361 context : `.queries.SqlQueryContext` 

362 Object that manages relation engines and database-side state 

363 (e.g. temporary tables) for the query. 

364 

365 Returns 

366 ------- 

367 relation : `~lsst.daf.relation.Relation` 

368 New relation that includes this relation's dimension key and 

369 record columns, with rows constrained to those for which the 

370 dimension key values exist in the registry. 

371 """ 

372 raise NotImplementedError() 

373 

374 @abstractmethod 

375 def get_record_cache(self, context: queries.SqlQueryContext) -> Mapping[DataCoordinate, DimensionRecord]: 

376 raise NotImplementedError() 

377 

378 @abstractmethod 

379 def registerInsertionListener(self, callback: Callable[[DimensionRecord], None]) -> None: 

380 """Add a function or method to be called after new records for this 

381 dimension are inserted by `insert` or `sync`. 

382 

383 Parameters 

384 ---------- 

385 callback 

386 Callable that takes a single `DimensionRecord` argument. This will 

387 be called immediately after any successful insertion, in the same 

388 transaction. 

389 """ 

390 raise NotImplementedError() 

391 

392 

393class SkyPixDimensionRecordStorage(DimensionRecordStorage): 

394 """Intermediate interface for `DimensionRecordStorage` objects that provide 

395 storage for `SkyPixDimension` instances. 

396 """ 

397 

398 @property 

399 @abstractmethod 

400 def element(self) -> SkyPixDimension: 

401 # Docstring inherited from DimensionRecordStorage. 

402 raise NotImplementedError() 

403 

404 

405class DatabaseDimensionRecordStorage(DimensionRecordStorage): 

406 """Intermediate interface for `DimensionRecordStorage` objects that provide 

407 storage for `DatabaseDimensionElement` instances. 

408 """ 

409 

410 @classmethod 

411 @abstractmethod 

412 def initialize( 

413 cls, 

414 db: Database, 

415 element: DatabaseDimensionElement, 

416 *, 

417 context: StaticTablesContext | None = None, 

418 config: Mapping[str, Any], 

419 governors: NamedKeyMapping[GovernorDimension, GovernorDimensionRecordStorage], 

420 view_target: DatabaseDimensionRecordStorage | None = None, 

421 ) -> DatabaseDimensionRecordStorage: 

422 """Construct an instance of this class using a standardized interface. 

423 

424 Parameters 

425 ---------- 

426 db : `Database` 

427 Interface to the underlying database engine and namespace. 

428 element : `DatabaseDimensionElement` 

429 Dimension element the new instance will manage records for. 

430 context : `StaticTablesContext`, optional 

431 If provided, an object to use to create any new tables. If not 

432 provided, ``db.ensureTableExists`` should be used instead. 

433 config : `Mapping` 

434 Extra configuration options specific to the implementation. 

435 governors : `NamedKeyMapping` 

436 Mapping containing all governor dimension storage implementations. 

437 view_target : `DatabaseDimensionRecordStorage`, optional 

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

439 (i.e. when `viewOf` is not `None`). 

440 

441 Returns 

442 ------- 

443 storage : `DatabaseDimensionRecordStorage` 

444 A new `DatabaseDimensionRecordStorage` subclass instance. 

445 """ 

446 raise NotImplementedError() 

447 

448 @property 

449 @abstractmethod 

450 def element(self) -> DatabaseDimensionElement: 

451 # Docstring inherited from DimensionRecordStorage. 

452 raise NotImplementedError() 

453 

454 def join( 

455 self, 

456 target: Relation, 

457 join: Join, 

458 context: queries.SqlQueryContext, 

459 ) -> Relation: 

460 # Docstring inherited. 

461 # See comment on similar code in GovernorDimensionRecordStorage.join 

462 # for why we use `Join.partial` here. 

463 return join.partial(self.make_relation(context)).apply(target) 

464 

465 @abstractmethod 

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

467 """Return a relation that represents this dimension element's table. 

468 

469 This is used to provide an implementation for 

470 `DimensionRecordStorage.join`, and is also callable in its own right. 

471 

472 Parameters 

473 ---------- 

474 context : `.queries.SqlQueryContext` 

475 Object that manages relation engines and database-side state 

476 (e.g. temporary tables) for the query. 

477 

478 Returns 

479 ------- 

480 relation : `~lsst.daf.relation.Relation` 

481 New relation that includes this relation's dimension key and 

482 record columns, with rows constrained to those for which the 

483 dimension key values exist in the registry. 

484 """ 

485 raise NotImplementedError() 

486 

487 def connect(self, overlaps: DatabaseDimensionOverlapStorage) -> None: 

488 """Inform this record storage object of the object that will manage 

489 the overlaps between this element and another element. 

490 

491 This will only be called if ``self.element.spatial is not None``, 

492 and will be called immediately after construction (before any other 

493 methods). In the future, implementations will be required to call a 

494 method on any connected overlap storage objects any time new records 

495 for the element are inserted. 

496 

497 Parameters 

498 ---------- 

499 overlaps : `DatabaseDimensionRecordStorage` 

500 Object managing overlaps between this element and another 

501 database-backed element. 

502 """ 

503 raise NotImplementedError(f"{type(self).__name__} does not support spatial elements.") 

504 

505 def make_spatial_join_relation( 

506 self, 

507 other: DimensionElement, 

508 context: queries.SqlQueryContext, 

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

510 ) -> Relation | None: 

511 """Return a `lsst.daf.relation.Relation` that represents the spatial 

512 overlap join between two dimension elements. 

513 

514 High-level code should generally call 

515 `DimensionRecordStorageManager.make_spatial_join_relation` (which 

516 delegates to this) instead of calling this method directly. 

517 

518 Parameters 

519 ---------- 

520 other : `DimensionElement` 

521 Element to compute overlaps with. Guaranteed by caller to be 

522 spatial (as is ``self``), with a different topological family. May 

523 be a `DatabaseDimensionElement` or a `SkyPixDimension`. 

524 context : `.queries.SqlQueryContext` 

525 Object that manages relation engines and database-side state 

526 (e.g. temporary tables) for the query. 

527 governor_constraints : `Mapping` [ `str`, `~collections.abc.Set` ], \ 

528 optional 

529 Constraints imposed by other aspects of the query on governor 

530 dimensions. 

531 

532 Returns 

533 ------- 

534 relation : `lsst.daf.relation.Relation` or `None` 

535 Join relation. Should be `None` when no direct overlaps for this 

536 combination are stored; higher-level code is responsible for 

537 working out alternative approaches involving multiple joins. 

538 """ 

539 return None 

540 

541 

542class DatabaseDimensionOverlapStorage(ABC): 

543 """A base class for objects that manage overlaps between a pair of 

544 database-backed dimensions. 

545 """ 

546 

547 @classmethod 

548 @abstractmethod 

549 def initialize( 

550 cls, 

551 db: Database, 

552 elementStorage: tuple[DatabaseDimensionRecordStorage, DatabaseDimensionRecordStorage], 

553 governorStorage: tuple[GovernorDimensionRecordStorage, GovernorDimensionRecordStorage], 

554 context: StaticTablesContext | None = None, 

555 ) -> DatabaseDimensionOverlapStorage: 

556 """Construct an instance of this class using a standardized interface. 

557 

558 Parameters 

559 ---------- 

560 db : `Database` 

561 Interface to the underlying database engine and namespace. 

562 elementStorage : `tuple` [ `DatabaseDimensionRecordStorage` ] 

563 Storage objects for the elements this object will related. 

564 governorStorage : `tuple` [ `GovernorDimensionRecordStorage` ] 

565 Storage objects for the governor dimensions of the elements this 

566 object will related. 

567 context : `StaticTablesContext`, optional 

568 If provided, an object to use to create any new tables. If not 

569 provided, ``db.ensureTableExists`` should be used instead. 

570 

571 Returns 

572 ------- 

573 storage : `DatabaseDimensionOverlapStorage` 

574 A new `DatabaseDimensionOverlapStorage` subclass instance. 

575 """ 

576 raise NotImplementedError() 

577 

578 @property 

579 @abstractmethod 

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

581 """The pair of elements whose overlaps this object manages. 

582 

583 The order of elements is the same as their ordering within the 

584 `DimensionUniverse`. 

585 """ 

586 raise NotImplementedError() 

587 

588 @abstractmethod 

589 def clearCaches(self) -> None: 

590 """Clear any cached state about which overlaps have been 

591 materialized.""" 

592 raise NotImplementedError() 

593 

594 @abstractmethod 

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

596 """Return tables used for schema digest. 

597 

598 Returns 

599 ------- 

600 tables : `Iterable` [ `sqlalchemy.schema.Table` ] 

601 Possibly empty set of tables for schema digest calculations. 

602 """ 

603 raise NotImplementedError() 

604 

605 @abstractmethod 

606 def make_relation( 

607 self, 

608 context: queries.SqlQueryContext, 

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

610 ) -> Relation | None: 

611 """Return a `lsst.daf.relation.Relation` that represents the join 

612 table. 

613 

614 High-level code should generally call 

615 `DimensionRecordStorageManager.make_spatial_join_relation` (which 

616 delegates to this) instead of calling this method directly. 

617 

618 Parameters 

619 ---------- 

620 context : `.queries.SqlQueryContext` 

621 Object that manages relation engines and database-side state 

622 (e.g. temporary tables) for the query. 

623 governor_constraints : `Mapping` [ `str`, `~collections.abc.Set` ], \ 

624 optional 

625 Constraints imposed by other aspects of the query on governor 

626 dimensions; collections inconsistent with these constraints will be 

627 skipped. 

628 

629 Returns 

630 ------- 

631 relation : `lsst.daf.relation.Relation` or `None` 

632 Join relation. Should be `None` when no direct overlaps for this 

633 combination are stored; higher-level code is responsible for 

634 working out alternative approaches involving multiple joins. 

635 """ 

636 raise NotImplementedError() 

637 

638 

639class DimensionRecordStorageManager(VersionedExtension): 

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

641 

642 `DimensionRecordStorageManager` primarily serves as a container and factory 

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

644 records for a different `DimensionElement`. 

645 

646 Parameters 

647 ---------- 

648 universe : `DimensionUniverse` 

649 Universe of all dimensions and dimension elements known to the 

650 `Registry`. 

651 

652 Notes 

653 ----- 

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

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

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

657 """ 

658 

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

660 super().__init__(registry_schema_version=registry_schema_version) 

661 self.universe = universe 

662 

663 @classmethod 

664 @abstractmethod 

665 def initialize( 

666 cls, 

667 db: Database, 

668 context: StaticTablesContext, 

669 *, 

670 universe: DimensionUniverse, 

671 registry_schema_version: VersionTuple | None = None, 

672 ) -> DimensionRecordStorageManager: 

673 """Construct an instance of the manager. 

674 

675 Parameters 

676 ---------- 

677 db : `Database` 

678 Interface to the underlying database engine and namespace. 

679 context : `StaticTablesContext` 

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

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

682 implemented with this manager. 

683 universe : `DimensionUniverse` 

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

685 registry_schema_version : `VersionTuple` or `None` 

686 Schema version of this extension as defined in registry. 

687 

688 Returns 

689 ------- 

690 manager : `DimensionRecordStorageManager` 

691 An instance of a concrete `DimensionRecordStorageManager` subclass. 

692 """ 

693 raise NotImplementedError() 

694 

695 def __getitem__(self, element: DimensionElement | str) -> DimensionRecordStorage: 

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

697 `None` on failure. 

698 """ 

699 r = self.get(element) 

700 if r is None: 

701 raise LookupError(f"No dimension element '{element}' found in this registry layer.") 

702 return r 

703 

704 @abstractmethod 

705 def get(self, element: DimensionElement | str) -> DimensionRecordStorage | None: 

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

707 the given element, if one exists in this layer. 

708 

709 Parameters 

710 ---------- 

711 element : `DimensionElement` 

712 Element for which records should be returned. 

713 

714 Returns 

715 ------- 

716 records : `DimensionRecordStorage` or `None` 

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

718 layer, or `None` if there are no records for that element in this 

719 layer. 

720 

721 Notes 

722 ----- 

723 Dimension elements registered by another client of the same layer since 

724 the last call to `initialize` or `refresh` may not be found. 

725 """ 

726 raise NotImplementedError() 

727 

728 @abstractmethod 

729 def register(self, element: DimensionElement) -> DimensionRecordStorage: 

730 """Ensure that this layer can hold records for the given element, 

731 creating new tables as necessary. 

732 

733 Parameters 

734 ---------- 

735 element : `DimensionElement` 

736 Element for which a table should created (as necessary) and 

737 an associated `DimensionRecordStorage` returned. 

738 

739 Returns 

740 ------- 

741 records : `DimensionRecordStorage` 

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

743 layer. 

744 

745 Raises 

746 ------ 

747 TransactionInterruption 

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

749 context. 

750 """ 

751 raise NotImplementedError() 

752 

753 @abstractmethod 

754 def saveDimensionGraph(self, graph: DimensionGraph) -> int: 

755 """Save a `DimensionGraph` definition to the database, allowing it to 

756 be retrieved later via the returned key. 

757 

758 Parameters 

759 ---------- 

760 graph : `DimensionGraph` 

761 Set of dimensions to save. 

762 

763 Returns 

764 ------- 

765 key : `int` 

766 Integer used as the unique key for this `DimensionGraph` in the 

767 database. 

768 

769 Raises 

770 ------ 

771 TransactionInterruption 

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

773 context. 

774 """ 

775 raise NotImplementedError() 

776 

777 @abstractmethod 

778 def loadDimensionGraph(self, key: int) -> DimensionGraph: 

779 """Retrieve a `DimensionGraph` that was previously saved in the 

780 database. 

781 

782 Parameters 

783 ---------- 

784 key : `int` 

785 Integer used as the unique key for this `DimensionGraph` in the 

786 database. 

787 

788 Returns 

789 ------- 

790 graph : `DimensionGraph` 

791 Retrieved graph. 

792 

793 Raises 

794 ------ 

795 KeyError 

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

797 """ 

798 raise NotImplementedError() 

799 

800 @abstractmethod 

801 def clearCaches(self) -> None: 

802 """Clear any in-memory caches held by nested `DimensionRecordStorage` 

803 instances. 

804 

805 This is called by `Registry` when transactions are rolled back, to 

806 avoid in-memory caches from ever containing records that are not 

807 present in persistent storage. 

808 """ 

809 raise NotImplementedError() 

810 

811 @abstractmethod 

812 def make_spatial_join_relation( 

813 self, 

814 element1: str, 

815 element2: str, 

816 context: queries.SqlQueryContext, 

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

818 ) -> tuple[Relation, bool]: 

819 """Create a relation that represents the spatial join between two 

820 dimension elements. 

821 

822 Parameters 

823 ---------- 

824 element1 : `str` 

825 Name of one of the elements participating in the join. 

826 element2 : `str` 

827 Name of the other element participating in the join. 

828 context : `.queries.SqlQueryContext` 

829 Object that manages relation engines and database-side state 

830 (e.g. temporary tables) for the query. 

831 governor_constraints : `Mapping` [ `str`, `collections.abc.Set` ], \ 

832 optional 

833 Constraints imposed by other aspects of the query on governor 

834 dimensions. 

835 

836 Returns 

837 ------- 

838 relation : ``lsst.daf.relation.Relation` 

839 New relation that represents a spatial join between the two given 

840 elements. Guaranteed to have key columns for all required 

841 dimensions of both elements. 

842 needs_refinement : `bool` 

843 Whether the returned relation represents a conservative join that 

844 needs refinement via native-iteration predicate. 

845 """ 

846 raise NotImplementedError() 

847 

848 universe: DimensionUniverse 

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

850 `Registry` (`DimensionUniverse`). 

851 """