Coverage for python/lsst/daf/butler/registry/interfaces/_opaque.py: 82%
42 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-30 08:35 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-30 08:35 +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/>.
28"""Interfaces for the objects that manage opaque (logical) tables within a
29`Registry`.
30"""
32from __future__ import annotations
34__all__ = ["OpaqueTableStorage", "OpaqueTableStorageManager"]
36from abc import ABC, abstractmethod
37from collections.abc import Iterable, Iterator, Mapping, Sequence
38from typing import TYPE_CHECKING, Any
40from ...ddl import TableSpec
41from ._database import Database, StaticTablesContext
42from ._versioning import VersionedExtension, VersionTuple
44if TYPE_CHECKING:
45 from ...datastore import DatastoreTransaction
48class OpaqueTableStorage(ABC):
49 """An interface that manages the records associated with a particular
50 opaque table in a `Registry`.
52 Parameters
53 ----------
54 name : `str`
55 Name of the opaque table.
56 """
58 def __init__(self, name: str):
59 self.name = name
61 @abstractmethod
62 def insert(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None:
63 """Insert records into the table.
65 Parameters
66 ----------
67 *data
68 Each additional positional argument is a dictionary that represents
69 a single row to be added.
70 transaction : `DatastoreTransaction`, optional
71 Transaction object that can be used to enable an explicit rollback
72 of the insert to be registered. Can be ignored if rollback is
73 handled via a different mechanism, such as by a database. Can be
74 `None` if no external transaction is available.
75 """
76 raise NotImplementedError()
78 @abstractmethod
79 def ensure(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None:
80 """Insert records into the table, skipping rows that already exist.
82 Parameters
83 ----------
84 *data
85 Each additional positional argument is a dictionary that represents
86 a single row to be added.
87 transaction : `DatastoreTransaction`, optional
88 Transaction object that can be used to enable an explicit rollback
89 of the insert to be registered. Can be ignored if rollback is
90 handled via a different mechanism, such as by a database. Can be
91 `None` if no external transaction is available.
92 """
93 raise NotImplementedError()
95 @abstractmethod
96 def replace(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None:
97 """Insert records into the table, replacing if previously existing
98 but different.
100 Parameters
101 ----------
102 *data
103 Each additional positional argument is a dictionary that represents
104 a single row to be added.
105 transaction : `DatastoreTransaction`, optional
106 Transaction object that can be used to enable an explicit rollback
107 of the insert to be registered. Can be ignored if rollback is
108 handled via a different mechanism, such as by a database. Can be
109 `None` if no external transaction is available.
110 """
111 raise NotImplementedError()
113 @abstractmethod
114 def fetch(self, **where: Any) -> Iterator[Mapping[Any, Any]]:
115 """Retrieve records from an opaque table.
117 Parameters
118 ----------
119 **where
120 Additional keyword arguments are interpreted as equality
121 constraints that restrict the returned rows (combined with AND);
122 keyword arguments are column names and values are the values they
123 must have.
125 Yields
126 ------
127 row : `dict`
128 A dictionary representing a single result row.
129 """
130 raise NotImplementedError()
132 @abstractmethod
133 def fetch_batches(
134 self,
135 **where: Any,
136 ) -> Iterator[Sequence[Mapping]]:
137 """Retrieve records from an opaque table in batches.
139 Parameters
140 ----------
141 **where
142 Same as ``OpaqueTableStorage.fetch``.
144 Yields
145 ------
146 batch
147 A batch of mappings representing a series of result rows.
148 """
149 raise NotImplementedError()
151 @abstractmethod
152 def delete(self, columns: Iterable[str], *rows: dict) -> None:
153 """Remove records from an opaque table.
155 Parameters
156 ----------
157 columns : `~collections.abc.Iterable` of `str`
158 The names of columns that will be used to constrain the rows to
159 be deleted; these will be combined via ``AND`` to form the
160 ``WHERE`` clause of the delete query.
161 *rows
162 Positional arguments are the keys of rows to be deleted, as
163 dictionaries mapping column name to value. The keys in all
164 dictionaries must be exactly the names in ``columns``.
165 """
166 raise NotImplementedError()
168 name: str
169 """The name of the logical table this instance manages (`str`).
170 """
173class OpaqueTableStorageManager(VersionedExtension):
174 """An interface that manages the opaque tables in a `Registry`.
176 `OpaqueTableStorageManager` primarily serves as a container and factory for
177 `OpaqueTableStorage` instances, which each provide access to the records
178 for a different (logical) opaque table.
180 Parameters
181 ----------
182 registry_schema_version : `VersionTuple` or `None`, optional
183 Version of registry schema.
185 Notes
186 -----
187 Opaque tables are primarily used by `Datastore` instances to manage their
188 internal data in the same database that hold the `Registry`, but are not
189 limited to this.
191 While an opaque table in a multi-layer `Registry` may in fact be the union
192 of multiple tables in different layers, we expect this to be rare, as
193 `Registry` layers will typically correspond to different leaf `Datastore`
194 instances (each with their own opaque table) in a `ChainedDatastore`.
195 """
197 def __init__(self, *, registry_schema_version: VersionTuple | None = None):
198 super().__init__(registry_schema_version=registry_schema_version)
200 @classmethod
201 @abstractmethod
202 def initialize(
203 cls, db: Database, context: StaticTablesContext, registry_schema_version: VersionTuple | None = None
204 ) -> OpaqueTableStorageManager:
205 """Construct an instance of the manager.
207 Parameters
208 ----------
209 db : `Database`
210 Interface to the underlying database engine and namespace.
211 context : `StaticTablesContext`
212 Context object obtained from `Database.declareStaticTables`; used
213 to declare any tables that should always be present in a layer
214 implemented with this manager.
215 registry_schema_version : `VersionTuple` or `None`
216 Schema version of this extension as defined in registry.
218 Returns
219 -------
220 manager : `OpaqueTableStorageManager`
221 An instance of a concrete `OpaqueTableStorageManager` subclass.
222 """
223 raise NotImplementedError()
225 def __getitem__(self, name: str) -> OpaqueTableStorage:
226 """Interface to `get` that raises `LookupError` instead of returning
227 `None` on failure.
228 """
229 r = self.get(name)
230 if r is None:
231 raise LookupError(f"No logical table with name '{name}' found.")
232 return r
234 @abstractmethod
235 def get(self, name: str) -> OpaqueTableStorage | None:
236 """Return an object that provides access to the records associated with
237 an opaque logical table.
239 Parameters
240 ----------
241 name : `str`
242 Name of the logical table.
244 Returns
245 -------
246 records : `OpaqueTableStorage` or `None`
247 The object representing the records for the given table in this
248 layer, or `None` if there are no records for that table in this
249 layer.
251 Notes
252 -----
253 Opaque tables must be registered with the layer (see `register`) by
254 the same client before they can safely be retrieved with `get`.
255 Unlike most other manager classes, the set of opaque tables cannot be
256 obtained from an existing data repository.
257 """
258 raise NotImplementedError()
260 @abstractmethod
261 def register(self, name: str, spec: TableSpec) -> OpaqueTableStorage:
262 """Ensure that this layer can hold records for the given opaque logical
263 table, creating new tables as necessary.
265 Parameters
266 ----------
267 name : `str`
268 Name of the logical table.
269 spec : `TableSpec`
270 Schema specification for the table to be created.
272 Returns
273 -------
274 records : `OpaqueTableStorage`
275 The object representing the records for the given element in this
276 layer.
278 Notes
279 -----
280 This operation may not be invoked within a transaction context block.
281 """
282 raise NotImplementedError()
284 @abstractmethod
285 def clone(self, db: Database) -> OpaqueTableStorageManager:
286 """Make an independent copy of this manager instance bound to a new
287 `Database` instance.
289 Parameters
290 ----------
291 db : `Database`
292 New `Database` object to use when instantiating the manager.
294 Returns
295 -------
296 instance : `OpaqueTableStorageManager`
297 New manager instance with the same configuration as this instance,
298 but bound to a new Database object.
299 """
300 raise NotImplementedError()