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

111 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-10-02 08:00 +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 ...core import ( 

46 ColumnTypeInfo, 

47 DatabaseDimensionElement, 

48 DataCoordinate, 

49 DimensionElement, 

50 DimensionGraph, 

51 DimensionRecord, 

52 DimensionUniverse, 

53 GovernorDimension, 

54 LogicalColumn, 

55 SkyPixDimension, 

56) 

57from ...core.named import NamedKeyMapping 

58from ._versioning import VersionedExtension, VersionTuple 

59 

60if TYPE_CHECKING: 

61 from .. import queries 

62 from ._database import Database, StaticTablesContext 

63 

64 

65OverlapSide = SkyPixDimension | tuple[DatabaseDimensionElement, str] 

66 

67 

68class DimensionRecordStorage(ABC): 

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

70 associated with a single `DimensionElement`. 

71 

72 Concrete `DimensionRecordStorage` instances should generally be constructed 

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

74 subclass for each element according to its configuration. 

75 

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

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

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

79 potentially-defaultable implementations are extremely trivial, so asking 

80 subclasses to provide them is not a significant burden. 

81 """ 

82 

83 @property 

84 @abstractmethod 

85 def element(self) -> DimensionElement: 

86 """The element whose records this instance managers 

87 (`DimensionElement`). 

88 """ 

89 raise NotImplementedError() 

90 

91 @abstractmethod 

92 def clearCaches(self) -> None: 

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

94 

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

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

97 present in persistent storage. 

98 """ 

99 raise NotImplementedError() 

100 

101 @abstractmethod 

102 def join( 

103 self, 

104 target: Relation, 

105 join: Join, 

106 context: queries.SqlQueryContext, 

107 ) -> Relation: 

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

109 

110 Parameters 

111 ---------- 

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

113 Existing relation to join to. Implementations may require that 

114 this relation already include dimension key columns for this 

115 dimension element and assume that dataset or spatial join relations 

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

117 first. 

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

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

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

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

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

123 context : `.queries.SqlQueryContext` 

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

125 temporary tables) for the query. 

126 

127 Returns 

128 ------- 

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

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

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

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

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

134 """ 

135 raise NotImplementedError() 

136 

137 @abstractmethod 

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

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

140 

141 Parameters 

142 ---------- 

143 records 

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

145 element this storage is associated with. 

146 replace: `bool`, optional 

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

148 database if there is a conflict. 

149 skip_existing : `bool`, optional 

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

151 the same primary key values already exists. 

152 

153 Raises 

154 ------ 

155 TypeError 

156 Raised if the element does not support record insertion. 

157 sqlalchemy.exc.IntegrityError 

158 Raised if one or more records violate database integrity 

159 constraints. 

160 

161 Notes 

162 ----- 

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

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

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

166 `clearCaches` when rolling back transactions. 

167 """ 

168 raise NotImplementedError() 

169 

170 @abstractmethod 

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

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

173 not exist and comparing values if it does. 

174 

175 Parameters 

176 ---------- 

177 record : `DimensionRecord`. 

178 An instance of the `DimensionRecord` subclass for the 

179 element this storage is associated with. 

180 update: `bool`, optional 

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

182 database if there is a conflict. 

183 

184 Returns 

185 ------- 

186 inserted_or_updated : `bool` or `dict` 

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

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

189 values if an update was performed (only possible if 

190 ``update=True``). 

191 

192 Raises 

193 ------ 

194 DatabaseConflictError 

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

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

197 TypeError 

198 Raised if the element does not support record synchronization. 

199 sqlalchemy.exc.IntegrityError 

200 Raised if one or more records violate database integrity 

201 constraints. 

202 """ 

203 raise NotImplementedError() 

204 

205 @abstractmethod 

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

207 """Retrieve a single record from storage. 

208 

209 Parameters 

210 ---------- 

211 data_id : `DataCoordinate` 

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

213 be present. 

214 context : `.queries.SqlQueryContext` 

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

216 available. 

217 

218 Returns 

219 ------- 

220 record : `DimensionRecord` or `None` 

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

222 given data ID. 

223 """ 

224 raise NotImplementedError() 

225 

226 def get_record_cache( 

227 self, context: queries.SqlQueryContext 

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

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

230 element, fetching it if necessary. 

231 

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

233 

234 Parameters 

235 ---------- 

236 context : `.queries.SqlQueryContext` 

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

238 available. 

239 

240 Returns 

241 ------- 

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

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

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

245 """ 

246 return None 

247 

248 @abstractmethod 

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

250 """Return tables used for schema digest. 

251 

252 Returns 

253 ------- 

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

255 Possibly empty list of tables for schema digest calculations. 

256 """ 

257 raise NotImplementedError() 

258 

259 def _build_sql_payload( 

260 self, 

261 from_clause: sqlalchemy.sql.FromClause, 

262 column_types: ColumnTypeInfo, 

263 ) -> sql.Payload[LogicalColumn]: 

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

265 

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

267 `make_relation` implementations. 

268 

269 Parameters 

270 ---------- 

271 from_clause : `sqlalchemy.sql.FromClause` 

272 SQLAlchemy table or subquery to select from. 

273 column_types : `ColumnTypeInfo` 

274 Struct with information about column types that depend on registry 

275 configuration. 

276 

277 Returns 

278 ------- 

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

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

281 columns, and optional WHERE clause. 

282 """ 

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

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

285 if field_name == "timespan": 

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

287 from_clause.columns, name=field_name 

288 ) 

289 else: 

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

291 return payload 

292 

293 

294class GovernorDimensionRecordStorage(DimensionRecordStorage): 

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

296 storage for `GovernorDimension` instances. 

297 """ 

298 

299 @classmethod 

300 @abstractmethod 

301 def initialize( 

302 cls, 

303 db: Database, 

304 dimension: GovernorDimension, 

305 *, 

306 context: StaticTablesContext | None = None, 

307 config: Mapping[str, Any], 

308 ) -> GovernorDimensionRecordStorage: 

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

310 

311 Parameters 

312 ---------- 

313 db : `Database` 

314 Interface to the underlying database engine and namespace. 

315 dimension : `GovernorDimension` 

316 Dimension the new instance will manage records for. 

317 context : `StaticTablesContext`, optional 

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

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

320 config : `~collections.abc.Mapping` 

321 Extra configuration options specific to the implementation. 

322 

323 Returns 

324 ------- 

325 storage : `GovernorDimensionRecordStorage` 

326 A new `GovernorDimensionRecordStorage` subclass instance. 

327 """ 

328 raise NotImplementedError() 

329 

330 @property 

331 @abstractmethod 

332 def element(self) -> GovernorDimension: 

333 # Docstring inherited from DimensionRecordStorage. 

334 raise NotImplementedError() 

335 

336 @property 

337 @abstractmethod 

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

339 """The SQLAlchemy table that backs this dimension 

340 (`sqlalchemy.schema.Table`). 

341 """ 

342 raise NotImplementedError() 

343 

344 def join( 

345 self, 

346 target: Relation, 

347 join: Join, 

348 context: queries.SqlQueryContext, 

349 ) -> Relation: 

350 # Docstring inherited. 

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

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

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

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

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

356 # SQL engine before the transfer to iteration. 

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

358 

359 @abstractmethod 

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

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

362 

363 This is used to provide an implementation for 

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

365 

366 Parameters 

367 ---------- 

368 context : `.queries.SqlQueryContext` 

369 Object that manages relation engines and database-side state 

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

371 

372 Returns 

373 ------- 

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

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

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

377 dimension key values exist in the registry. 

378 """ 

379 raise NotImplementedError() 

380 

381 @abstractmethod 

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

383 raise NotImplementedError() 

384 

385 @abstractmethod 

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

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

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

389 

390 Parameters 

391 ---------- 

392 callback 

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

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

395 transaction. 

396 """ 

397 raise NotImplementedError() 

398 

399 

400class SkyPixDimensionRecordStorage(DimensionRecordStorage): 

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

402 storage for `SkyPixDimension` instances. 

403 """ 

404 

405 @property 

406 @abstractmethod 

407 def element(self) -> SkyPixDimension: 

408 # Docstring inherited from DimensionRecordStorage. 

409 raise NotImplementedError() 

410 

411 

412class DatabaseDimensionRecordStorage(DimensionRecordStorage): 

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

414 storage for `DatabaseDimensionElement` instances. 

415 """ 

416 

417 @classmethod 

418 @abstractmethod 

419 def initialize( 

420 cls, 

421 db: Database, 

422 element: DatabaseDimensionElement, 

423 *, 

424 context: StaticTablesContext | None = None, 

425 config: Mapping[str, Any], 

426 governors: NamedKeyMapping[GovernorDimension, GovernorDimensionRecordStorage], 

427 view_target: DatabaseDimensionRecordStorage | None = None, 

428 ) -> DatabaseDimensionRecordStorage: 

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

430 

431 Parameters 

432 ---------- 

433 db : `Database` 

434 Interface to the underlying database engine and namespace. 

435 element : `DatabaseDimensionElement` 

436 Dimension element the new instance will manage records for. 

437 context : `StaticTablesContext`, optional 

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

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

440 config : `~collections.abc.Mapping` 

441 Extra configuration options specific to the implementation. 

442 governors : `NamedKeyMapping` 

443 Mapping containing all governor dimension storage implementations. 

444 view_target : `DatabaseDimensionRecordStorage`, optional 

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

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

447 

448 Returns 

449 ------- 

450 storage : `DatabaseDimensionRecordStorage` 

451 A new `DatabaseDimensionRecordStorage` subclass instance. 

452 """ 

453 raise NotImplementedError() 

454 

455 @property 

456 @abstractmethod 

457 def element(self) -> DatabaseDimensionElement: 

458 # Docstring inherited from DimensionRecordStorage. 

459 raise NotImplementedError() 

460 

461 def join( 

462 self, 

463 target: Relation, 

464 join: Join, 

465 context: queries.SqlQueryContext, 

466 ) -> Relation: 

467 # Docstring inherited. 

468 # See comment on similar code in GovernorDimensionRecordStorage.join 

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

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

471 

472 @abstractmethod 

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

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

475 

476 This is used to provide an implementation for 

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

478 

479 Parameters 

480 ---------- 

481 context : `.queries.SqlQueryContext` 

482 Object that manages relation engines and database-side state 

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

484 

485 Returns 

486 ------- 

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

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

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

490 dimension key values exist in the registry. 

491 """ 

492 raise NotImplementedError() 

493 

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

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

496 the overlaps between this element and another element. 

497 

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

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

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

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

502 for the element are inserted. 

503 

504 Parameters 

505 ---------- 

506 overlaps : `DatabaseDimensionRecordStorage` 

507 Object managing overlaps between this element and another 

508 database-backed element. 

509 """ 

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

511 

512 def make_spatial_join_relation( 

513 self, 

514 other: DimensionElement, 

515 context: queries.SqlQueryContext, 

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

517 ) -> Relation | None: 

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

519 overlap join between two dimension elements. 

520 

521 High-level code should generally call 

522 `DimensionRecordStorageManager.make_spatial_join_relation` (which 

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

524 

525 Parameters 

526 ---------- 

527 other : `DimensionElement` 

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

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

530 be a `DatabaseDimensionElement` or a `SkyPixDimension`. 

531 context : `.queries.SqlQueryContext` 

532 Object that manages relation engines and database-side state 

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

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

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

536 Constraints imposed by other aspects of the query on governor 

537 dimensions. 

538 

539 Returns 

540 ------- 

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

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

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

544 working out alternative approaches involving multiple joins. 

545 """ 

546 return None 

547 

548 

549class DatabaseDimensionOverlapStorage(ABC): 

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

551 database-backed dimensions. 

552 """ 

553 

554 @classmethod 

555 @abstractmethod 

556 def initialize( 

557 cls, 

558 db: Database, 

559 elementStorage: tuple[DatabaseDimensionRecordStorage, DatabaseDimensionRecordStorage], 

560 governorStorage: tuple[GovernorDimensionRecordStorage, GovernorDimensionRecordStorage], 

561 context: StaticTablesContext | None = None, 

562 ) -> DatabaseDimensionOverlapStorage: 

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

564 

565 Parameters 

566 ---------- 

567 db : `Database` 

568 Interface to the underlying database engine and namespace. 

569 elementStorage : `tuple` [ `DatabaseDimensionRecordStorage` ] 

570 Storage objects for the elements this object will related. 

571 governorStorage : `tuple` [ `GovernorDimensionRecordStorage` ] 

572 Storage objects for the governor dimensions of the elements this 

573 object will related. 

574 context : `StaticTablesContext`, optional 

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

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

577 

578 Returns 

579 ------- 

580 storage : `DatabaseDimensionOverlapStorage` 

581 A new `DatabaseDimensionOverlapStorage` subclass instance. 

582 """ 

583 raise NotImplementedError() 

584 

585 @property 

586 @abstractmethod 

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

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

589 

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

591 `DimensionUniverse`. 

592 """ 

593 raise NotImplementedError() 

594 

595 @abstractmethod 

596 def clearCaches(self) -> None: 

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

598 materialized. 

599 """ 

600 raise NotImplementedError() 

601 

602 @abstractmethod 

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

604 """Return tables used for schema digest. 

605 

606 Returns 

607 ------- 

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

609 Possibly empty set of tables for schema digest calculations. 

610 """ 

611 raise NotImplementedError() 

612 

613 @abstractmethod 

614 def make_relation( 

615 self, 

616 context: queries.SqlQueryContext, 

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

618 ) -> Relation | None: 

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

620 table. 

621 

622 High-level code should generally call 

623 `DimensionRecordStorageManager.make_spatial_join_relation` (which 

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

625 

626 Parameters 

627 ---------- 

628 context : `.queries.SqlQueryContext` 

629 Object that manages relation engines and database-side state 

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

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

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

633 Constraints imposed by other aspects of the query on governor 

634 dimensions; collections inconsistent with these constraints will be 

635 skipped. 

636 

637 Returns 

638 ------- 

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

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

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

642 working out alternative approaches involving multiple joins. 

643 """ 

644 raise NotImplementedError() 

645 

646 

647class DimensionRecordStorageManager(VersionedExtension): 

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

649 

650 `DimensionRecordStorageManager` primarily serves as a container and factory 

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

652 records for a different `DimensionElement`. 

653 

654 Parameters 

655 ---------- 

656 universe : `DimensionUniverse` 

657 Universe of all dimensions and dimension elements known to the 

658 `Registry`. 

659 

660 Notes 

661 ----- 

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

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

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

665 """ 

666 

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

668 super().__init__(registry_schema_version=registry_schema_version) 

669 self.universe = universe 

670 

671 @classmethod 

672 @abstractmethod 

673 def initialize( 

674 cls, 

675 db: Database, 

676 context: StaticTablesContext, 

677 *, 

678 universe: DimensionUniverse, 

679 registry_schema_version: VersionTuple | None = None, 

680 ) -> DimensionRecordStorageManager: 

681 """Construct an instance of the manager. 

682 

683 Parameters 

684 ---------- 

685 db : `Database` 

686 Interface to the underlying database engine and namespace. 

687 context : `StaticTablesContext` 

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

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

690 implemented with this manager. 

691 universe : `DimensionUniverse` 

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

693 registry_schema_version : `VersionTuple` or `None` 

694 Schema version of this extension as defined in registry. 

695 

696 Returns 

697 ------- 

698 manager : `DimensionRecordStorageManager` 

699 An instance of a concrete `DimensionRecordStorageManager` subclass. 

700 """ 

701 raise NotImplementedError() 

702 

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

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

705 `None` on failure. 

706 """ 

707 r = self.get(element) 

708 if r is None: 

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

710 return r 

711 

712 @abstractmethod 

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

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

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

716 

717 Parameters 

718 ---------- 

719 element : `DimensionElement` 

720 Element for which records should be returned. 

721 

722 Returns 

723 ------- 

724 records : `DimensionRecordStorage` or `None` 

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

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

727 layer. 

728 

729 Notes 

730 ----- 

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

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

733 """ 

734 raise NotImplementedError() 

735 

736 @abstractmethod 

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

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

739 creating new tables as necessary. 

740 

741 Parameters 

742 ---------- 

743 element : `DimensionElement` 

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

745 an associated `DimensionRecordStorage` returned. 

746 

747 Returns 

748 ------- 

749 records : `DimensionRecordStorage` 

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

751 layer. 

752 

753 Raises 

754 ------ 

755 TransactionInterruption 

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

757 context. 

758 """ 

759 raise NotImplementedError() 

760 

761 @abstractmethod 

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

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

764 be retrieved later via the returned key. 

765 

766 Parameters 

767 ---------- 

768 graph : `DimensionGraph` 

769 Set of dimensions to save. 

770 

771 Returns 

772 ------- 

773 key : `int` 

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

775 database. 

776 

777 Raises 

778 ------ 

779 TransactionInterruption 

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

781 context. 

782 """ 

783 raise NotImplementedError() 

784 

785 @abstractmethod 

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

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

788 database. 

789 

790 Parameters 

791 ---------- 

792 key : `int` 

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

794 database. 

795 

796 Returns 

797 ------- 

798 graph : `DimensionGraph` 

799 Retrieved graph. 

800 

801 Raises 

802 ------ 

803 KeyError 

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

805 """ 

806 raise NotImplementedError() 

807 

808 @abstractmethod 

809 def clearCaches(self) -> None: 

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

811 instances. 

812 

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

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

815 present in persistent storage. 

816 """ 

817 raise NotImplementedError() 

818 

819 @abstractmethod 

820 def make_spatial_join_relation( 

821 self, 

822 element1: str, 

823 element2: str, 

824 context: queries.SqlQueryContext, 

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

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

827 ) -> tuple[Relation, bool]: 

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

829 dimension elements. 

830 

831 Parameters 

832 ---------- 

833 element1 : `str` 

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

835 element2 : `str` 

836 Name of the other element participating in the join. 

837 context : `.queries.SqlQueryContext` 

838 Object that manages relation engines and database-side state 

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

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

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

842 Constraints imposed by other aspects of the query on governor 

843 dimensions. 

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

845 ] ], optional 

846 Relationships between dimensions that are already present in the 

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

848 that duplicate these relationships will not be included in the 

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

850 a spatial relationship has already been established. 

851 

852 Returns 

853 ------- 

854 relation : `lsst.daf.relation.Relation` 

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

856 elements. Guaranteed to have key columns for all required 

857 dimensions of both elements. 

858 needs_refinement : `bool` 

859 Whether the returned relation represents a conservative join that 

860 needs refinement via native-iteration predicate. 

861 """ 

862 raise NotImplementedError() 

863 

864 universe: DimensionUniverse 

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

866 `Registry` (`DimensionUniverse`). 

867 """