Coverage for python/lsst/daf/butler/registry/interfaces/_dimensions.py: 81%
111 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-01 02:05 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-01 02:05 -0700
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
23__all__ = (
24 "DatabaseDimensionOverlapStorage",
25 "DatabaseDimensionRecordStorage",
26 "DimensionRecordStorage",
27 "DimensionRecordStorageManager",
28 "GovernorDimensionRecordStorage",
29 "SkyPixDimensionRecordStorage",
30)
32from abc import ABC, abstractmethod
33from collections.abc import Callable, Iterable, Mapping, Set
34from typing import TYPE_CHECKING, Any, Tuple, Union
36import sqlalchemy
37from lsst.daf.relation import Join, Relation, sql
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
54if TYPE_CHECKING:
55 from .. import queries
56 from ._database import Database, StaticTablesContext
59OverlapSide = Union[SkyPixDimension, Tuple[DatabaseDimensionElement, str]]
62class DimensionRecordStorage(ABC):
63 """An abstract base class that represents a way of storing the records
64 associated with a single `DimensionElement`.
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.
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 """
77 @property
78 @abstractmethod
79 def element(self) -> DimensionElement:
80 """The element whose records this instance managers
81 (`DimensionElement`).
82 """
83 raise NotImplementedError()
85 @abstractmethod
86 def clearCaches(self) -> None:
87 """Clear any in-memory caches held by the storage instance.
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()
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.
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.
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()
131 @abstractmethod
132 def insert(self, *records: DimensionRecord, replace: bool = False, skip_existing: bool = False) -> None:
133 """Insert one or more records into storage.
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.
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.
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()
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.
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.
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``).
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()
199 @abstractmethod
200 def fetch_one(self, data_id: DataCoordinate, context: queries.SqlQueryContext) -> DimensionRecord | None:
201 """Retrieve a single record from storage.
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.
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()
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.
226 Implementations that never cache records should return `None`.
228 Parameters
229 ----------
230 context : `.queries.SqlQueryContext`
231 Context to be used to execute queries when no cached result is
232 available.
234 Returns
235 -------
236 cache : `Mapping` [ `DataCoordinate`, `DimensionRecord` ] or `None`
237 Mapping from data ID to dimension record, or `None`.
238 """
239 return None
241 @abstractmethod
242 def digestTables(self) -> list[sqlalchemy.schema.Table]:
243 """Return tables used for schema digest.
245 Returns
246 -------
247 tables : `list` [ `sqlalchemy.schema.Table` ]
248 Possibly empty list of tables for schema digest calculations.
249 """
250 raise NotImplementedError()
252 def _build_sql_payload(
253 self,
254 from_clause: sqlalchemy.sql.FromClause,
255 column_types: ColumnTypeInfo,
256 ) -> sql.Payload[LogicalColumn]:
257 """Construct a `lsst.daf.relation.sql.Payload` for a dimension table.
259 This is a conceptually "protected" helper method for use by subclass
260 `make_relation` implementations.
262 Parameters
263 ----------
264 from_clause : `sqlalchemy.sql.FromClause`
265 SQLAlchemy table or subquery to select from.
266 column_types : `ColumnTypeInfo`
267 Struct with information about column types that depend on registry
268 configuration.
270 Returns
271 -------
272 payload : `lsst.daf.relation.sql.Payload`
273 Relation SQL "payload" struct, containing a SQL FROM clause,
274 columns, and optional WHERE clause.
275 """
276 payload = sql.Payload[LogicalColumn](from_clause)
277 for tag, field_name in self.element.RecordClass.fields.columns.items():
278 if field_name == "timespan":
279 payload.columns_available[tag] = column_types.timespan_cls.from_columns(
280 from_clause.columns, name=field_name
281 )
282 else:
283 payload.columns_available[tag] = from_clause.columns[field_name]
284 return payload
287class GovernorDimensionRecordStorage(DimensionRecordStorage):
288 """Intermediate interface for `DimensionRecordStorage` objects that provide
289 storage for `GovernorDimension` instances.
290 """
292 @classmethod
293 @abstractmethod
294 def initialize(
295 cls,
296 db: Database,
297 dimension: GovernorDimension,
298 *,
299 context: StaticTablesContext | None = None,
300 config: Mapping[str, Any],
301 ) -> GovernorDimensionRecordStorage:
302 """Construct an instance of this class using a standardized interface.
304 Parameters
305 ----------
306 db : `Database`
307 Interface to the underlying database engine and namespace.
308 dimension : `GovernorDimension`
309 Dimension the new instance will manage records for.
310 context : `StaticTablesContext`, optional
311 If provided, an object to use to create any new tables. If not
312 provided, ``db.ensureTableExists`` should be used instead.
313 config : `Mapping`
314 Extra configuration options specific to the implementation.
316 Returns
317 -------
318 storage : `GovernorDimensionRecordStorage`
319 A new `GovernorDimensionRecordStorage` subclass instance.
320 """
321 raise NotImplementedError()
323 @property
324 @abstractmethod
325 def element(self) -> GovernorDimension:
326 # Docstring inherited from DimensionRecordStorage.
327 raise NotImplementedError()
329 @property
330 @abstractmethod
331 def table(self) -> sqlalchemy.schema.Table:
332 """The SQLAlchemy table that backs this dimension
333 (`sqlalchemy.schema.Table`).
334 """
335 raise NotImplementedError()
337 def join(
338 self,
339 target: Relation,
340 join: Join,
341 context: queries.SqlQueryContext,
342 ) -> Relation:
343 # Docstring inherited.
344 # We use Join.partial(...).apply(...) instead of Join.apply(..., ...)
345 # for the "backtracking" insertion capabilities of the former; more
346 # specifically, if `target` is a tree that starts with SQL relations
347 # and ends with iteration-engine operations (e.g. region-overlap
348 # postprocessing), this will try to perform the join upstream in the
349 # SQL engine before the transfer to iteration.
350 return join.partial(self.make_relation(context)).apply(target)
352 @abstractmethod
353 def make_relation(self, context: queries.SqlQueryContext) -> Relation:
354 """Return a relation that represents this dimension element's table.
356 This is used to provide an implementation for
357 `DimensionRecordStorage.join`, and is also callable in its own right.
359 Parameters
360 ----------
361 context : `.queries.SqlQueryContext`
362 Object that manages relation engines and database-side state
363 (e.g. temporary tables) for the query.
365 Returns
366 -------
367 relation : `~lsst.daf.relation.Relation`
368 New relation that includes this relation's dimension key and
369 record columns, with rows constrained to those for which the
370 dimension key values exist in the registry.
371 """
372 raise NotImplementedError()
374 @abstractmethod
375 def get_record_cache(self, context: queries.SqlQueryContext) -> Mapping[DataCoordinate, DimensionRecord]:
376 raise NotImplementedError()
378 @abstractmethod
379 def registerInsertionListener(self, callback: Callable[[DimensionRecord], None]) -> None:
380 """Add a function or method to be called after new records for this
381 dimension are inserted by `insert` or `sync`.
383 Parameters
384 ----------
385 callback
386 Callable that takes a single `DimensionRecord` argument. This will
387 be called immediately after any successful insertion, in the same
388 transaction.
389 """
390 raise NotImplementedError()
393class SkyPixDimensionRecordStorage(DimensionRecordStorage):
394 """Intermediate interface for `DimensionRecordStorage` objects that provide
395 storage for `SkyPixDimension` instances.
396 """
398 @property
399 @abstractmethod
400 def element(self) -> SkyPixDimension:
401 # Docstring inherited from DimensionRecordStorage.
402 raise NotImplementedError()
405class DatabaseDimensionRecordStorage(DimensionRecordStorage):
406 """Intermediate interface for `DimensionRecordStorage` objects that provide
407 storage for `DatabaseDimensionElement` instances.
408 """
410 @classmethod
411 @abstractmethod
412 def initialize(
413 cls,
414 db: Database,
415 element: DatabaseDimensionElement,
416 *,
417 context: StaticTablesContext | None = None,
418 config: Mapping[str, Any],
419 governors: NamedKeyMapping[GovernorDimension, GovernorDimensionRecordStorage],
420 view_target: DatabaseDimensionRecordStorage | None = None,
421 ) -> DatabaseDimensionRecordStorage:
422 """Construct an instance of this class using a standardized interface.
424 Parameters
425 ----------
426 db : `Database`
427 Interface to the underlying database engine and namespace.
428 element : `DatabaseDimensionElement`
429 Dimension element the new instance will manage records for.
430 context : `StaticTablesContext`, optional
431 If provided, an object to use to create any new tables. If not
432 provided, ``db.ensureTableExists`` should be used instead.
433 config : `Mapping`
434 Extra configuration options specific to the implementation.
435 governors : `NamedKeyMapping`
436 Mapping containing all governor dimension storage implementations.
437 view_target : `DatabaseDimensionRecordStorage`, optional
438 Storage object for the element this target's storage is a view of
439 (i.e. when `viewOf` is not `None`).
441 Returns
442 -------
443 storage : `DatabaseDimensionRecordStorage`
444 A new `DatabaseDimensionRecordStorage` subclass instance.
445 """
446 raise NotImplementedError()
448 @property
449 @abstractmethod
450 def element(self) -> DatabaseDimensionElement:
451 # Docstring inherited from DimensionRecordStorage.
452 raise NotImplementedError()
454 def join(
455 self,
456 target: Relation,
457 join: Join,
458 context: queries.SqlQueryContext,
459 ) -> Relation:
460 # Docstring inherited.
461 # See comment on similar code in GovernorDimensionRecordStorage.join
462 # for why we use `Join.partial` here.
463 return join.partial(self.make_relation(context)).apply(target)
465 @abstractmethod
466 def make_relation(self, context: queries.SqlQueryContext) -> Relation:
467 """Return a relation that represents this dimension element's table.
469 This is used to provide an implementation for
470 `DimensionRecordStorage.join`, and is also callable in its own right.
472 Parameters
473 ----------
474 context : `.queries.SqlQueryContext`
475 Object that manages relation engines and database-side state
476 (e.g. temporary tables) for the query.
478 Returns
479 -------
480 relation : `~lsst.daf.relation.Relation`
481 New relation that includes this relation's dimension key and
482 record columns, with rows constrained to those for which the
483 dimension key values exist in the registry.
484 """
485 raise NotImplementedError()
487 def connect(self, overlaps: DatabaseDimensionOverlapStorage) -> None:
488 """Inform this record storage object of the object that will manage
489 the overlaps between this element and another element.
491 This will only be called if ``self.element.spatial is not None``,
492 and will be called immediately after construction (before any other
493 methods). In the future, implementations will be required to call a
494 method on any connected overlap storage objects any time new records
495 for the element are inserted.
497 Parameters
498 ----------
499 overlaps : `DatabaseDimensionRecordStorage`
500 Object managing overlaps between this element and another
501 database-backed element.
502 """
503 raise NotImplementedError(f"{type(self).__name__} does not support spatial elements.")
505 def make_spatial_join_relation(
506 self,
507 other: DimensionElement,
508 context: queries.SqlQueryContext,
509 governor_constraints: Mapping[str, Set[str]],
510 ) -> Relation | None:
511 """Return a `lsst.daf.relation.Relation` that represents the spatial
512 overlap join between two dimension elements.
514 High-level code should generally call
515 `DimensionRecordStorageManager.make_spatial_join_relation` (which
516 delegates to this) instead of calling this method directly.
518 Parameters
519 ----------
520 other : `DimensionElement`
521 Element to compute overlaps with. Guaranteed by caller to be
522 spatial (as is ``self``), with a different topological family. May
523 be a `DatabaseDimensionElement` or a `SkyPixDimension`.
524 context : `.queries.SqlQueryContext`
525 Object that manages relation engines and database-side state
526 (e.g. temporary tables) for the query.
527 governor_constraints : `Mapping` [ `str`, `~collections.abc.Set` ], \
528 optional
529 Constraints imposed by other aspects of the query on governor
530 dimensions.
532 Returns
533 -------
534 relation : `lsst.daf.relation.Relation` or `None`
535 Join relation. Should be `None` when no direct overlaps for this
536 combination are stored; higher-level code is responsible for
537 working out alternative approaches involving multiple joins.
538 """
539 return None
542class DatabaseDimensionOverlapStorage(ABC):
543 """A base class for objects that manage overlaps between a pair of
544 database-backed dimensions.
545 """
547 @classmethod
548 @abstractmethod
549 def initialize(
550 cls,
551 db: Database,
552 elementStorage: tuple[DatabaseDimensionRecordStorage, DatabaseDimensionRecordStorage],
553 governorStorage: tuple[GovernorDimensionRecordStorage, GovernorDimensionRecordStorage],
554 context: StaticTablesContext | None = None,
555 ) -> DatabaseDimensionOverlapStorage:
556 """Construct an instance of this class using a standardized interface.
558 Parameters
559 ----------
560 db : `Database`
561 Interface to the underlying database engine and namespace.
562 elementStorage : `tuple` [ `DatabaseDimensionRecordStorage` ]
563 Storage objects for the elements this object will related.
564 governorStorage : `tuple` [ `GovernorDimensionRecordStorage` ]
565 Storage objects for the governor dimensions of the elements this
566 object will related.
567 context : `StaticTablesContext`, optional
568 If provided, an object to use to create any new tables. If not
569 provided, ``db.ensureTableExists`` should be used instead.
571 Returns
572 -------
573 storage : `DatabaseDimensionOverlapStorage`
574 A new `DatabaseDimensionOverlapStorage` subclass instance.
575 """
576 raise NotImplementedError()
578 @property
579 @abstractmethod
580 def elements(self) -> tuple[DatabaseDimensionElement, DatabaseDimensionElement]:
581 """The pair of elements whose overlaps this object manages.
583 The order of elements is the same as their ordering within the
584 `DimensionUniverse`.
585 """
586 raise NotImplementedError()
588 @abstractmethod
589 def clearCaches(self) -> None:
590 """Clear any cached state about which overlaps have been
591 materialized."""
592 raise NotImplementedError()
594 @abstractmethod
595 def digestTables(self) -> Iterable[sqlalchemy.schema.Table]:
596 """Return tables used for schema digest.
598 Returns
599 -------
600 tables : `Iterable` [ `sqlalchemy.schema.Table` ]
601 Possibly empty set of tables for schema digest calculations.
602 """
603 raise NotImplementedError()
605 @abstractmethod
606 def make_relation(
607 self,
608 context: queries.SqlQueryContext,
609 governor_constraints: Mapping[str, Set[str]],
610 ) -> Relation | None:
611 """Return a `lsst.daf.relation.Relation` that represents the join
612 table.
614 High-level code should generally call
615 `DimensionRecordStorageManager.make_spatial_join_relation` (which
616 delegates to this) instead of calling this method directly.
618 Parameters
619 ----------
620 context : `.queries.SqlQueryContext`
621 Object that manages relation engines and database-side state
622 (e.g. temporary tables) for the query.
623 governor_constraints : `Mapping` [ `str`, `~collections.abc.Set` ], \
624 optional
625 Constraints imposed by other aspects of the query on governor
626 dimensions; collections inconsistent with these constraints will be
627 skipped.
629 Returns
630 -------
631 relation : `lsst.daf.relation.Relation` or `None`
632 Join relation. Should be `None` when no direct overlaps for this
633 combination are stored; higher-level code is responsible for
634 working out alternative approaches involving multiple joins.
635 """
636 raise NotImplementedError()
639class DimensionRecordStorageManager(VersionedExtension):
640 """An interface for managing the dimension records in a `Registry`.
642 `DimensionRecordStorageManager` primarily serves as a container and factory
643 for `DimensionRecordStorage` instances, which each provide access to the
644 records for a different `DimensionElement`.
646 Parameters
647 ----------
648 universe : `DimensionUniverse`
649 Universe of all dimensions and dimension elements known to the
650 `Registry`.
652 Notes
653 -----
654 In a multi-layer `Registry`, many dimension elements will only have
655 records in one layer (often the base layer). The union of the records
656 across all layers forms the logical table for the full `Registry`.
657 """
659 def __init__(self, *, universe: DimensionUniverse, registry_schema_version: VersionTuple | None = None):
660 super().__init__(registry_schema_version=registry_schema_version)
661 self.universe = universe
663 @classmethod
664 @abstractmethod
665 def initialize(
666 cls,
667 db: Database,
668 context: StaticTablesContext,
669 *,
670 universe: DimensionUniverse,
671 registry_schema_version: VersionTuple | None = None,
672 ) -> DimensionRecordStorageManager:
673 """Construct an instance of the manager.
675 Parameters
676 ----------
677 db : `Database`
678 Interface to the underlying database engine and namespace.
679 context : `StaticTablesContext`
680 Context object obtained from `Database.declareStaticTables`; used
681 to declare any tables that should always be present in a layer
682 implemented with this manager.
683 universe : `DimensionUniverse`
684 Universe graph containing dimensions known to this `Registry`.
685 registry_schema_version : `VersionTuple` or `None`
686 Schema version of this extension as defined in registry.
688 Returns
689 -------
690 manager : `DimensionRecordStorageManager`
691 An instance of a concrete `DimensionRecordStorageManager` subclass.
692 """
693 raise NotImplementedError()
695 def __getitem__(self, element: DimensionElement | str) -> DimensionRecordStorage:
696 """Interface to `get` that raises `LookupError` instead of returning
697 `None` on failure.
698 """
699 r = self.get(element)
700 if r is None:
701 raise LookupError(f"No dimension element '{element}' found in this registry layer.")
702 return r
704 @abstractmethod
705 def get(self, element: DimensionElement | str) -> DimensionRecordStorage | None:
706 """Return an object that provides access to the records associated with
707 the given element, if one exists in this layer.
709 Parameters
710 ----------
711 element : `DimensionElement`
712 Element for which records should be returned.
714 Returns
715 -------
716 records : `DimensionRecordStorage` or `None`
717 The object representing the records for the given element in this
718 layer, or `None` if there are no records for that element in this
719 layer.
721 Notes
722 -----
723 Dimension elements registered by another client of the same layer since
724 the last call to `initialize` or `refresh` may not be found.
725 """
726 raise NotImplementedError()
728 @abstractmethod
729 def register(self, element: DimensionElement) -> DimensionRecordStorage:
730 """Ensure that this layer can hold records for the given element,
731 creating new tables as necessary.
733 Parameters
734 ----------
735 element : `DimensionElement`
736 Element for which a table should created (as necessary) and
737 an associated `DimensionRecordStorage` returned.
739 Returns
740 -------
741 records : `DimensionRecordStorage`
742 The object representing the records for the given element in this
743 layer.
745 Raises
746 ------
747 TransactionInterruption
748 Raised if this operation is invoked within a `Database.transaction`
749 context.
750 """
751 raise NotImplementedError()
753 @abstractmethod
754 def saveDimensionGraph(self, graph: DimensionGraph) -> int:
755 """Save a `DimensionGraph` definition to the database, allowing it to
756 be retrieved later via the returned key.
758 Parameters
759 ----------
760 graph : `DimensionGraph`
761 Set of dimensions to save.
763 Returns
764 -------
765 key : `int`
766 Integer used as the unique key for this `DimensionGraph` in the
767 database.
769 Raises
770 ------
771 TransactionInterruption
772 Raised if this operation is invoked within a `Database.transaction`
773 context.
774 """
775 raise NotImplementedError()
777 @abstractmethod
778 def loadDimensionGraph(self, key: int) -> DimensionGraph:
779 """Retrieve a `DimensionGraph` that was previously saved in the
780 database.
782 Parameters
783 ----------
784 key : `int`
785 Integer used as the unique key for this `DimensionGraph` in the
786 database.
788 Returns
789 -------
790 graph : `DimensionGraph`
791 Retrieved graph.
793 Raises
794 ------
795 KeyError
796 Raised if the given key cannot be found in the database.
797 """
798 raise NotImplementedError()
800 @abstractmethod
801 def clearCaches(self) -> None:
802 """Clear any in-memory caches held by nested `DimensionRecordStorage`
803 instances.
805 This is called by `Registry` when transactions are rolled back, to
806 avoid in-memory caches from ever containing records that are not
807 present in persistent storage.
808 """
809 raise NotImplementedError()
811 @abstractmethod
812 def make_spatial_join_relation(
813 self,
814 element1: str,
815 element2: str,
816 context: queries.SqlQueryContext,
817 governor_constraints: Mapping[str, Set[str]],
818 ) -> tuple[Relation, bool]:
819 """Create a relation that represents the spatial join between two
820 dimension elements.
822 Parameters
823 ----------
824 element1 : `str`
825 Name of one of the elements participating in the join.
826 element2 : `str`
827 Name of the other element participating in the join.
828 context : `.queries.SqlQueryContext`
829 Object that manages relation engines and database-side state
830 (e.g. temporary tables) for the query.
831 governor_constraints : `Mapping` [ `str`, `collections.abc.Set` ], \
832 optional
833 Constraints imposed by other aspects of the query on governor
834 dimensions.
836 Returns
837 -------
838 relation : ``lsst.daf.relation.Relation`
839 New relation that represents a spatial join between the two given
840 elements. Guaranteed to have key columns for all required
841 dimensions of both elements.
842 needs_refinement : `bool`
843 Whether the returned relation represents a conservative join that
844 needs refinement via native-iteration predicate.
845 """
846 raise NotImplementedError()
848 universe: DimensionUniverse
849 """Universe of all dimensions and dimension elements known to the
850 `Registry` (`DimensionUniverse`).
851 """