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 DataCoordinate, DimensionElement, DimensionRecord, Timespan, TIMESPAN_FIELD_SPECS
30from ...core.dimensions.schema import makeElementTableSpec
31from ...core.utils import NamedKeyDict
32from ..interfaces import Database, DimensionRecordStorage, StaticTablesContext
33from ..queries import QueryBuilder
36class TableDimensionRecordStorage(DimensionRecordStorage):
37 """A record storage implementation uses a regular database table.
39 For spatial dimension elements, use `SpatialDimensionRecordStorage`
40 instead.
42 Parameters
43 ----------
44 db : `Database`
45 Interface to the database engine and namespace that will hold these
46 dimension records.
47 element : `DimensionElement`
48 The element whose records this storage will manage.
49 table : `sqlalchemy.schema.Table`
50 The logical table for the element.
51 """
52 def __init__(self, db: Database, element: DimensionElement, *, table: sqlalchemy.schema.Table):
53 self._db = db
54 self._table = table
55 self._element = element
57 @classmethod
58 def initialize(cls, db: Database, element: DimensionElement, *,
59 context: Optional[StaticTablesContext] = None) -> DimensionRecordStorage:
60 # Docstring inherited from DimensionRecordStorage.
61 spec = makeElementTableSpec(element)
62 if context is not None: 62 ↛ 65line 62 didn't jump to line 65, because the condition on line 62 was never false
63 table = context.addTable(element.name, spec)
64 else:
65 table = db.ensureTableExists(element.name, spec)
66 return cls(db, element, table=table)
68 @property
69 def element(self) -> DimensionElement:
70 # Docstring inherited from DimensionRecordStorage.element.
71 return self._element
73 def clearCaches(self):
74 # Docstring inherited from DimensionRecordStorage.clearCaches.
75 pass
77 def join(
78 self,
79 builder: QueryBuilder, *,
80 regions: Optional[NamedKeyDict[DimensionElement, sqlalchemy.sql.ColumnElement]] = None,
81 timespans: Optional[NamedKeyDict[DimensionElement, Timespan[sqlalchemy.sql.ColumnElement]]] = None,
82 ):
83 # Docstring inherited from DimensionRecordStorage.
84 assert regions is None, "This implementation does not handle spatial joins."
85 joinDimensions = list(self.element.graph.required)
86 joinDimensions.extend(self.element.implied)
87 joinOn = builder.startJoin(self._table, joinDimensions, self.element.RecordClass.__slots__)
88 if timespans is not None:
89 timespanInTable = Timespan(
90 begin=self._table.columns[TIMESPAN_FIELD_SPECS.begin.name],
91 end=self._table.columns[TIMESPAN_FIELD_SPECS.end.name],
92 )
93 for timespanInQuery in timespans.values(): 93 ↛ 94line 93 didn't jump to line 94, because the loop on line 93 never started
94 joinOn.append(timespanInQuery.overlaps(timespanInTable, ops=sqlalchemy.sql))
95 timespans[self.element] = timespanInTable
96 builder.finishJoin(self._table, joinOn)
97 return self._table
99 def fetch(self, dataId: DataCoordinate) -> Optional[DimensionRecord]:
100 # Docstring inherited from DimensionRecordStorage.fetch.
101 RecordClass = self.element.RecordClass
102 # I don't know how expensive it is to construct the query below, and
103 # hence how much gain there might be to caching it, so I'm going to
104 # wait for it to appear as a hotspot in a profile before trying that.
105 whereTerms = [self._table.columns[fieldName] == dataId[dimension.name]
106 for fieldName, dimension in zip(RecordClass.__slots__, self.element.graph.required)]
107 query = sqlalchemy.sql.select(
108 [self._table.columns[name] for name in RecordClass.__slots__]
109 ).select_from(
110 self._table
111 ).where(sqlalchemy.sql.and_(*whereTerms))
112 row = self._db.query(query).fetchone()
113 if row is None:
114 return None
115 return RecordClass(*row)
117 def insert(self, *records: DimensionRecord):
118 # Docstring inherited from DimensionRecordStorage.insert.
119 elementRows = [record.toDict() for record in records]
120 with self._db.transaction():
121 self._db.insert(self._table, *elementRows)