auditable.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """
  2. This wrapper intercepts calls through the store interface and implements
  3. thread-safe logging of destructive operations (adds / removes) in reverse.
  4. This is persisted on the store instance and the reverse operations are
  5. executed In order to return the store to the state it was when the transaction
  6. began Since the reverse operations are persisted on the store, the store
  7. itself acts as a transaction.
  8. Calls to commit or rollback, flush the list of reverse operations This
  9. provides thread-safe atomicity and isolation (assuming concurrent operations
  10. occur with different store instances), but no durability (transactions are
  11. persisted in memory and won't be available to reverse operations after the
  12. system fails): A and I out of ACID.
  13. """
  14. from __future__ import annotations
  15. import threading
  16. from typing import TYPE_CHECKING, Any, Generator, Iterator, List, Optional, Tuple, Union
  17. from rdflib.graph import ConjunctiveGraph, Graph
  18. from rdflib.store import Store
  19. if TYPE_CHECKING:
  20. from rdflib.graph import (
  21. _ContextIdentifierType,
  22. _ContextType,
  23. _ObjectType,
  24. _PredicateType,
  25. _SubjectType,
  26. _TriplePatternType,
  27. _TripleType,
  28. )
  29. from rdflib.query import Result
  30. from rdflib.term import URIRef
  31. destructiveOpLocks = { # noqa: N816
  32. "add": None,
  33. "remove": None,
  34. }
  35. class AuditableStore(Store):
  36. """A store that logs destructive operations (add/remove) in reverse order."""
  37. def __init__(self, store: Store):
  38. self.store = store
  39. self.context_aware = store.context_aware
  40. # NOTE: this store can't be formula_aware as it doesn't have enough
  41. # info to reverse the removal of a quoted statement
  42. self.formula_aware = False # store.formula_aware
  43. self.transaction_aware = True # This is only half true
  44. self.reverseOps: List[
  45. Tuple[
  46. Optional[_SubjectType],
  47. Optional[_PredicateType],
  48. Optional[_ObjectType],
  49. Optional[_ContextIdentifierType],
  50. str,
  51. ]
  52. ] = []
  53. self.rollbackLock = threading.RLock()
  54. def open(
  55. self, configuration: Union[str, tuple[str, str]], create: bool = True
  56. ) -> Optional[int]:
  57. return self.store.open(configuration, create)
  58. def close(self, commit_pending_transaction: bool = False) -> None:
  59. self.store.close()
  60. def destroy(self, configuration: str) -> None:
  61. self.store.destroy(configuration)
  62. def query(self, *args: Any, **kw: Any) -> Result:
  63. return self.store.query(*args, **kw)
  64. def add(
  65. self, triple: _TripleType, context: _ContextType, quoted: bool = False
  66. ) -> None:
  67. (s, p, o) = triple
  68. lock = destructiveOpLocks["add"]
  69. lock = lock if lock else threading.RLock()
  70. with lock:
  71. context = (
  72. context.__class__(self.store, context.identifier)
  73. if context is not None
  74. else None
  75. )
  76. ctxId = context.identifier if context is not None else None # noqa: N806
  77. if list(self.store.triples(triple, context)):
  78. return # triple already in store, do nothing
  79. self.reverseOps.append((s, p, o, ctxId, "remove"))
  80. try:
  81. self.reverseOps.remove((s, p, o, ctxId, "add"))
  82. except ValueError:
  83. pass
  84. self.store.add((s, p, o), context, quoted)
  85. def remove(
  86. self, spo: _TriplePatternType, context: Optional[_ContextType] = None
  87. ) -> None:
  88. subject, predicate, object_ = spo
  89. lock = destructiveOpLocks["remove"]
  90. lock = lock if lock else threading.RLock()
  91. with lock:
  92. # Need to determine which quads will be removed if any term is a
  93. # wildcard
  94. context = (
  95. context.__class__(self.store, context.identifier)
  96. if context is not None
  97. else None
  98. )
  99. ctxId = context.identifier if context is not None else None # noqa: N806
  100. if None in [subject, predicate, object_, context]:
  101. if ctxId:
  102. # type error: Item "None" of "Optional[Graph]" has no attribute "triples"
  103. for s, p, o in context.triples((subject, predicate, object_)): # type: ignore[union-attr]
  104. try:
  105. self.reverseOps.remove((s, p, o, ctxId, "remove"))
  106. except ValueError:
  107. self.reverseOps.append((s, p, o, ctxId, "add"))
  108. else:
  109. for s, p, o, ctx in ConjunctiveGraph(self.store).quads(
  110. (subject, predicate, object_)
  111. ):
  112. try:
  113. # type error: Item "None" of "Optional[Graph]" has no attribute "identifier"
  114. self.reverseOps.remove((s, p, o, ctx.identifier, "remove")) # type: ignore[union-attr]
  115. except ValueError:
  116. # type error: Item "None" of "Optional[Graph]" has no attribute "identifier"
  117. self.reverseOps.append((s, p, o, ctx.identifier, "add")) # type: ignore[union-attr]
  118. else:
  119. if not list(self.triples((subject, predicate, object_), context)):
  120. return # triple not present in store, do nothing
  121. try:
  122. self.reverseOps.remove(
  123. (subject, predicate, object_, ctxId, "remove")
  124. )
  125. except ValueError:
  126. self.reverseOps.append((subject, predicate, object_, ctxId, "add"))
  127. self.store.remove((subject, predicate, object_), context)
  128. def triples(
  129. self, triple: _TriplePatternType, context: Optional[_ContextType] = None
  130. ) -> Iterator[Tuple[_TripleType, Iterator[Optional[_ContextType]]]]:
  131. (su, pr, ob) = triple
  132. context = (
  133. context.__class__(self.store, context.identifier)
  134. if context is not None
  135. else None
  136. )
  137. for (s, p, o), cg in self.store.triples((su, pr, ob), context):
  138. yield (s, p, o), cg
  139. def __len__(self, context: Optional[_ContextType] = None):
  140. context = (
  141. context.__class__(self.store, context.identifier)
  142. if context is not None
  143. else None
  144. )
  145. return self.store.__len__(context)
  146. def contexts(
  147. self, triple: Optional[_TripleType] = None
  148. ) -> Generator[_ContextType, None, None]:
  149. for ctx in self.store.contexts(triple):
  150. yield ctx
  151. def bind(self, prefix: str, namespace: URIRef, override: bool = True) -> None:
  152. self.store.bind(prefix, namespace, override=override)
  153. def prefix(self, namespace: URIRef) -> Optional[str]:
  154. return self.store.prefix(namespace)
  155. def namespace(self, prefix: str) -> Optional[URIRef]:
  156. return self.store.namespace(prefix)
  157. def namespaces(self) -> Iterator[Tuple[str, URIRef]]:
  158. return self.store.namespaces()
  159. def commit(self) -> None:
  160. self.reverseOps = []
  161. def rollback(self) -> None:
  162. # Acquire Rollback lock and apply reverse operations in the forward
  163. # order
  164. with self.rollbackLock:
  165. for subject, predicate, obj, context, op in self.reverseOps:
  166. if op == "add":
  167. # type error: Argument 2 to "Graph" has incompatible type "Optional[Node]"; expected "Union[IdentifiedNode, str, None]"
  168. self.store.add(
  169. (subject, predicate, obj), Graph(self.store, context) # type: ignore[arg-type]
  170. )
  171. else:
  172. self.store.remove(
  173. (subject, predicate, obj), Graph(self.store, context)
  174. )
  175. self.reverseOps = []