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

112 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-27 09:44 +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__ = ( 

30 "DatabaseDimensionOverlapStorage", 

31 "DatabaseDimensionRecordStorage", 

32 "DimensionRecordStorage", 

33 "DimensionRecordStorageManager", 

34 "GovernorDimensionRecordStorage", 

35 "SkyPixDimensionRecordStorage", 

36) 

37 

38from abc import ABC, abstractmethod 

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

40from typing import TYPE_CHECKING, Any 

41 

42import sqlalchemy 

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

44 

45from ..._column_type_info import ColumnTypeInfo, LogicalColumn 

46from ..._named import NamedKeyMapping 

47from ...dimensions import ( 

48 DatabaseDimensionElement, 

49 DataCoordinate, 

50 DimensionElement, 

51 DimensionGraph, 

52 DimensionRecord, 

53 DimensionUniverse, 

54 GovernorDimension, 

55 SkyPixDimension, 

56) 

57from ._versioning import VersionedExtension, VersionTuple 

58 

59if TYPE_CHECKING: 

60 from .. import queries 

61 from ._database import Database, StaticTablesContext 

62 

63 

64OverlapSide = SkyPixDimension | tuple[DatabaseDimensionElement, str] 

65 

66 

67class DimensionRecordStorage(ABC): 

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

69 associated with a single `DimensionElement`. 

70 

71 Concrete `DimensionRecordStorage` instances should generally be constructed 

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

73 subclass for each element according to its configuration. 

74 

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

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

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

78 potentially-defaultable implementations are extremely trivial, so asking 

79 subclasses to provide them is not a significant burden. 

80 """ 

81 

82 @property 

83 @abstractmethod 

84 def element(self) -> DimensionElement: 

85 """The element whose records this instance managers 

86 (`DimensionElement`). 

87 """ 

88 raise NotImplementedError() 

89 

90 @abstractmethod 

91 def clearCaches(self) -> None: 

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

93 

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

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

96 present in persistent storage. 

97 """ 

98 raise NotImplementedError() 

99 

100 @abstractmethod 

101 def join( 

102 self, 

103 target: Relation, 

104 join: Join, 

105 context: queries.SqlQueryContext, 

106 ) -> Relation: 

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

108 

109 Parameters 

110 ---------- 

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

112 Existing relation to join to. Implementations may require that 

113 this relation already include dimension key columns for this 

114 dimension element and assume that dataset or spatial join relations 

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

116 first. 

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

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

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

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

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

122 context : `.queries.SqlQueryContext` 

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

124 temporary tables) for the query. 

125 

126 Returns 

127 ------- 

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

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

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

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

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

133 """ 

134 raise NotImplementedError() 

135 

136 @abstractmethod 

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

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

139 

140 Parameters 

141 ---------- 

142 records 

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

144 element this storage is associated with. 

145 replace: `bool`, optional 

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

147 database if there is a conflict. 

148 skip_existing : `bool`, optional 

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

150 the same primary key values already exists. 

151 

152 Raises 

153 ------ 

154 TypeError 

155 Raised if the element does not support record insertion. 

156 sqlalchemy.exc.IntegrityError 

157 Raised if one or more records violate database integrity 

158 constraints. 

159 

160 Notes 

161 ----- 

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

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

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

165 `clearCaches` when rolling back transactions. 

166 """ 

167 raise NotImplementedError() 

168 

169 @abstractmethod 

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

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

172 not exist and comparing values if it does. 

173 

174 Parameters 

175 ---------- 

176 record : `DimensionRecord`. 

177 An instance of the `DimensionRecord` subclass for the 

178 element this storage is associated with. 

179 update: `bool`, optional 

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

181 database if there is a conflict. 

182 

183 Returns 

184 ------- 

185 inserted_or_updated : `bool` or `dict` 

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

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

188 values if an update was performed (only possible if 

189 ``update=True``). 

190 

191 Raises 

192 ------ 

193 DatabaseConflictError 

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

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

196 TypeError 

197 Raised if the element does not support record synchronization. 

198 sqlalchemy.exc.IntegrityError 

199 Raised if one or more records violate database integrity 

200 constraints. 

201 """ 

202 raise NotImplementedError() 

203 

204 @abstractmethod 

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

206 """Retrieve a single record from storage. 

207 

208 Parameters 

209 ---------- 

210 data_id : `DataCoordinate` 

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

212 be present. 

213 context : `.queries.SqlQueryContext` 

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

215 available. 

216 

217 Returns 

218 ------- 

219 record : `DimensionRecord` or `None` 

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

221 given data ID. 

222 """ 

223 raise NotImplementedError() 

224 

225 def get_record_cache( 

226 self, context: queries.SqlQueryContext 

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

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

229 element, fetching it if necessary. 

230 

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

232 

233 Parameters 

234 ---------- 

235 context : `.queries.SqlQueryContext` 

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

237 available. 

238 

239 Returns 

240 ------- 

241 cache : `~collections.abc.Mapping` \ 

242 [ `DataCoordinate`, `DimensionRecord` ] or `None` 

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

244 """ 

245 return None 

246 

247 @abstractmethod 

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

249 """Return tables used for schema digest. 

250 

251 Returns 

252 ------- 

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

254 Possibly empty list of tables for schema digest calculations. 

255 """ 

256 raise NotImplementedError() 

257 

258 def _build_sql_payload( 

259 self, 

260 from_clause: sqlalchemy.sql.FromClause, 

261 column_types: ColumnTypeInfo, 

262 ) -> sql.Payload[LogicalColumn]: 

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

264 

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

266 `make_relation` implementations. 

267 

268 Parameters 

269 ---------- 

270 from_clause : `sqlalchemy.sql.FromClause` 

271 SQLAlchemy table or subquery to select from. 

272 column_types : `ColumnTypeInfo` 

273 Struct with information about column types that depend on registry 

274 configuration. 

275 

276 Returns 

277 ------- 

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

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

280 columns, and optional WHERE clause. 

281 """ 

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

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

284 if field_name == "timespan": 

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

286 from_clause.columns, name=field_name 

287 ) 

288 else: 

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

290 return payload 

291 

292 

293class GovernorDimensionRecordStorage(DimensionRecordStorage): 

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

295 storage for `GovernorDimension` instances. 

296 """ 

297 

298 @classmethod 

299 @abstractmethod 

300 def initialize( 

301 cls, 

302 db: Database, 

303 dimension: GovernorDimension, 

304 *, 

305 context: StaticTablesContext | None = None, 

306 config: Mapping[str, Any], 

307 ) -> GovernorDimensionRecordStorage: 

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

309 

310 Parameters 

311 ---------- 

312 db : `Database` 

313 Interface to the underlying database engine and namespace. 

314 dimension : `GovernorDimension` 

315 Dimension the new instance will manage records for. 

316 context : `StaticTablesContext`, optional 

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

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

319 config : `~collections.abc.Mapping` 

320 Extra configuration options specific to the implementation. 

321 

322 Returns 

323 ------- 

324 storage : `GovernorDimensionRecordStorage` 

325 A new `GovernorDimensionRecordStorage` subclass instance. 

326 """ 

327 raise NotImplementedError() 

328 

329 @property 

330 @abstractmethod 

331 def element(self) -> GovernorDimension: 

332 # Docstring inherited from DimensionRecordStorage. 

333 raise NotImplementedError() 

334 

335 @property 

336 @abstractmethod 

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

338 """The SQLAlchemy table that backs this dimension 

339 (`sqlalchemy.schema.Table`). 

340 """ 

341 raise NotImplementedError() 

342 

343 def join( 

344 self, 

345 target: Relation, 

346 join: Join, 

347 context: queries.SqlQueryContext, 

348 ) -> Relation: 

349 # Docstring inherited. 

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

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

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

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

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

355 # SQL engine before the transfer to iteration. 

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

357 

358 @abstractmethod 

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

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

361 

362 This is used to provide an implementation for 

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

364 

365 Parameters 

366 ---------- 

367 context : `.queries.SqlQueryContext` 

368 Object that manages relation engines and database-side state 

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

370 

371 Returns 

372 ------- 

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

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

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

376 dimension key values exist in the registry. 

377 """ 

378 raise NotImplementedError() 

379 

380 @abstractmethod 

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

382 raise NotImplementedError() 

383 

384 @abstractmethod 

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

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

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

388 

389 Parameters 

390 ---------- 

391 callback 

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

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

394 transaction. 

395 """ 

396 raise NotImplementedError() 

397 

398 

399class SkyPixDimensionRecordStorage(DimensionRecordStorage): 

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

401 storage for `SkyPixDimension` instances. 

402 """ 

403 

404 @property 

405 @abstractmethod 

406 def element(self) -> SkyPixDimension: 

407 # Docstring inherited from DimensionRecordStorage. 

408 raise NotImplementedError() 

409 

410 

411class DatabaseDimensionRecordStorage(DimensionRecordStorage): 

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

413 storage for `DatabaseDimensionElement` instances. 

414 """ 

415 

416 @classmethod 

417 @abstractmethod 

418 def initialize( 

419 cls, 

420 db: Database, 

421 element: DatabaseDimensionElement, 

422 *, 

423 context: StaticTablesContext | None = None, 

424 config: Mapping[str, Any], 

425 governors: NamedKeyMapping[GovernorDimension, GovernorDimensionRecordStorage], 

426 view_target: DatabaseDimensionRecordStorage | None = None, 

427 ) -> DatabaseDimensionRecordStorage: 

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

429 

430 Parameters 

431 ---------- 

432 db : `Database` 

433 Interface to the underlying database engine and namespace. 

434 element : `DatabaseDimensionElement` 

435 Dimension element the new instance will manage records for. 

436 context : `StaticTablesContext`, optional 

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

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

439 config : `~collections.abc.Mapping` 

440 Extra configuration options specific to the implementation. 

441 governors : `NamedKeyMapping` 

442 Mapping containing all governor dimension storage implementations. 

443 view_target : `DatabaseDimensionRecordStorage`, optional 

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

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

446 

447 Returns 

448 ------- 

449 storage : `DatabaseDimensionRecordStorage` 

450 A new `DatabaseDimensionRecordStorage` subclass instance. 

451 """ 

452 raise NotImplementedError() 

453 

454 @property 

455 @abstractmethod 

456 def element(self) -> DatabaseDimensionElement: 

457 # Docstring inherited from DimensionRecordStorage. 

458 raise NotImplementedError() 

459 

460 def join( 

461 self, 

462 target: Relation, 

463 join: Join, 

464 context: queries.SqlQueryContext, 

465 ) -> Relation: 

466 # Docstring inherited. 

467 # See comment on similar code in GovernorDimensionRecordStorage.join 

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

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

470 

471 @abstractmethod 

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

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

474 

475 This is used to provide an implementation for 

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

477 

478 Parameters 

479 ---------- 

480 context : `.queries.SqlQueryContext` 

481 Object that manages relation engines and database-side state 

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

483 

484 Returns 

485 ------- 

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

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

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

489 dimension key values exist in the registry. 

490 """ 

491 raise NotImplementedError() 

492 

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

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

495 the overlaps between this element and another element. 

496 

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

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

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

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

501 for the element are inserted. 

502 

503 Parameters 

504 ---------- 

505 overlaps : `DatabaseDimensionRecordStorage` 

506 Object managing overlaps between this element and another 

507 database-backed element. 

508 """ 

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

510 

511 def make_spatial_join_relation( 

512 self, 

513 other: DimensionElement, 

514 context: queries.SqlQueryContext, 

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

516 ) -> Relation | None: 

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

518 overlap join between two dimension elements. 

519 

520 High-level code should generally call 

521 `DimensionRecordStorageManager.make_spatial_join_relation` (which 

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

523 

524 Parameters 

525 ---------- 

526 other : `DimensionElement` 

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

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

529 be a `DatabaseDimensionElement` or a `SkyPixDimension`. 

530 context : `.queries.SqlQueryContext` 

531 Object that manages relation engines and database-side state 

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

533 governor_constraints : `~collections.abc.Mapping` \ 

534 [ `str`, `~collections.abc.Set` ], optional 

535 Constraints imposed by other aspects of the query on governor 

536 dimensions. 

537 

538 Returns 

539 ------- 

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

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

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

543 working out alternative approaches involving multiple joins. 

544 """ 

545 return None 

546 

547 

548class DatabaseDimensionOverlapStorage(ABC): 

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

550 database-backed dimensions. 

551 """ 

552 

553 @classmethod 

554 @abstractmethod 

555 def initialize( 

556 cls, 

557 db: Database, 

558 elementStorage: tuple[DatabaseDimensionRecordStorage, DatabaseDimensionRecordStorage], 

559 governorStorage: tuple[GovernorDimensionRecordStorage, GovernorDimensionRecordStorage], 

560 context: StaticTablesContext | None = None, 

561 ) -> DatabaseDimensionOverlapStorage: 

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

563 

564 Parameters 

565 ---------- 

566 db : `Database` 

567 Interface to the underlying database engine and namespace. 

568 elementStorage : `tuple` [ `DatabaseDimensionRecordStorage` ] 

569 Storage objects for the elements this object will related. 

570 governorStorage : `tuple` [ `GovernorDimensionRecordStorage` ] 

571 Storage objects for the governor dimensions of the elements this 

572 object will related. 

573 context : `StaticTablesContext`, optional 

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

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

576 

577 Returns 

578 ------- 

579 storage : `DatabaseDimensionOverlapStorage` 

580 A new `DatabaseDimensionOverlapStorage` subclass instance. 

581 """ 

582 raise NotImplementedError() 

583 

584 @property 

585 @abstractmethod 

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

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

588 

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

590 `DimensionUniverse`. 

591 """ 

592 raise NotImplementedError() 

593 

594 @abstractmethod 

595 def clearCaches(self) -> None: 

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

597 materialized. 

598 """ 

599 raise NotImplementedError() 

600 

601 @abstractmethod 

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

603 """Return tables used for schema digest. 

604 

605 Returns 

606 ------- 

607 tables : `~collections.abc.Iterable` [ `sqlalchemy.schema.Table` ] 

608 Possibly empty set of tables for schema digest calculations. 

609 """ 

610 raise NotImplementedError() 

611 

612 @abstractmethod 

613 def make_relation( 

614 self, 

615 context: queries.SqlQueryContext, 

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

617 ) -> Relation | None: 

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

619 table. 

620 

621 High-level code should generally call 

622 `DimensionRecordStorageManager.make_spatial_join_relation` (which 

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

624 

625 Parameters 

626 ---------- 

627 context : `.queries.SqlQueryContext` 

628 Object that manages relation engines and database-side state 

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

630 governor_constraints : `~collections.abc.Mapping` \ 

631 [ `str`, `~collections.abc.Set` ], optional 

632 Constraints imposed by other aspects of the query on governor 

633 dimensions; collections inconsistent with these constraints will be 

634 skipped. 

635 

636 Returns 

637 ------- 

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

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

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

641 working out alternative approaches involving multiple joins. 

642 """ 

643 raise NotImplementedError() 

644 

645 

646class DimensionRecordStorageManager(VersionedExtension): 

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

648 

649 `DimensionRecordStorageManager` primarily serves as a container and factory 

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

651 records for a different `DimensionElement`. 

652 

653 Parameters 

654 ---------- 

655 universe : `DimensionUniverse` 

656 Universe of all dimensions and dimension elements known to the 

657 `Registry`. 

658 

659 Notes 

660 ----- 

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

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

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

664 """ 

665 

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

667 super().__init__(registry_schema_version=registry_schema_version) 

668 self.universe = universe 

669 

670 @classmethod 

671 @abstractmethod 

672 def initialize( 

673 cls, 

674 db: Database, 

675 context: StaticTablesContext, 

676 *, 

677 universe: DimensionUniverse, 

678 registry_schema_version: VersionTuple | None = None, 

679 ) -> DimensionRecordStorageManager: 

680 """Construct an instance of the manager. 

681 

682 Parameters 

683 ---------- 

684 db : `Database` 

685 Interface to the underlying database engine and namespace. 

686 context : `StaticTablesContext` 

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

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

689 implemented with this manager. 

690 universe : `DimensionUniverse` 

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

692 registry_schema_version : `VersionTuple` or `None` 

693 Schema version of this extension as defined in registry. 

694 

695 Returns 

696 ------- 

697 manager : `DimensionRecordStorageManager` 

698 An instance of a concrete `DimensionRecordStorageManager` subclass. 

699 """ 

700 raise NotImplementedError() 

701 

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

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

704 `None` on failure. 

705 """ 

706 r = self.get(element) 

707 if r is None: 

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

709 return r 

710 

711 @abstractmethod 

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

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

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

715 

716 Parameters 

717 ---------- 

718 element : `DimensionElement` 

719 Element for which records should be returned. 

720 

721 Returns 

722 ------- 

723 records : `DimensionRecordStorage` or `None` 

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

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

726 layer. 

727 

728 Notes 

729 ----- 

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

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

732 """ 

733 raise NotImplementedError() 

734 

735 @abstractmethod 

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

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

738 creating new tables as necessary. 

739 

740 Parameters 

741 ---------- 

742 element : `DimensionElement` 

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

744 an associated `DimensionRecordStorage` returned. 

745 

746 Returns 

747 ------- 

748 records : `DimensionRecordStorage` 

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

750 layer. 

751 

752 Raises 

753 ------ 

754 TransactionInterruption 

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

756 context. 

757 """ 

758 raise NotImplementedError() 

759 

760 @abstractmethod 

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

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

763 be retrieved later via the returned key. 

764 

765 Parameters 

766 ---------- 

767 graph : `DimensionGraph` 

768 Set of dimensions to save. 

769 

770 Returns 

771 ------- 

772 key : `int` 

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

774 database. 

775 

776 Raises 

777 ------ 

778 TransactionInterruption 

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

780 context. 

781 """ 

782 raise NotImplementedError() 

783 

784 @abstractmethod 

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

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

787 database. 

788 

789 Parameters 

790 ---------- 

791 key : `int` 

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

793 database. 

794 

795 Returns 

796 ------- 

797 graph : `DimensionGraph` 

798 Retrieved graph. 

799 

800 Raises 

801 ------ 

802 KeyError 

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

804 """ 

805 raise NotImplementedError() 

806 

807 @abstractmethod 

808 def clearCaches(self) -> None: 

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

810 instances. 

811 

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

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

814 present in persistent storage. 

815 """ 

816 raise NotImplementedError() 

817 

818 @abstractmethod 

819 def make_spatial_join_relation( 

820 self, 

821 element1: str, 

822 element2: str, 

823 context: queries.SqlQueryContext, 

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

825 existing_relationships: Set[frozenset[str]] = frozenset(), 

826 ) -> tuple[Relation, bool]: 

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

828 dimension elements. 

829 

830 Parameters 

831 ---------- 

832 element1 : `str` 

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

834 element2 : `str` 

835 Name of the other element participating in the join. 

836 context : `.queries.SqlQueryContext` 

837 Object that manages relation engines and database-side state 

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

839 governor_constraints : `~collections.abc.Mapping` \ 

840 [ `str`, `collections.abc.Set` ], optional 

841 Constraints imposed by other aspects of the query on governor 

842 dimensions. 

843 existing_relationships : `~collections.abc.Set` [ `frozenset` [ `str` \ 

844 ] ], optional 

845 Relationships between dimensions that are already present in the 

846 relation the result will be joined to. Spatial join relations 

847 that duplicate these relationships will not be included in the 

848 result, which may cause an identity relation to be returned if 

849 a spatial relationship has already been established. 

850 

851 Returns 

852 ------- 

853 relation : `lsst.daf.relation.Relation` 

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

855 elements. Guaranteed to have key columns for all required 

856 dimensions of both elements. 

857 needs_refinement : `bool` 

858 Whether the returned relation represents a conservative join that 

859 needs refinement via native-iteration predicate. 

860 """ 

861 raise NotImplementedError() 

862 

863 universe: DimensionUniverse 

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

865 `Registry` (`DimensionUniverse`). 

866 """