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

65 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-24 10:51 +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 TYPE_CHECKING, Any 

28 

29from .apdbMetadata import ApdbMetadata 

30from .apdbSchema import ApdbTables 

31from .cassandra_utils import PreparedStatementCache, quote_id 

32 

33if TYPE_CHECKING: 33 ↛ 34line 33 didn't jump to line 34, because the condition on line 33 was never true

34 from .apdbCassandra import ApdbCassandraConfig 

35 from .apdbCassandraSchema import ApdbCassandraSchema 

36 

37 

38class ApdbMetadataCassandra(ApdbMetadata): 

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

40 

41 Parameters 

42 ---------- 

43 session : `cassandra.cluster.Session` 

44 Cassandra session instance. 

45 schema : `ApdbSqlSchema` 

46 Object providing access to schema details. 

47 """ 

48 

49 def __init__(self, session: Any, schema: ApdbCassandraSchema, config: ApdbCassandraConfig): 

50 self._session = session 

51 self._config = config 

52 self._part = 0 # Partition for all rows 

53 self._preparer = PreparedStatementCache(session) 

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

55 self._table_clause: str | None = None 

56 if ApdbTables.metadata in schema.tableSchemas: 

57 table_name = schema.tableName(ApdbTables.metadata) 

58 keyspace = schema.keyspace() 

59 if not keyspace: 

60 self._table_clause = quote_id(table_name) 

61 else: 

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

63 

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

65 # Docstring is inherited. 

66 if self._table_clause is None: 

67 return default 

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

69 result = self._session.execute( 

70 self._preparer.prepare(query), 

71 (self._part, key), 

72 timeout=self._config.read_timeout, 

73 execution_profile="read_tuples", 

74 ) 

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

76 return row[0] 

77 else: 

78 return default 

79 

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

81 # Docstring is inherited. 

82 if self._table_clause is None: 

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

84 if not key or not value: 

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

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

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

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

89 # Race is still possible between check and insert. 

90 self._session.execute( 

91 self._preparer.prepare(query), 

92 (self._part, key, value), 

93 timeout=self._config.write_timeout, 

94 execution_profile="write", 

95 ) 

96 

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

98 # Docstring is inherited. 

99 if self._table_clause is None: 

100 # Missing table means nothing to delete. 

101 return False 

102 if not key: 

103 raise ValueError("name cannot be empty") 

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

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

106 # exists now. 

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

108 # Race is still possible between check and remove. 

109 self._session.execute( 

110 self._preparer.prepare(query), 

111 (self._part, key), 

112 timeout=self._config.write_timeout, 

113 execution_profile="write", 

114 ) 

115 return exists 

116 

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

118 # Docstring is inherited. 

119 if self._table_clause is None: 

120 # Missing table means nothing to return. 

121 return 

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

123 result = self._session.execute( 

124 self._preparer.prepare(query), 

125 (self._part,), 

126 timeout=self._config.read_timeout, 

127 execution_profile="read_tuples", 

128 ) 

129 for row in result: 

130 yield tuple(row) 

131 

132 def empty(self) -> bool: 

133 # Docstring is inherited. 

134 if self._table_clause is None: 

135 # Missing table means empty. 

136 return True 

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

138 result = self._session.execute( 

139 self._preparer.prepare(query), 

140 (self._part,), 

141 timeout=self._config.read_timeout, 

142 execution_profile="read_tuples", 

143 ) 

144 row = result.one() 

145 return row[0] == 0 

146 

147 def table_exists(self) -> bool: 

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

149 return self._table_clause is not None