Coverage for python/lsst/daf/butler/registry/opaque.py: 33%

72 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-06 10:53 +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/>. 

27 

28"""The default concrete implementations of the classes that manage 

29opaque tables for `Registry`. 

30""" 

31 

32from __future__ import annotations 

33 

34__all__ = ["ByNameOpaqueTableStorage", "ByNameOpaqueTableStorageManager"] 

35 

36import itertools 

37from collections.abc import Iterable, Iterator 

38from typing import TYPE_CHECKING, Any, ClassVar 

39 

40import sqlalchemy 

41 

42from ..ddl import FieldSpec, TableSpec 

43from .interfaces import ( 

44 Database, 

45 OpaqueTableStorage, 

46 OpaqueTableStorageManager, 

47 StaticTablesContext, 

48 VersionTuple, 

49) 

50 

51if TYPE_CHECKING: 

52 from ..datastore import DatastoreTransaction 

53 

54# This has to be updated on every schema change 

55_VERSION = VersionTuple(0, 2, 0) 

56 

57 

58class ByNameOpaqueTableStorage(OpaqueTableStorage): 

59 """An implementation of `OpaqueTableStorage` that simply creates a true 

60 table for each different named opaque logical table. 

61 

62 A `ByNameOpaqueTableStorageManager` instance should always be used to 

63 construct and manage instances of this class. 

64 

65 Parameters 

66 ---------- 

67 db : `Database` 

68 Database engine interface for the namespace in which this table lives. 

69 name : `str` 

70 Name of the logical table (also used as the name of the actual table). 

71 table : `sqlalchemy.schema.Table` 

72 SQLAlchemy representation of the table, which must have already been 

73 created in the namespace managed by ``db`` (this is the responsibility 

74 of `ByNameOpaqueTableStorageManager`). 

75 """ 

76 

77 def __init__(self, *, db: Database, name: str, table: sqlalchemy.schema.Table): 

78 super().__init__(name=name) 

79 self._db = db 

80 self._table = table 

81 

82 def insert(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None: 

83 # Docstring inherited from OpaqueTableStorage. 

84 # The provided transaction object can be ignored since we rely on 

85 # the database itself providing any rollback functionality. 

86 self._db.insert(self._table, *data) 

87 

88 def ensure(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None: 

89 # Docstring inherited from OpaqueTableStorage. 

90 # The provided transaction object can be ignored since we rely on 

91 # the database itself providing any rollback functionality. 

92 self._db.ensure(self._table, *data) 

93 

94 def replace(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None: 

95 # Docstring inherited from OpaqueTableStorage. 

96 # The provided transaction object can be ignored since we rely on 

97 # the database itself providing any rollback functionality. 

98 self._db.replace(self._table, *data) 

99 

100 def fetch(self, **where: Any) -> Iterator[sqlalchemy.RowMapping]: 

101 # Docstring inherited from OpaqueTableStorage. 

102 

103 def _batch_in_clause( 

104 column: sqlalchemy.schema.Column, values: Iterable[Any] 

105 ) -> Iterator[sqlalchemy.sql.expression.ClauseElement]: 

106 """Split one long IN clause into a series of shorter ones.""" 

107 in_limit = 1000 

108 # We have to remove possible duplicates from values; and in many 

109 # cases it should be helpful to order the items in the clause. 

110 values = sorted(set(values)) 

111 for iposn in range(0, len(values), in_limit): 

112 in_clause = column.in_(values[iposn : iposn + in_limit]) 

113 yield in_clause 

114 

115 def _batch_in_clauses(**where: Any) -> Iterator[sqlalchemy.sql.expression.ColumnElement]: 

116 """Generate a sequence of WHERE clauses with a limited number of 

117 items in IN clauses. 

118 """ 

119 batches: list[Iterable[Any]] = [] 

120 for k, v in where.items(): 

121 column = self._table.columns[k] 

122 if isinstance(v, list | tuple | set): 

123 batches.append(_batch_in_clause(column, v)) 

124 else: 

125 # single "batch" for a regular eq operator 

126 batches.append([column == v]) 

127 

128 for clauses in itertools.product(*batches): 

129 yield sqlalchemy.sql.and_(*clauses) 

130 

131 sql = self._table.select() 

132 if where: 

133 # Split long IN clauses into shorter batches 

134 batched_sql = [sql.where(clause) for clause in _batch_in_clauses(**where)] 

135 else: 

136 batched_sql = [sql] 

137 for sql_batch in batched_sql: 

138 with self._db.query(sql_batch) as sql_result: 

139 sql_mappings = sql_result.mappings().fetchall() 

140 yield from sql_mappings 

141 

142 def delete(self, columns: Iterable[str], *rows: dict) -> None: 

143 # Docstring inherited from OpaqueTableStorage. 

144 self._db.delete(self._table, columns, *rows) 

145 

146 

147class ByNameOpaqueTableStorageManager(OpaqueTableStorageManager): 

148 """An implementation of `OpaqueTableStorageManager` that simply creates a 

149 true table for each different named opaque logical table. 

150 

151 Instances of this class should generally be constructed via the 

152 `initialize` class method instead of invoking ``__init__`` directly. 

153 

154 Parameters 

155 ---------- 

156 db : `Database` 

157 Database engine interface for the namespace in which this table lives. 

158 metaTable : `sqlalchemy.schema.Table` 

159 SQLAlchemy representation of the table that records which opaque 

160 logical tables exist. 

161 """ 

162 

163 def __init__( 

164 self, 

165 db: Database, 

166 metaTable: sqlalchemy.schema.Table, 

167 registry_schema_version: VersionTuple | None = None, 

168 ): 

169 super().__init__(registry_schema_version=registry_schema_version) 

170 self._db = db 

171 self._metaTable = metaTable 

172 self._storage: dict[str, OpaqueTableStorage] = {} 

173 

174 _META_TABLE_NAME: ClassVar[str] = "opaque_meta" 

175 

176 _META_TABLE_SPEC: ClassVar[TableSpec] = TableSpec( 

177 fields=[ 

178 FieldSpec("table_name", dtype=sqlalchemy.String, length=128, primaryKey=True), 

179 ], 

180 ) 

181 

182 @classmethod 

183 def initialize( 

184 cls, db: Database, context: StaticTablesContext, registry_schema_version: VersionTuple | None = None 

185 ) -> OpaqueTableStorageManager: 

186 # Docstring inherited from OpaqueTableStorageManager. 

187 metaTable = context.addTable(cls._META_TABLE_NAME, cls._META_TABLE_SPEC) 

188 return cls(db=db, metaTable=metaTable, registry_schema_version=registry_schema_version) 

189 

190 def get(self, name: str) -> OpaqueTableStorage | None: 

191 # Docstring inherited from OpaqueTableStorageManager. 

192 return self._storage.get(name) 

193 

194 def register(self, name: str, spec: TableSpec) -> OpaqueTableStorage: 

195 # Docstring inherited from OpaqueTableStorageManager. 

196 result = self._storage.get(name) 

197 if result is None: 

198 # Create the table itself. If it already exists but wasn't in 

199 # the dict because it was added by another client since this one 

200 # was initialized, that's fine. 

201 table = self._db.ensureTableExists(name, spec) 

202 # Add a row to the meta table so we can find this table in the 

203 # future. Also okay if that already exists, so we use sync. 

204 self._db.sync(self._metaTable, keys={"table_name": name}) 

205 result = ByNameOpaqueTableStorage(name=name, table=table, db=self._db) 

206 self._storage[name] = result 

207 return result 

208 

209 @classmethod 

210 def currentVersions(cls) -> list[VersionTuple]: 

211 # Docstring inherited from VersionedExtension. 

212 return [_VERSION]