Coverage for python/lsst/dax/apdb/sql/apdbSqlReplica.py: 33%
96 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:35 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:35 -0700
1# This file is part of dax_apdb.
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/>.
22"""Module defining Apdb class and related methods."""
24from __future__ import annotations
26__all__ = ["ApdbSqlReplica"]
28import logging
29from collections.abc import Collection, Iterable, Mapping, Sequence
30from typing import TYPE_CHECKING, cast
32import astropy.time
33import felis.datamodel
34import sqlalchemy
35from sqlalchemy import sql
37from ..apdbReplica import ApdbReplica, ApdbTableData, ReplicaChunk
38from ..apdbSchema import ApdbTables
39from ..monitor import MonAgent
40from ..schema_model import ExtraDataTypes
41from ..timer import Timer
42from ..versionTuple import VersionTuple
43from .apdbSqlSchema import ExtraTables
45if TYPE_CHECKING:
46 from .apdbSqlSchema import ApdbSqlSchema
49_LOG = logging.getLogger(__name__)
51_MON = MonAgent(__name__)
53VERSION = VersionTuple(1, 0, 0)
54"""Version for the code controlling replication tables. This needs to be
55updated following compatibility rules when schema produced by this code
56changes.
57"""
60class ApdbSqlTableData(ApdbTableData):
61 """Implementation of ApdbTableData that wraps sqlalchemy Result."""
63 def __init__(self, result: sqlalchemy.engine.Result, column_types: dict[str, felis.datamodel.DataType]):
64 self._column_defs = tuple((column, column_types[column]) for column in result.keys())
65 self._rows: list[tuple] = cast(list[tuple], list(result.fetchall()))
67 def column_names(self) -> Sequence[str]:
68 return tuple(column_def[0] for column_def in self._column_defs)
70 def column_defs(self) -> Sequence[tuple[str, felis.datamodel.DataType]]:
71 return self._column_defs
73 def rows(self) -> Collection[tuple]:
74 return self._rows
77class ApdbSqlReplica(ApdbReplica):
78 """Implementation of `ApdbReplica` for SQL backend.
80 Parameters
81 ----------
82 schema : `ApdbSqlSchema`
83 Instance of `ApdbSqlSchema` class for APDB database.
84 engine : `sqlalchemy.engine.Engine`
85 Engine for database access.
86 db_schema_version : `VersionTuple`
87 Version of the database schema.
88 timer : `bool`, optional
89 If `True` then log timing information.
90 """
92 def __init__(
93 self,
94 schema: ApdbSqlSchema,
95 engine: sqlalchemy.engine.Engine,
96 db_schema_version: VersionTuple,
97 timer: bool = False,
98 ):
99 self._schema = schema
100 self._engine = engine
101 self._db_schema_version = db_schema_version
103 self._timer_args: list[MonAgent | logging.Logger] = [_MON]
104 if timer:
105 self._timer_args.append(_LOG)
107 def _timer(self, name: str, *, tags: Mapping[str, str | int] | None = None) -> Timer:
108 """Create `Timer` instance given its name."""
109 return Timer(name, *self._timer_args, tags=tags)
111 def schemaVersion(self) -> VersionTuple:
112 # Docstring inherited from base class.
113 return self._db_schema_version
115 @classmethod
116 def apdbReplicaImplementationVersion(cls) -> VersionTuple:
117 # Docstring inherited from base class.
118 return VERSION
120 def getReplicaChunks(self) -> list[ReplicaChunk] | None:
121 # docstring is inherited from a base class
122 if not self._schema.replication_enabled:
123 return None
125 table = self._schema.get_table(ExtraTables.ApdbReplicaChunks)
126 assert table is not None, "replication_enabled=True means it must be defined"
127 query = sql.select(
128 table.columns["apdb_replica_chunk"], table.columns["last_update_time"], table.columns["unique_id"]
129 ).order_by(table.columns["last_update_time"])
130 with self._timer("chunks_select_time") as timer:
131 with self._engine.connect() as conn:
132 result = conn.execution_options(stream_results=True, max_row_buffer=10000).execute(query)
133 ids = []
134 for row in result:
135 last_update_time = astropy.time.Time(row[1].timestamp(), format="unix_tai")
136 ids.append(ReplicaChunk(id=row[0], last_update_time=last_update_time, unique_id=row[2]))
137 timer.add_values(row_count=len(ids))
138 return ids
140 def deleteReplicaChunks(self, chunks: Iterable[int]) -> None:
141 # docstring is inherited from a base class
142 if not self._schema.replication_enabled:
143 raise ValueError("APDB is not configured for replication")
145 table = self._schema.get_table(ExtraTables.ApdbReplicaChunks)
146 chunk_list = list(chunks)
147 where_clause = table.columns["apdb_replica_chunk"].in_(chunk_list)
148 stmt = table.delete().where(where_clause)
149 with self._timer("chunks_delete_time") as timer:
150 with self._engine.begin() as conn:
151 conn.execute(stmt)
152 timer.add_values(row_count=len(chunk_list))
154 def getTableDataChunks(self, table: ApdbTables, chunks: Iterable[int]) -> ApdbTableData:
155 # docstring is inherited from a base class
156 for chunk_table, table_enum in ExtraTables.replica_chunk_tables().items():
157 if table is table_enum:
158 return self._get_chunks(chunks, table, chunk_table)
159 raise ValueError(f"Table {table} does not support replica chunks.")
161 def _get_chunks(
162 self,
163 chunks: Iterable[int],
164 table_enum: ApdbTables,
165 chunk_table_enum: ExtraTables,
166 ) -> ApdbTableData:
167 """Return catalog of records for given insert identifiers, common
168 implementation for all DIA tables.
169 """
170 if not self._schema.replication_enabled:
171 raise ValueError("APDB is not configured for replication")
173 table = self._schema.get_table(table_enum)
174 chunk_table = self._schema.get_table(chunk_table_enum)
176 join = table.join(chunk_table)
177 chunk_id_column = chunk_table.columns["apdb_replica_chunk"]
178 apdb_columns = self._schema.get_apdb_columns(table_enum)
179 where_clause = chunk_id_column.in_(chunks)
180 query = sql.select(chunk_id_column, *apdb_columns).select_from(join).where(where_clause)
182 table_schema = self._schema.tableSchemas[table_enum]
183 # Regular tables should never have columns of ExtraDataTypes, this is
184 # just to make mypy happy.
185 column_types = {
186 column.name: column.datatype
187 for column in table_schema.columns
188 if not isinstance(column.datatype, ExtraDataTypes)
189 }
190 column_types["apdb_replica_chunk"] = felis.datamodel.DataType.long
192 # execute select
193 with self._timer("table_chunk_select_time", tags={"table": table.name}) as timer:
194 with self._engine.begin() as conn:
195 result = conn.execution_options(stream_results=True, max_row_buffer=10000).execute(query)
196 table_data = ApdbSqlTableData(result, column_types)
197 timer.add_values(row_count=len(table_data.rows()))
198 return table_data
200 def getTableUpdateChunks(self, table: ApdbTables, chunks: Iterable[int]) -> ApdbTableData:
201 # docstring is inherited from a base class
202 raise NotImplementedError()