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
« 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
29__all__ = (
30 "DatabaseDimensionOverlapStorage",
31 "DatabaseDimensionRecordStorage",
32 "DimensionRecordStorage",
33 "DimensionRecordStorageManager",
34 "GovernorDimensionRecordStorage",
35 "SkyPixDimensionRecordStorage",
36)
38from abc import ABC, abstractmethod
39from collections.abc import Callable, Iterable, Mapping, Set
40from typing import TYPE_CHECKING, Any
42import sqlalchemy
43from lsst.daf.relation import Join, Relation, sql
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
60if TYPE_CHECKING:
61 from .. import queries
62 from ._database import Database, StaticTablesContext
65OverlapSide = SkyPixDimension | tuple[DatabaseDimensionElement, str]
68class DimensionRecordStorage(ABC):
69 """An abstract base class that represents a way of storing the records
70 associated with a single `DimensionElement`.
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.
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 """
83 @property
84 @abstractmethod
85 def element(self) -> DimensionElement:
86 """The element whose records this instance managers
87 (`DimensionElement`).
88 """
89 raise NotImplementedError()
91 @abstractmethod
92 def clearCaches(self) -> None:
93 """Clear any in-memory caches held by the storage instance.
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()
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.
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.
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()
137 @abstractmethod
138 def insert(self, *records: DimensionRecord, replace: bool = False, skip_existing: bool = False) -> None:
139 """Insert one or more records into storage.
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.
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.
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()
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.
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.
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``).
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()
205 @abstractmethod
206 def fetch_one(self, data_id: DataCoordinate, context: queries.SqlQueryContext) -> DimensionRecord | None:
207 """Retrieve a single record from storage.
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.
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()
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.
232 Implementations that never cache records should return `None`.
234 Parameters
235 ----------
236 context : `.queries.SqlQueryContext`
237 Context to be used to execute queries when no cached result is
238 available.
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
248 @abstractmethod
249 def digestTables(self) -> list[sqlalchemy.schema.Table]:
250 """Return tables used for schema digest.
252 Returns
253 -------
254 tables : `list` [ `sqlalchemy.schema.Table` ]
255 Possibly empty list of tables for schema digest calculations.
256 """
257 raise NotImplementedError()
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.
266 This is a conceptually "protected" helper method for use by subclass
267 `make_relation` implementations.
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.
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
294class GovernorDimensionRecordStorage(DimensionRecordStorage):
295 """Intermediate interface for `DimensionRecordStorage` objects that provide
296 storage for `GovernorDimension` instances.
297 """
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.
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.
323 Returns
324 -------
325 storage : `GovernorDimensionRecordStorage`
326 A new `GovernorDimensionRecordStorage` subclass instance.
327 """
328 raise NotImplementedError()
330 @property
331 @abstractmethod
332 def element(self) -> GovernorDimension:
333 # Docstring inherited from DimensionRecordStorage.
334 raise NotImplementedError()
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()
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)
359 @abstractmethod
360 def make_relation(self, context: queries.SqlQueryContext) -> Relation:
361 """Return a relation that represents this dimension element's table.
363 This is used to provide an implementation for
364 `DimensionRecordStorage.join`, and is also callable in its own right.
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.
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()
381 @abstractmethod
382 def get_record_cache(self, context: queries.SqlQueryContext) -> Mapping[DataCoordinate, DimensionRecord]:
383 raise NotImplementedError()
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`.
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()
400class SkyPixDimensionRecordStorage(DimensionRecordStorage):
401 """Intermediate interface for `DimensionRecordStorage` objects that provide
402 storage for `SkyPixDimension` instances.
403 """
405 @property
406 @abstractmethod
407 def element(self) -> SkyPixDimension:
408 # Docstring inherited from DimensionRecordStorage.
409 raise NotImplementedError()
412class DatabaseDimensionRecordStorage(DimensionRecordStorage):
413 """Intermediate interface for `DimensionRecordStorage` objects that provide
414 storage for `DatabaseDimensionElement` instances.
415 """
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.
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`).
448 Returns
449 -------
450 storage : `DatabaseDimensionRecordStorage`
451 A new `DatabaseDimensionRecordStorage` subclass instance.
452 """
453 raise NotImplementedError()
455 @property
456 @abstractmethod
457 def element(self) -> DatabaseDimensionElement:
458 # Docstring inherited from DimensionRecordStorage.
459 raise NotImplementedError()
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)
472 @abstractmethod
473 def make_relation(self, context: queries.SqlQueryContext) -> Relation:
474 """Return a relation that represents this dimension element's table.
476 This is used to provide an implementation for
477 `DimensionRecordStorage.join`, and is also callable in its own right.
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.
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()
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.
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.
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.")
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.
521 High-level code should generally call
522 `DimensionRecordStorageManager.make_spatial_join_relation` (which
523 delegates to this) instead of calling this method directly.
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.
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
549class DatabaseDimensionOverlapStorage(ABC):
550 """A base class for objects that manage overlaps between a pair of
551 database-backed dimensions.
552 """
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.
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.
578 Returns
579 -------
580 storage : `DatabaseDimensionOverlapStorage`
581 A new `DatabaseDimensionOverlapStorage` subclass instance.
582 """
583 raise NotImplementedError()
585 @property
586 @abstractmethod
587 def elements(self) -> tuple[DatabaseDimensionElement, DatabaseDimensionElement]:
588 """The pair of elements whose overlaps this object manages.
590 The order of elements is the same as their ordering within the
591 `DimensionUniverse`.
592 """
593 raise NotImplementedError()
595 @abstractmethod
596 def clearCaches(self) -> None:
597 """Clear any cached state about which overlaps have been
598 materialized.
599 """
600 raise NotImplementedError()
602 @abstractmethod
603 def digestTables(self) -> Iterable[sqlalchemy.schema.Table]:
604 """Return tables used for schema digest.
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()
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.
622 High-level code should generally call
623 `DimensionRecordStorageManager.make_spatial_join_relation` (which
624 delegates to this) instead of calling this method directly.
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.
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()
647class DimensionRecordStorageManager(VersionedExtension):
648 """An interface for managing the dimension records in a `Registry`.
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`.
654 Parameters
655 ----------
656 universe : `DimensionUniverse`
657 Universe of all dimensions and dimension elements known to the
658 `Registry`.
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 """
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
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.
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.
696 Returns
697 -------
698 manager : `DimensionRecordStorageManager`
699 An instance of a concrete `DimensionRecordStorageManager` subclass.
700 """
701 raise NotImplementedError()
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
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.
717 Parameters
718 ----------
719 element : `DimensionElement`
720 Element for which records should be returned.
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.
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()
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.
741 Parameters
742 ----------
743 element : `DimensionElement`
744 Element for which a table should created (as necessary) and
745 an associated `DimensionRecordStorage` returned.
747 Returns
748 -------
749 records : `DimensionRecordStorage`
750 The object representing the records for the given element in this
751 layer.
753 Raises
754 ------
755 TransactionInterruption
756 Raised if this operation is invoked within a `Database.transaction`
757 context.
758 """
759 raise NotImplementedError()
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.
766 Parameters
767 ----------
768 graph : `DimensionGraph`
769 Set of dimensions to save.
771 Returns
772 -------
773 key : `int`
774 Integer used as the unique key for this `DimensionGraph` in the
775 database.
777 Raises
778 ------
779 TransactionInterruption
780 Raised if this operation is invoked within a `Database.transaction`
781 context.
782 """
783 raise NotImplementedError()
785 @abstractmethod
786 def loadDimensionGraph(self, key: int) -> DimensionGraph:
787 """Retrieve a `DimensionGraph` that was previously saved in the
788 database.
790 Parameters
791 ----------
792 key : `int`
793 Integer used as the unique key for this `DimensionGraph` in the
794 database.
796 Returns
797 -------
798 graph : `DimensionGraph`
799 Retrieved graph.
801 Raises
802 ------
803 KeyError
804 Raised if the given key cannot be found in the database.
805 """
806 raise NotImplementedError()
808 @abstractmethod
809 def clearCaches(self) -> None:
810 """Clear any in-memory caches held by nested `DimensionRecordStorage`
811 instances.
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()
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.
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.
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()
864 universe: DimensionUniverse
865 """Universe of all dimensions and dimension elements known to the
866 `Registry` (`DimensionUniverse`).
867 """