Coverage for python/lsst/daf/butler/registry/dimensions/overlaps.py: 97%
52 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-12 09:00 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-12 09: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 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/>.
22from __future__ import annotations
24__all__ = ("CrossFamilyDimensionOverlapStorage",)
26import logging
27from collections.abc import Iterable
29import sqlalchemy
31from ...core import DatabaseDimensionElement, addDimensionForeignKey, ddl
32from ..interfaces import (
33 Database,
34 DatabaseDimensionOverlapStorage,
35 DatabaseDimensionRecordStorage,
36 GovernorDimensionRecordStorage,
37 StaticTablesContext,
38)
40_LOG = logging.getLogger(__name__)
43class CrossFamilyDimensionOverlapStorage(DatabaseDimensionOverlapStorage):
44 """Basic implementation of materialized overlaps between
45 otherwise-unrelated dimension elements.
47 New instances should be constructed by calling `initialize`, not by calling
48 the constructor directly.
50 Parameters
51 ----------
52 db : `Database`
53 Interface to the underlying database engine and namespace.
54 elementStorage : `tuple` [ `DatabaseDimensionRecordStorage` ]
55 Storage objects for the elements this object will relate.
56 governorStorage : `tuple` [ `GovernorDimensionRecordStorage` ]
57 Storage objects for the governor dimensions of the elements this
58 object will relate.
59 summaryTable : `sqlalchemy.schema.Table`
60 Table that records which combinations of governor dimension values
61 have materialized overlap rows.
62 overlapTable : `sqlalchemy.schema.Table`
63 Table containing the actual materialized overlap rows.
65 Notes
66 -----
67 At present, this class (like its ABC) is just a stub that creates the
68 tables it will use, but does nothing else.
69 """
71 def __init__(
72 self,
73 db: Database,
74 elementStorage: tuple[DatabaseDimensionRecordStorage, DatabaseDimensionRecordStorage],
75 governorStorage: tuple[GovernorDimensionRecordStorage, GovernorDimensionRecordStorage],
76 summaryTable: sqlalchemy.schema.Table,
77 overlapTable: sqlalchemy.schema.Table,
78 ):
79 self._db = db
80 self._elementStorage = elementStorage
81 self._governorStorage = governorStorage
82 self._summaryTable = summaryTable
83 self._overlapTable = overlapTable
85 @classmethod
86 def initialize(
87 cls,
88 db: Database,
89 elementStorage: tuple[DatabaseDimensionRecordStorage, DatabaseDimensionRecordStorage],
90 governorStorage: tuple[GovernorDimensionRecordStorage, GovernorDimensionRecordStorage],
91 context: StaticTablesContext | None = None,
92 ) -> DatabaseDimensionOverlapStorage:
93 # Docstring inherited from DatabaseDimensionOverlapStorage.
94 if context is not None: 94 ↛ 97line 94 didn't jump to line 97, because the condition on line 94 was never false
95 op = context.addTable
96 else:
97 op = db.ensureTableExists
98 elements = (elementStorage[0].element, elementStorage[1].element)
99 summaryTable = op(
100 cls._SUMMARY_TABLE_NAME_SPEC.format(*elements),
101 cls._makeSummaryTableSpec(elements),
102 )
103 overlapTable = op(
104 cls._OVERLAP_TABLE_NAME_SPEC.format(*elements),
105 cls._makeOverlapTableSpec(elements),
106 )
107 return CrossFamilyDimensionOverlapStorage(
108 db,
109 elementStorage,
110 governorStorage,
111 summaryTable=summaryTable,
112 overlapTable=overlapTable,
113 )
115 @property
116 def elements(self) -> tuple[DatabaseDimensionElement, DatabaseDimensionElement]:
117 # Docstring inherited from DatabaseDimensionOverlapStorage.
118 return (self._elementStorage[0].element, self._elementStorage[1].element)
120 def digestTables(self) -> Iterable[sqlalchemy.schema.Table]:
121 # Docstring inherited from DatabaseDimensionOverlapStorage.
122 return [self._summaryTable, self._overlapTable]
124 _SUMMARY_TABLE_NAME_SPEC = "{0.name}_{1.name}_overlap_summary"
126 @classmethod
127 def _makeSummaryTableSpec(
128 cls, elements: tuple[DatabaseDimensionElement, DatabaseDimensionElement]
129 ) -> ddl.TableSpec:
130 """Create a specification for the table that records which combinations
131 of skypix dimension and governor value have materialized overlaps.
133 Parameters
134 ----------
135 elements : `tuple` [ `DatabaseDimensionElement` ]
136 Dimension elements whose overlaps are to be managed.
138 Returns
139 -------
140 tableSpec : `ddl.TableSpec`
141 Table specification.
142 """
143 assert elements[0].spatial is not None and elements[1].spatial is not None
144 assert elements[0].spatial.governor != elements[1].spatial.governor
145 tableSpec = ddl.TableSpec(fields=[])
146 addDimensionForeignKey(tableSpec, elements[0].spatial.governor, primaryKey=True)
147 addDimensionForeignKey(tableSpec, elements[1].spatial.governor, primaryKey=True)
148 return tableSpec
150 _OVERLAP_TABLE_NAME_SPEC = "{0.name}_{1.name}_overlap"
152 @classmethod
153 def _makeOverlapTableSpec(
154 cls, elements: tuple[DatabaseDimensionElement, DatabaseDimensionElement]
155 ) -> ddl.TableSpec:
156 """Create a specification for the table that holds materialized
157 overlap rows.
159 Parameters
160 ----------
161 elements : `tuple` [ `DatabaseDimensionElement` ]
162 Dimension elements whose overlaps are to be managed.
164 Returns
165 -------
166 tableSpec : `ddl.TableSpec`
167 Table specification.
168 """
169 assert elements[0].graph.required.isdisjoint(elements[1].graph.required)
170 tableSpec = ddl.TableSpec(fields=[])
171 # Add governor dimensions first, so they appear first in the primary
172 # key; we may often (in the future, perhaps always) know these at
173 # query-generation time.
174 for element in elements:
175 assert element.spatial is not None
176 addDimensionForeignKey(tableSpec, element.spatial.governor, primaryKey=True)
177 # Add remaining dimension keys.
178 for element in elements:
179 assert element.spatial is not None
180 for dimension in element.required:
181 if dimension != element.spatial.governor:
182 addDimensionForeignKey(tableSpec, dimension, primaryKey=True)
183 return tableSpec