Coverage for python/lsst/dax/apdb/apdbMetadataCassandra.py: 17%

61 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-10 10:38 +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__ = ["ApdbMetadataCassandra"] 

25 

26from collections.abc import Generator 

27from typing import Any 

28 

29from .apdbMetadata import ApdbMetadata 

30from .cassandra_utils import PreparedStatementCache, quote_id 

31 

32 

33class ApdbMetadataCassandra(ApdbMetadata): 

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

35 

36 Parameters 

37 ---------- 

38 session : `cassandra.cluster.Session` 

39 Cassandra session instance. 

40 schema : `ApdbSqlSchema` 

41 Object providing access to schema details. 

42 """ 

43 

44 def __init__(self, session: Any, table_name: str, keyspace: str, read_profile: str, write_profile: str): 

45 self._session = session 

46 self._read_profile = read_profile 

47 self._write_profile = write_profile 

48 self._part = 0 # Partition for all rows 

49 self._preparer = PreparedStatementCache(session) 

50 # _table_clause will be None when metadata table is not configured 

51 self._table_clause: str | None = None 

52 

53 query = "SELECT count(*) FROM system_schema.tables WHERE keyspace_name = %s and table_name = %s" 

54 result = self._session.execute(query, (keyspace, table_name), execution_profile=read_profile) 

55 exists = bool(result.one()[0]) 

56 if exists: 

57 self._table_clause = f"{quote_id(keyspace)}.{quote_id(table_name)}" 

58 

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

60 # Docstring is inherited. 

61 if self._table_clause is None: 

62 return default 

63 query = f"SELECT value FROM {self._table_clause} WHERE meta_part = ? AND name = ?" 

64 result = self._session.execute( 

65 self._preparer.prepare(query), (self._part, key), execution_profile=self._read_profile 

66 ) 

67 if (row := result.one()) is not None: 

68 return row[0] 

69 else: 

70 return default 

71 

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

73 # Docstring is inherited. 

74 if self._table_clause is None: 

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

76 if not key or not value: 

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

78 query = f"INSERT INTO {self._table_clause} (meta_part, name, value) VALUES (?, ?, ?)" 

79 if not force and self.get(key) is not None: 

80 raise KeyError(f"Metadata key {key!r} already exists") 

81 # Race is still possible between check and insert. 

82 self._session.execute( 

83 self._preparer.prepare(query), (self._part, key, value), execution_profile=self._write_profile 

84 ) 

85 

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

87 # Docstring is inherited. 

88 if self._table_clause is None: 

89 # Missing table means nothing to delete. 

90 return False 

91 if not key: 

92 raise ValueError("name cannot be empty") 

93 query = f"DELETE FROM {self._table_clause} WHERE meta_part = ? AND name = ?" 

94 # Cassandra cannot tell how many rows are deleted, just check if row 

95 # exists now. 

96 exists = self.get(key) is not None 

97 # Race is still possible between check and remove. 

98 self._session.execute( 

99 self._preparer.prepare(query), (self._part, key), execution_profile=self._write_profile 

100 ) 

101 return exists 

102 

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

104 # Docstring is inherited. 

105 if self._table_clause is None: 

106 # Missing table means nothing to return. 

107 return 

108 query = f"SELECT name, value FROM {self._table_clause} WHERE meta_part = ?" 

109 result = self._session.execute( 

110 self._preparer.prepare(query), (self._part,), execution_profile=self._read_profile 

111 ) 

112 for row in result: 

113 yield tuple(row) 

114 

115 def empty(self) -> bool: 

116 # Docstring is inherited. 

117 if self._table_clause is None: 

118 # Missing table means empty. 

119 return True 

120 query = f"SELECT count(*) FROM {self._table_clause} WHERE meta_part = ?" 

121 result = self._session.execute( 

122 self._preparer.prepare(query), (self._part,), execution_profile=self._read_profile 

123 ) 

124 row = result.one() 

125 return row[0] == 0 

126 

127 def table_exists(self) -> bool: 

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

129 return self._table_clause is not None