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

111 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-25 15:14 +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 

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 = 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 : `~collections.abc.Mapping` \ 

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

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

239 """ 

240 return None 

241 

242 @abstractmethod 

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

244 """Return tables used for schema digest. 

245 

246 Returns 

247 ------- 

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

249 Possibly empty list of tables for schema digest calculations. 

250 """ 

251 raise NotImplementedError() 

252 

253 def _build_sql_payload( 

254 self, 

255 from_clause: sqlalchemy.sql.FromClause, 

256 column_types: ColumnTypeInfo, 

257 ) -> sql.Payload[LogicalColumn]: 

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

259 

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

261 `make_relation` implementations. 

262 

263 Parameters 

264 ---------- 

265 from_clause : `sqlalchemy.sql.FromClause` 

266 SQLAlchemy table or subquery to select from. 

267 column_types : `ColumnTypeInfo` 

268 Struct with information about column types that depend on registry 

269 configuration. 

270 

271 Returns 

272 ------- 

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

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

275 columns, and optional WHERE clause. 

276 """ 

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

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

279 if field_name == "timespan": 

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

281 from_clause.columns, name=field_name 

282 ) 

283 else: 

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

285 return payload 

286 

287 

288class GovernorDimensionRecordStorage(DimensionRecordStorage): 

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

290 storage for `GovernorDimension` instances. 

291 """ 

292 

293 @classmethod 

294 @abstractmethod 

295 def initialize( 

296 cls, 

297 db: Database, 

298 dimension: GovernorDimension, 

299 *, 

300 context: StaticTablesContext | None = None, 

301 config: Mapping[str, Any], 

302 ) -> GovernorDimensionRecordStorage: 

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

304 

305 Parameters 

306 ---------- 

307 db : `Database` 

308 Interface to the underlying database engine and namespace. 

309 dimension : `GovernorDimension` 

310 Dimension the new instance will manage records for. 

311 context : `StaticTablesContext`, optional 

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

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

314 config : `~collections.abc.Mapping` 

315 Extra configuration options specific to the implementation. 

316 

317 Returns 

318 ------- 

319 storage : `GovernorDimensionRecordStorage` 

320 A new `GovernorDimensionRecordStorage` subclass instance. 

321 """ 

322 raise NotImplementedError() 

323 

324 @property 

325 @abstractmethod 

326 def element(self) -> GovernorDimension: 

327 # Docstring inherited from DimensionRecordStorage. 

328 raise NotImplementedError() 

329 

330 @property 

331 @abstractmethod 

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

333 """The SQLAlchemy table that backs this dimension 

334 (`sqlalchemy.schema.Table`). 

335 """ 

336 raise NotImplementedError() 

337 

338 def join( 

339 self, 

340 target: Relation, 

341 join: Join, 

342 context: queries.SqlQueryContext, 

343 ) -> Relation: 

344 # Docstring inherited. 

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

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

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

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

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

350 # SQL engine before the transfer to iteration. 

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

352 

353 @abstractmethod 

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

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

356 

357 This is used to provide an implementation for 

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

359 

360 Parameters 

361 ---------- 

362 context : `.queries.SqlQueryContext` 

363 Object that manages relation engines and database-side state 

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

365 

366 Returns 

367 ------- 

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

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

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

371 dimension key values exist in the registry. 

372 """ 

373 raise NotImplementedError() 

374 

375 @abstractmethod 

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

377 raise NotImplementedError() 

378 

379 @abstractmethod 

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

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

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

383 

384 Parameters 

385 ---------- 

386 callback 

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

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

389 transaction. 

390 """ 

391 raise NotImplementedError() 

392 

393 

394class SkyPixDimensionRecordStorage(DimensionRecordStorage): 

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

396 storage for `SkyPixDimension` instances. 

397 """ 

398 

399 @property 

400 @abstractmethod 

401 def element(self) -> SkyPixDimension: 

402 # Docstring inherited from DimensionRecordStorage. 

403 raise NotImplementedError() 

404 

405 

406class DatabaseDimensionRecordStorage(DimensionRecordStorage): 

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

408 storage for `DatabaseDimensionElement` instances. 

409 """ 

410 

411 @classmethod 

412 @abstractmethod 

413 def initialize( 

414 cls, 

415 db: Database, 

416 element: DatabaseDimensionElement, 

417 *, 

418 context: StaticTablesContext | None = None, 

419 config: Mapping[str, Any], 

420 governors: NamedKeyMapping[GovernorDimension, GovernorDimensionRecordStorage], 

421 view_target: DatabaseDimensionRecordStorage | None = None, 

422 ) -> DatabaseDimensionRecordStorage: 

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

424 

425 Parameters 

426 ---------- 

427 db : `Database` 

428 Interface to the underlying database engine and namespace. 

429 element : `DatabaseDimensionElement` 

430 Dimension element the new instance will manage records for. 

431 context : `StaticTablesContext`, optional 

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

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

434 config : `~collections.abc.Mapping` 

435 Extra configuration options specific to the implementation. 

436 governors : `NamedKeyMapping` 

437 Mapping containing all governor dimension storage implementations. 

438 view_target : `DatabaseDimensionRecordStorage`, optional 

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

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

441 

442 Returns 

443 ------- 

444 storage : `DatabaseDimensionRecordStorage` 

445 A new `DatabaseDimensionRecordStorage` subclass instance. 

446 """ 

447 raise NotImplementedError() 

448 

449 @property 

450 @abstractmethod 

451 def element(self) -> DatabaseDimensionElement: 

452 # Docstring inherited from DimensionRecordStorage. 

453 raise NotImplementedError() 

454 

455 def join( 

456 self, 

457 target: Relation, 

458 join: Join, 

459 context: queries.SqlQueryContext, 

460 ) -> Relation: 

461 # Docstring inherited. 

462 # See comment on similar code in GovernorDimensionRecordStorage.join 

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

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

465 

466 @abstractmethod 

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

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

469 

470 This is used to provide an implementation for 

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

472 

473 Parameters 

474 ---------- 

475 context : `.queries.SqlQueryContext` 

476 Object that manages relation engines and database-side state 

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

478 

479 Returns 

480 ------- 

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

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

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

484 dimension key values exist in the registry. 

485 """ 

486 raise NotImplementedError() 

487 

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

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

490 the overlaps between this element and another element. 

491 

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

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

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

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

496 for the element are inserted. 

497 

498 Parameters 

499 ---------- 

500 overlaps : `DatabaseDimensionRecordStorage` 

501 Object managing overlaps between this element and another 

502 database-backed element. 

503 """ 

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

505 

506 def make_spatial_join_relation( 

507 self, 

508 other: DimensionElement, 

509 context: queries.SqlQueryContext, 

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

511 ) -> Relation | None: 

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

513 overlap join between two dimension elements. 

514 

515 High-level code should generally call 

516 `DimensionRecordStorageManager.make_spatial_join_relation` (which 

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

518 

519 Parameters 

520 ---------- 

521 other : `DimensionElement` 

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

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

524 be a `DatabaseDimensionElement` or a `SkyPixDimension`. 

525 context : `.queries.SqlQueryContext` 

526 Object that manages relation engines and database-side state 

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

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

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

530 Constraints imposed by other aspects of the query on governor 

531 dimensions. 

532 

533 Returns 

534 ------- 

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

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

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

538 working out alternative approaches involving multiple joins. 

539 """ 

540 return None 

541 

542 

543class DatabaseDimensionOverlapStorage(ABC): 

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

545 database-backed dimensions. 

546 """ 

547 

548 @classmethod 

549 @abstractmethod 

550 def initialize( 

551 cls, 

552 db: Database, 

553 elementStorage: tuple[DatabaseDimensionRecordStorage, DatabaseDimensionRecordStorage], 

554 governorStorage: tuple[GovernorDimensionRecordStorage, GovernorDimensionRecordStorage], 

555 context: StaticTablesContext | None = None, 

556 ) -> DatabaseDimensionOverlapStorage: 

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

558 

559 Parameters 

560 ---------- 

561 db : `Database` 

562 Interface to the underlying database engine and namespace. 

563 elementStorage : `tuple` [ `DatabaseDimensionRecordStorage` ] 

564 Storage objects for the elements this object will related. 

565 governorStorage : `tuple` [ `GovernorDimensionRecordStorage` ] 

566 Storage objects for the governor dimensions of the elements this 

567 object will related. 

568 context : `StaticTablesContext`, optional 

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

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

571 

572 Returns 

573 ------- 

574 storage : `DatabaseDimensionOverlapStorage` 

575 A new `DatabaseDimensionOverlapStorage` subclass instance. 

576 """ 

577 raise NotImplementedError() 

578 

579 @property 

580 @abstractmethod 

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

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

583 

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

585 `DimensionUniverse`. 

586 """ 

587 raise NotImplementedError() 

588 

589 @abstractmethod 

590 def clearCaches(self) -> None: 

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

592 materialized. 

593 """ 

594 raise NotImplementedError() 

595 

596 @abstractmethod 

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

598 """Return tables used for schema digest. 

599 

600 Returns 

601 ------- 

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

603 Possibly empty set of tables for schema digest calculations. 

604 """ 

605 raise NotImplementedError() 

606 

607 @abstractmethod 

608 def make_relation( 

609 self, 

610 context: queries.SqlQueryContext, 

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

612 ) -> Relation | None: 

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

614 table. 

615 

616 High-level code should generally call 

617 `DimensionRecordStorageManager.make_spatial_join_relation` (which 

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

619 

620 Parameters 

621 ---------- 

622 context : `.queries.SqlQueryContext` 

623 Object that manages relation engines and database-side state 

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

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

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

627 Constraints imposed by other aspects of the query on governor 

628 dimensions; collections inconsistent with these constraints will be 

629 skipped. 

630 

631 Returns 

632 ------- 

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

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

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

636 working out alternative approaches involving multiple joins. 

637 """ 

638 raise NotImplementedError() 

639 

640 

641class DimensionRecordStorageManager(VersionedExtension): 

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

643 

644 `DimensionRecordStorageManager` primarily serves as a container and factory 

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

646 records for a different `DimensionElement`. 

647 

648 Parameters 

649 ---------- 

650 universe : `DimensionUniverse` 

651 Universe of all dimensions and dimension elements known to the 

652 `Registry`. 

653 

654 Notes 

655 ----- 

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

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

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

659 """ 

660 

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

662 super().__init__(registry_schema_version=registry_schema_version) 

663 self.universe = universe 

664 

665 @classmethod 

666 @abstractmethod 

667 def initialize( 

668 cls, 

669 db: Database, 

670 context: StaticTablesContext, 

671 *, 

672 universe: DimensionUniverse, 

673 registry_schema_version: VersionTuple | None = None, 

674 ) -> DimensionRecordStorageManager: 

675 """Construct an instance of the manager. 

676 

677 Parameters 

678 ---------- 

679 db : `Database` 

680 Interface to the underlying database engine and namespace. 

681 context : `StaticTablesContext` 

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

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

684 implemented with this manager. 

685 universe : `DimensionUniverse` 

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

687 registry_schema_version : `VersionTuple` or `None` 

688 Schema version of this extension as defined in registry. 

689 

690 Returns 

691 ------- 

692 manager : `DimensionRecordStorageManager` 

693 An instance of a concrete `DimensionRecordStorageManager` subclass. 

694 """ 

695 raise NotImplementedError() 

696 

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

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

699 `None` on failure. 

700 """ 

701 r = self.get(element) 

702 if r is None: 

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

704 return r 

705 

706 @abstractmethod 

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

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

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

710 

711 Parameters 

712 ---------- 

713 element : `DimensionElement` 

714 Element for which records should be returned. 

715 

716 Returns 

717 ------- 

718 records : `DimensionRecordStorage` or `None` 

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

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

721 layer. 

722 

723 Notes 

724 ----- 

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

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

727 """ 

728 raise NotImplementedError() 

729 

730 @abstractmethod 

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

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

733 creating new tables as necessary. 

734 

735 Parameters 

736 ---------- 

737 element : `DimensionElement` 

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

739 an associated `DimensionRecordStorage` returned. 

740 

741 Returns 

742 ------- 

743 records : `DimensionRecordStorage` 

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

745 layer. 

746 

747 Raises 

748 ------ 

749 TransactionInterruption 

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

751 context. 

752 """ 

753 raise NotImplementedError() 

754 

755 @abstractmethod 

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

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

758 be retrieved later via the returned key. 

759 

760 Parameters 

761 ---------- 

762 graph : `DimensionGraph` 

763 Set of dimensions to save. 

764 

765 Returns 

766 ------- 

767 key : `int` 

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

769 database. 

770 

771 Raises 

772 ------ 

773 TransactionInterruption 

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

775 context. 

776 """ 

777 raise NotImplementedError() 

778 

779 @abstractmethod 

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

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

782 database. 

783 

784 Parameters 

785 ---------- 

786 key : `int` 

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

788 database. 

789 

790 Returns 

791 ------- 

792 graph : `DimensionGraph` 

793 Retrieved graph. 

794 

795 Raises 

796 ------ 

797 KeyError 

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

799 """ 

800 raise NotImplementedError() 

801 

802 @abstractmethod 

803 def clearCaches(self) -> None: 

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

805 instances. 

806 

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

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

809 present in persistent storage. 

810 """ 

811 raise NotImplementedError() 

812 

813 @abstractmethod 

814 def make_spatial_join_relation( 

815 self, 

816 element1: str, 

817 element2: str, 

818 context: queries.SqlQueryContext, 

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

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

821 ) -> tuple[Relation, bool]: 

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

823 dimension elements. 

824 

825 Parameters 

826 ---------- 

827 element1 : `str` 

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

829 element2 : `str` 

830 Name of the other element participating in the join. 

831 context : `.queries.SqlQueryContext` 

832 Object that manages relation engines and database-side state 

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

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

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

836 Constraints imposed by other aspects of the query on governor 

837 dimensions. 

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

839 ] ], optional 

840 Relationships between dimensions that are already present in the 

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

842 that duplicate these relationships will not be included in the 

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

844 a spatial relationship has already been established. 

845 

846 Returns 

847 ------- 

848 relation : `lsst.daf.relation.Relation` 

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

850 elements. Guaranteed to have key columns for all required 

851 dimensions of both elements. 

852 needs_refinement : `bool` 

853 Whether the returned relation represents a conservative join that 

854 needs refinement via native-iteration predicate. 

855 """ 

856 raise NotImplementedError() 

857 

858 universe: DimensionUniverse 

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

860 `Registry` (`DimensionUniverse`). 

861 """