Coverage for python/lsst/dax/apdb/apdbMetadataSql.py: 14%

61 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-01-31 10:44 +0000

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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ["ApdbMetadataSql"] 

25 

26from collections.abc import Generator 

27 

28import sqlalchemy 

29 

30from .apdbMetadata import ApdbMetadata 

31 

32 

33class ApdbMetadataSql(ApdbMetadata): 

34 """Implementation of `ApdbMetadata` for SQL backend. 

35 

36 Parameters 

37 ---------- 

38 engine : `sqlalchemy.engine.Engine` 

39 Database access engine. 

40 table : `sqlalchemy.schema.Table` or `None` 

41 Database table holding metadata. If table does not exists then `None` 

42 should be specified. 

43 """ 

44 

45 def __init__(self, engine: sqlalchemy.engine.Engine, table: sqlalchemy.schema.Table | None): 

46 self._engine = engine 

47 self._table = table 

48 

49 def get(self, key: str, default: str | None = None) -> str | None: 

50 # Docstring is inherited. 

51 if self._table is None: 

52 return default 

53 sql = sqlalchemy.sql.select(self._table.columns.value).where(self._table.columns.name == key) 

54 with self._engine.begin() as conn: 

55 result = conn.execute(sql) 

56 value = result.scalar() 

57 if value is not None: 

58 return value 

59 return default 

60 

61 def set(self, key: str, value: str, *, force: bool = False) -> None: 

62 # Docstring is inherited. 

63 if self._table is None: 

64 raise RuntimeError("Metadata table does not exist") 

65 if not key or not value: 

66 raise ValueError("name and value cannot be empty") 

67 try: 

68 insert = sqlalchemy.sql.insert(self._table).values(name=key, value=value) 

69 with self._engine.begin() as conn: 

70 conn.execute(insert) 

71 except sqlalchemy.exc.IntegrityError as exc: 

72 # Try to update if it exists. 

73 if not force: 

74 raise KeyError(f"Metadata key {key!r} already exists") from exc 

75 update = ( 

76 sqlalchemy.sql.update(self._table).where(self._table.columns.name == key).values(value=value) 

77 ) 

78 with self._engine.begin() as conn: 

79 result = conn.execute(update) 

80 if result.rowcount != 1: 

81 raise RuntimeError(f"Metadata update failed unexpectedly, count={result.rowcount}") 

82 

83 def delete(self, key: str) -> bool: 

84 # Docstring is inherited. 

85 if self._table is None: 

86 # Missing table means nothing to delete. 

87 return False 

88 stmt = sqlalchemy.sql.delete(self._table).where(self._table.columns.name == key) 

89 with self._engine.begin() as conn: 

90 result = conn.execute(stmt) 

91 return result.rowcount > 0 

92 

93 def items(self) -> Generator[tuple[str, str], None, None]: 

94 # Docstring is inherited. 

95 if self._table is None: 

96 # Missing table means nothing to return. 

97 return 

98 stmt = sqlalchemy.sql.select(self._table.columns.name, self._table.columns.value) 

99 with self._engine.begin() as conn: 

100 result = conn.execute(stmt) 

101 for row in result: 

102 yield row._tuple() 

103 

104 def empty(self) -> bool: 

105 # Docstring is inherited. 

106 if self._table is None: 

107 # Missing table means empty. 

108 return True 

109 stmt = sqlalchemy.sql.select(sqlalchemy.sql.func.count()).select_from(self._table) 

110 with self._engine.begin() as conn: 

111 result = conn.execute(stmt) 

112 count = result.scalar() 

113 return count == 0 

114 

115 def table_exists(self) -> bool: 

116 """Return `True` if metadata table exists.""" 

117 return self._table is not None