Coverage for python/lsst/daf/butler/registry/dimensions/table.py : 94%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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__ = ["TableDimensionRecordStorage"]
25from typing import Optional
27import sqlalchemy
29from ...core import (
30 DataId,
31 DimensionElement,
32 DimensionRecord,
33 makeDimensionElementTableSpec,
34 NamedKeyDict,
35 Timespan,
36 TIMESPAN_FIELD_SPECS,
37)
38from ..interfaces import Database, DimensionRecordStorage, StaticTablesContext
39from ..queries import QueryBuilder
42class TableDimensionRecordStorage(DimensionRecordStorage):
43 """A record storage implementation uses a regular database table.
45 For spatial dimension elements, use `SpatialDimensionRecordStorage`
46 instead.
48 Parameters
49 ----------
50 db : `Database`
51 Interface to the database engine and namespace that will hold these
52 dimension records.
53 element : `DimensionElement`
54 The element whose records this storage will manage.
55 table : `sqlalchemy.schema.Table`
56 The logical table for the element.
57 """
58 def __init__(self, db: Database, element: DimensionElement, *, table: sqlalchemy.schema.Table):
59 self._db = db
60 self._table = table
61 self._element = element
63 @classmethod
64 def initialize(cls, db: Database, element: DimensionElement, *,
65 context: Optional[StaticTablesContext] = None) -> DimensionRecordStorage:
66 # Docstring inherited from DimensionRecordStorage.
67 spec = makeDimensionElementTableSpec(element)
68 if context is not None: 68 ↛ 71line 68 didn't jump to line 71, because the condition on line 68 was never false
69 table = context.addTable(element.name, spec)
70 else:
71 table = db.ensureTableExists(element.name, spec)
72 return cls(db, element, table=table)
74 @property
75 def element(self) -> DimensionElement:
76 # Docstring inherited from DimensionRecordStorage.element.
77 return self._element
79 def clearCaches(self) -> None:
80 # Docstring inherited from DimensionRecordStorage.clearCaches.
81 pass
83 def join(
84 self,
85 builder: QueryBuilder, *,
86 regions: Optional[NamedKeyDict[DimensionElement, sqlalchemy.sql.ColumnElement]] = None,
87 timespans: Optional[NamedKeyDict[DimensionElement, Timespan[sqlalchemy.sql.ColumnElement]]] = None,
88 ) -> None:
89 # Docstring inherited from DimensionRecordStorage.
90 assert regions is None, "This implementation does not handle spatial joins."
91 joinDimensions = list(self.element.required)
92 joinDimensions.extend(self.element.implied)
93 joinOn = builder.startJoin(self._table, joinDimensions, self.element.RecordClass.__slots__)
94 if timespans is not None:
95 timespanInTable: Timespan[sqlalchemy.sql.ColumnElement] = Timespan(
96 begin=self._table.columns[TIMESPAN_FIELD_SPECS.begin.name],
97 end=self._table.columns[TIMESPAN_FIELD_SPECS.end.name],
98 )
99 for timespanInQuery in timespans.values(): 99 ↛ 100line 99 didn't jump to line 100, because the loop on line 99 never started
100 joinOn.append(timespanInQuery.overlaps(timespanInTable, ops=sqlalchemy.sql))
101 timespans[self.element] = timespanInTable
102 builder.finishJoin(self._table, joinOn)
103 return self._table
105 def fetch(self, dataId: DataId) -> Optional[DimensionRecord]:
106 # Docstring inherited from DimensionRecordStorage.fetch.
107 RecordClass = self.element.RecordClass
108 # I don't know how expensive it is to construct the query below, and
109 # hence how much gain there might be to caching it, so I'm going to
110 # wait for it to appear as a hotspot in a profile before trying that.
111 whereTerms = [self._table.columns[fieldName] == dataId[dimension.name]
112 for fieldName, dimension in zip(RecordClass.__slots__, self.element.required)]
113 query = sqlalchemy.sql.select(
114 [self._table.columns[name] for name in RecordClass.__slots__]
115 ).select_from(
116 self._table
117 ).where(sqlalchemy.sql.and_(*whereTerms))
118 row = self._db.query(query).fetchone()
119 if row is None:
120 return None
121 return RecordClass(*row)
123 def insert(self, *records: DimensionRecord) -> None:
124 # Docstring inherited from DimensionRecordStorage.insert.
125 elementRows = [record.toDict() for record in records]
126 with self._db.transaction():
127 self._db.insert(self._table, *elementRows)
129 def sync(self, record: DimensionRecord) -> bool:
130 # Docstring inherited from DimensionRecordStorage.sync.
131 n = len(self.element.required)
132 _, inserted = self._db.sync(
133 self._table,
134 keys={k: getattr(record, k) for k in record.__slots__[:n]},
135 compared={k: getattr(record, k) for k in record.__slots__[n:]},
136 )
137 return inserted