berkeleydb.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. from __future__ import annotations
  2. import logging
  3. from os import mkdir
  4. from os.path import abspath, exists
  5. from threading import Thread
  6. from typing import (
  7. TYPE_CHECKING,
  8. Any,
  9. Callable,
  10. Dict,
  11. Generator,
  12. List,
  13. Optional,
  14. Tuple,
  15. Union,
  16. )
  17. from urllib.request import pathname2url
  18. from rdflib.store import NO_STORE, VALID_STORE, Store
  19. from rdflib.term import Identifier, Node, URIRef
  20. if TYPE_CHECKING:
  21. from rdflib.graph import Graph, _ContextType, _TriplePatternType, _TripleType
  22. def bb(u: str) -> bytes:
  23. return u.encode("utf-8")
  24. try:
  25. from berkeleydb import db
  26. has_bsddb = True
  27. except ImportError:
  28. has_bsddb = False
  29. if has_bsddb:
  30. # These are passed to bsddb when creating DBs
  31. # passed to db.DBEnv.set_flags
  32. ENVSETFLAGS = db.DB_CDB_ALLDB
  33. # passed to db.DBEnv.open
  34. ENVFLAGS = db.DB_INIT_MPOOL | db.DB_INIT_CDB | db.DB_THREAD
  35. CACHESIZE = 1024 * 1024 * 50
  36. # passed to db.DB.Open()
  37. DBOPENFLAGS = db.DB_THREAD
  38. logger = logging.getLogger(__name__)
  39. __all__ = [
  40. "BerkeleyDB",
  41. "_ToKeyFunc",
  42. "_FromKeyFunc",
  43. "_GetPrefixFunc",
  44. "_ResultsFromKeyFunc",
  45. ]
  46. _ToKeyFunc = Callable[[Tuple[bytes, bytes, bytes], bytes], bytes]
  47. _FromKeyFunc = Callable[[bytes], Tuple[bytes, bytes, bytes, bytes]]
  48. _GetPrefixFunc = Callable[
  49. [Tuple[str, str, str], Optional[str]], Generator[str, None, None]
  50. ]
  51. _ResultsFromKeyFunc = Callable[
  52. [bytes, Optional[Node], Optional[Node], Optional[Node], bytes],
  53. Tuple[Tuple[Node, Node, Node], Generator[Node, None, None]],
  54. ]
  55. class BerkeleyDB(Store):
  56. """A store that allows for on-disk persistent using BerkeleyDB, a fast key/value DB.
  57. This store implementation used to be known, previous to rdflib 6.0.0
  58. as 'Sleepycat' due to that being the then name of the Python wrapper
  59. for BerkeleyDB.
  60. This store allows for quads as well as triples. See examples of use
  61. in both the `examples.berkeleydb_example` and `test/test_store/test_store_berkeleydb.py`
  62. files.
  63. **NOTE on installation**:
  64. To use this store, you must have BerkeleyDB installed on your system
  65. separately to Python (`brew install berkeley-db` on a Mac) and also have
  66. the BerkeleyDB Python wrapper installed (`pip install berkeleydb`).
  67. You may need to install BerkeleyDB Python wrapper like this:
  68. `YES_I_HAVE_THE_RIGHT_TO_USE_THIS_BERKELEY_DB_VERSION=1 pip install berkeleydb`
  69. """
  70. context_aware = True
  71. formula_aware = True
  72. transaction_aware = False
  73. graph_aware = True
  74. db_env: db.DBEnv = None
  75. def __init__(
  76. self,
  77. configuration: Optional[str] = None,
  78. identifier: Optional[Identifier] = None,
  79. ):
  80. if not has_bsddb:
  81. raise ImportError("Unable to import berkeleydb, store is unusable.")
  82. self.__open = False
  83. self.__identifier = identifier
  84. super(BerkeleyDB, self).__init__(configuration)
  85. self._loads = self.node_pickler.loads
  86. self._dumps = self.node_pickler.dumps
  87. self.__indicies_info: List[Tuple[Any, _ToKeyFunc, _FromKeyFunc]]
  88. def __get_identifier(self) -> Optional[Identifier]:
  89. return self.__identifier
  90. identifier = property(__get_identifier)
  91. def _init_db_environment(
  92. self, homeDir: str, create: bool = True # noqa: N803
  93. ) -> db.DBEnv:
  94. if not exists(homeDir):
  95. if create is True:
  96. mkdir(homeDir)
  97. # TODO: implement create method and refactor this to it
  98. self.create(homeDir)
  99. else:
  100. return NO_STORE
  101. db_env = db.DBEnv()
  102. db_env.set_cachesize(0, CACHESIZE) # TODO
  103. # db_env.set_lg_max(1024*1024)
  104. db_env.set_flags(ENVSETFLAGS, 1)
  105. db_env.open(homeDir, ENVFLAGS | db.DB_CREATE)
  106. return db_env
  107. def is_open(self) -> bool:
  108. return self.__open
  109. def open(
  110. self, configuration: Union[str, tuple[str, str]], create: bool = True
  111. ) -> Optional[int]:
  112. if not has_bsddb:
  113. return NO_STORE
  114. if type(configuration) is str:
  115. homeDir = configuration # noqa: N806
  116. else:
  117. raise Exception("Invalid configuration provided")
  118. if self.__identifier is None:
  119. self.__identifier = URIRef(pathname2url(abspath(homeDir)))
  120. db_env = self._init_db_environment(homeDir, create)
  121. if db_env == NO_STORE:
  122. return NO_STORE
  123. self.db_env = db_env
  124. self.__open = True
  125. dbname = None
  126. dbtype = db.DB_BTREE
  127. # auto-commit ensures that the open-call commits when transactions
  128. # are enabled
  129. dbopenflags = DBOPENFLAGS
  130. if self.transaction_aware is True:
  131. dbopenflags |= db.DB_AUTO_COMMIT
  132. if create:
  133. dbopenflags |= db.DB_CREATE
  134. dbmode = 0o660
  135. dbsetflags = 0
  136. # create and open the DBs
  137. self.__indicies: List[db.DB] = [
  138. None,
  139. ] * 3
  140. # NOTE on type ingore: this is because type checker does not like this
  141. # way of initializing, using a temporary variable will solve it.
  142. # type error: error: List item 0 has incompatible type "None"; expected "Tuple[Any, Callable[[Tuple[bytes, bytes, bytes], bytes], bytes], Callable[[bytes], Tuple[bytes, bytes, bytes, bytes]]]"
  143. self.__indicies_info = [
  144. None, # type: ignore[list-item]
  145. ] * 3
  146. for i in range(0, 3):
  147. index_name = to_key_func(i)(
  148. ("s".encode("latin-1"), "p".encode("latin-1"), "o".encode("latin-1")),
  149. "c".encode("latin-1"),
  150. ).decode()
  151. index = db.DB(db_env)
  152. index.set_flags(dbsetflags)
  153. index.open(index_name, dbname, dbtype, dbopenflags, dbmode)
  154. self.__indicies[i] = index
  155. self.__indicies_info[i] = (index, to_key_func(i), from_key_func(i))
  156. lookup: Dict[
  157. int, Tuple[db.DB, _GetPrefixFunc, _FromKeyFunc, _ResultsFromKeyFunc]
  158. ] = {}
  159. for i in range(0, 8):
  160. results: List[Tuple[Tuple[int, int], int, int]] = []
  161. for start in range(0, 3):
  162. score = 1
  163. len = 0
  164. for j in range(start, start + 3):
  165. if i & (1 << (j % 3)):
  166. score = score << 1
  167. len += 1
  168. else:
  169. break
  170. tie_break = 2 - start
  171. results.append(((score, tie_break), start, len))
  172. results.sort()
  173. # NOTE on type error: this is because the variable `score` is
  174. # reused with different type
  175. # type error: Incompatible types in assignment (expression has type "Tuple[int, int]", variable has type "int")
  176. score, start, len = results[-1] # type: ignore[assignment]
  177. def get_prefix_func(start: int, end: int) -> _GetPrefixFunc:
  178. def get_prefix(
  179. triple: Tuple[str, str, str], context: Optional[str]
  180. ) -> Generator[str, None, None]:
  181. if context is None:
  182. yield ""
  183. else:
  184. yield context
  185. i = start
  186. while i < end:
  187. yield triple[i % 3]
  188. i += 1
  189. yield ""
  190. return get_prefix
  191. lookup[i] = (
  192. self.__indicies[start],
  193. get_prefix_func(start, start + len),
  194. from_key_func(start),
  195. results_from_key_func(start, self._from_string),
  196. )
  197. self.__lookup_dict = lookup
  198. self.__contexts = db.DB(db_env)
  199. self.__contexts.set_flags(dbsetflags)
  200. self.__contexts.open("contexts", dbname, dbtype, dbopenflags, dbmode)
  201. self.__namespace = db.DB(db_env)
  202. self.__namespace.set_flags(dbsetflags)
  203. self.__namespace.open("namespace", dbname, dbtype, dbopenflags, dbmode)
  204. self.__prefix = db.DB(db_env)
  205. self.__prefix.set_flags(dbsetflags)
  206. self.__prefix.open("prefix", dbname, dbtype, dbopenflags, dbmode)
  207. self.__k2i = db.DB(db_env)
  208. self.__k2i.set_flags(dbsetflags)
  209. self.__k2i.open("k2i", dbname, db.DB_HASH, dbopenflags, dbmode)
  210. self.__i2k = db.DB(db_env)
  211. self.__i2k.set_flags(dbsetflags)
  212. self.__i2k.open("i2k", dbname, db.DB_RECNO, dbopenflags, dbmode)
  213. self.__needs_sync = False
  214. t = Thread(target=self.__sync_run)
  215. t.setDaemon(True)
  216. t.start()
  217. self.__sync_thread = t
  218. return VALID_STORE
  219. def __sync_run(self) -> None:
  220. from time import sleep, time
  221. try:
  222. min_seconds, max_seconds = 10, 300
  223. while self.__open:
  224. if self.__needs_sync:
  225. t0 = t1 = time()
  226. self.__needs_sync = False
  227. while self.__open:
  228. sleep(0.1)
  229. if self.__needs_sync:
  230. t1 = time()
  231. self.__needs_sync = False
  232. if time() - t1 > min_seconds or time() - t0 > max_seconds:
  233. self.__needs_sync = False
  234. logger.debug("sync")
  235. self.sync()
  236. break
  237. else:
  238. sleep(1)
  239. except Exception as e:
  240. logger.exception(e)
  241. def sync(self) -> None:
  242. if self.__open:
  243. for i in self.__indicies:
  244. i.sync()
  245. self.__contexts.sync()
  246. self.__namespace.sync()
  247. self.__prefix.sync()
  248. self.__i2k.sync()
  249. self.__k2i.sync()
  250. def close(self, commit_pending_transaction: bool = False) -> None:
  251. self.__open = False
  252. self.__sync_thread.join()
  253. for i in self.__indicies:
  254. i.close()
  255. self.__contexts.close()
  256. self.__namespace.close()
  257. self.__prefix.close()
  258. self.__i2k.close()
  259. self.__k2i.close()
  260. self.db_env.close()
  261. def add(
  262. self,
  263. triple: _TripleType,
  264. context: _ContextType,
  265. quoted: bool = False,
  266. txn: Optional[Any] = None,
  267. ) -> None:
  268. """\
  269. Add a triple to the store of triples.
  270. """
  271. (subject, predicate, object) = triple
  272. assert self.__open, "The Store must be open."
  273. assert context != self, "Can not add triple directly to store"
  274. Store.add(self, (subject, predicate, object), context, quoted)
  275. _to_string = self._to_string
  276. s = _to_string(subject, txn=txn)
  277. p = _to_string(predicate, txn=txn)
  278. o = _to_string(object, txn=txn)
  279. c = _to_string(context, txn=txn)
  280. cspo, cpos, cosp = self.__indicies
  281. value = cspo.get(bb("%s^%s^%s^%s^" % (c, s, p, o)), txn=txn)
  282. if value is None:
  283. self.__contexts.put(bb(c), b"", txn=txn)
  284. contexts_value = cspo.get(
  285. bb("%s^%s^%s^%s^" % ("", s, p, o)), txn=txn
  286. ) or "".encode("latin-1")
  287. contexts = set(contexts_value.split("^".encode("latin-1")))
  288. contexts.add(bb(c))
  289. contexts_value = "^".encode("latin-1").join(contexts)
  290. assert contexts_value is not None
  291. cspo.put(bb("%s^%s^%s^%s^" % (c, s, p, o)), b"", txn=txn)
  292. cpos.put(bb("%s^%s^%s^%s^" % (c, p, o, s)), b"", txn=txn)
  293. cosp.put(bb("%s^%s^%s^%s^" % (c, o, s, p)), b"", txn=txn)
  294. if not quoted:
  295. cspo.put(bb("%s^%s^%s^%s^" % ("", s, p, o)), contexts_value, txn=txn)
  296. cpos.put(bb("%s^%s^%s^%s^" % ("", p, o, s)), contexts_value, txn=txn)
  297. cosp.put(bb("%s^%s^%s^%s^" % ("", o, s, p)), contexts_value, txn=txn)
  298. self.__needs_sync = True
  299. def __remove(
  300. self,
  301. spo: Tuple[bytes, bytes, bytes],
  302. c: bytes,
  303. quoted: bool = False,
  304. txn: Optional[Any] = None,
  305. ) -> None:
  306. s, p, o = spo
  307. cspo, cpos, cosp = self.__indicies
  308. contexts_value = cspo.get(
  309. "^".encode("latin-1").join(
  310. ["".encode("latin-1"), s, p, o, "".encode("latin-1")]
  311. ),
  312. txn=txn,
  313. ) or "".encode("latin-1")
  314. contexts = set(contexts_value.split("^".encode("latin-1")))
  315. contexts.discard(c)
  316. contexts_value = "^".encode("latin-1").join(contexts)
  317. for i, _to_key, _from_key in self.__indicies_info:
  318. i.delete(_to_key((s, p, o), c), txn=txn)
  319. if not quoted:
  320. if contexts_value:
  321. for i, _to_key, _from_key in self.__indicies_info:
  322. i.put(
  323. _to_key((s, p, o), "".encode("latin-1")),
  324. contexts_value,
  325. txn=txn,
  326. )
  327. else:
  328. for i, _to_key, _from_key in self.__indicies_info:
  329. try:
  330. i.delete(_to_key((s, p, o), "".encode("latin-1")), txn=txn)
  331. except db.DBNotFoundError:
  332. pass # TODO: is it okay to ignore these?
  333. # type error: Signature of "remove" incompatible with supertype "Store"
  334. def remove( # type: ignore[override]
  335. self,
  336. spo: _TriplePatternType,
  337. context: Optional[_ContextType],
  338. txn: Optional[Any] = None,
  339. ) -> None:
  340. subject, predicate, object = spo
  341. assert self.__open, "The Store must be open."
  342. Store.remove(self, (subject, predicate, object), context)
  343. _to_string = self._to_string
  344. if context is not None:
  345. if context == self:
  346. context = None
  347. if (
  348. subject is not None
  349. and predicate is not None
  350. and object is not None
  351. and context is not None
  352. ):
  353. s = _to_string(subject, txn=txn)
  354. p = _to_string(predicate, txn=txn)
  355. o = _to_string(object, txn=txn)
  356. c = _to_string(context, txn=txn)
  357. value = self.__indicies[0].get(bb("%s^%s^%s^%s^" % (c, s, p, o)), txn=txn)
  358. if value is not None:
  359. self.__remove((bb(s), bb(p), bb(o)), bb(c), txn=txn)
  360. self.__needs_sync = True
  361. else:
  362. cspo, cpos, cosp = self.__indicies
  363. index, prefix, from_key, results_from_key = self.__lookup(
  364. (subject, predicate, object), context, txn=txn
  365. )
  366. cursor = index.cursor(txn=txn)
  367. try:
  368. current = cursor.set_range(prefix)
  369. needs_sync = True
  370. except db.DBNotFoundError:
  371. current = None
  372. needs_sync = False
  373. cursor.close()
  374. while current:
  375. key, value = current
  376. cursor = index.cursor(txn=txn)
  377. try:
  378. cursor.set_range(key)
  379. # Hack to stop 2to3 converting this to next(cursor)
  380. current = getattr(cursor, "next")()
  381. except db.DBNotFoundError:
  382. current = None
  383. cursor.close()
  384. if key.startswith(prefix):
  385. # NOTE on type error: variables are being reused with a
  386. # different type
  387. # type error: Incompatible types in assignment (expression has type "bytes", variable has type "str")
  388. c, s, p, o = from_key(key) # type: ignore[assignment]
  389. if context is None:
  390. contexts_value = index.get(key, txn=txn) or "".encode("latin-1")
  391. # remove triple from all non quoted contexts
  392. contexts = set(contexts_value.split("^".encode("latin-1")))
  393. # and from the conjunctive index
  394. contexts.add("".encode("latin-1"))
  395. for c in contexts:
  396. for i, _to_key, _ in self.__indicies_info:
  397. # NOTE on type error: variables are being
  398. # reused with a different type
  399. # type error: Argument 1 has incompatible type "Tuple[str, str, str]"; expected "Tuple[bytes, bytes, bytes]"
  400. # type error: Argument 2 has incompatible type "str"; expected "bytes"
  401. i.delete(_to_key((s, p, o), c), txn=txn) # type: ignore[arg-type]
  402. else:
  403. # type error: Argument 1 to "__remove" of "BerkeleyDB" has incompatible type "Tuple[str, str, str]"; expected "Tuple[bytes, bytes, bytes]"
  404. # type error: Argument 2 to "__remove" of "BerkeleyDB" has incompatible type "str"; expected "bytes"
  405. self.__remove((s, p, o), c, txn=txn) # type: ignore[arg-type]
  406. else:
  407. break
  408. if context is not None:
  409. if subject is None and predicate is None and object is None:
  410. # TODO: also if context becomes empty and not just on
  411. # remove((None, None, None), c)
  412. try:
  413. self.__contexts.delete(
  414. bb(_to_string(context, txn=txn)), txn=txn
  415. )
  416. except db.DBNotFoundError:
  417. pass
  418. self.__needs_sync = needs_sync
  419. def triples(
  420. self,
  421. spo: _TriplePatternType,
  422. context: Optional[_ContextType] = None,
  423. txn: Optional[Any] = None,
  424. ) -> Generator[
  425. Tuple[_TripleType, Generator[Optional[_ContextType], None, None]],
  426. None,
  427. None,
  428. ]:
  429. """A generator over all the triples matching"""
  430. assert self.__open, "The Store must be open."
  431. subject, predicate, object = spo
  432. if context is not None:
  433. if context == self:
  434. context = None
  435. # _from_string = self._from_string ## UNUSED
  436. index, prefix, from_key, results_from_key = self.__lookup(
  437. (subject, predicate, object), context, txn=txn
  438. )
  439. cursor = index.cursor(txn=txn)
  440. try:
  441. current = cursor.set_range(prefix)
  442. except db.DBNotFoundError:
  443. current = None
  444. cursor.close()
  445. while current:
  446. key, value = current
  447. cursor = index.cursor(txn=txn)
  448. try:
  449. cursor.set_range(key)
  450. # Cheap hack so 2to3 doesn't convert to next(cursor)
  451. current = getattr(cursor, "next")()
  452. except db.DBNotFoundError:
  453. current = None
  454. cursor.close()
  455. if key and key.startswith(prefix):
  456. contexts_value = index.get(key, txn=txn)
  457. # type error: Incompatible types in "yield" (actual type "Tuple[Tuple[Node, Node, Node], Generator[Node, None, None]]", expected type "Tuple[Tuple[IdentifiedNode, URIRef, Identifier], Iterator[Optional[Graph]]]")
  458. # NOTE on type ignore: this is needed because some context is
  459. # lost in the process of extracting triples from the database.
  460. yield results_from_key(key, subject, predicate, object, contexts_value) # type: ignore[misc]
  461. else:
  462. break
  463. def __len__(self, context: Optional[_ContextType] = None) -> int:
  464. assert self.__open, "The Store must be open."
  465. if context is not None:
  466. if context == self:
  467. context = None
  468. if context is None:
  469. prefix = "^".encode("latin-1")
  470. else:
  471. prefix = bb("%s^" % self._to_string(context))
  472. index = self.__indicies[0]
  473. cursor = index.cursor()
  474. current = cursor.set_range(prefix)
  475. count = 0
  476. while current:
  477. key, value = current
  478. if key.startswith(prefix):
  479. count += 1
  480. # Hack to stop 2to3 converting this to next(cursor)
  481. current = getattr(cursor, "next")()
  482. else:
  483. break
  484. cursor.close()
  485. return count
  486. def bind(self, prefix: str, namespace: URIRef, override: bool = True) -> None:
  487. # NOTE on type error: this is because the variables are reused with
  488. # another type.
  489. # type error: Incompatible types in assignment (expression has type "bytes", variable has type "str")
  490. prefix = prefix.encode("utf-8") # type: ignore[assignment]
  491. # type error: Incompatible types in assignment (expression has type "bytes", variable has type "URIRef")
  492. namespace = namespace.encode("utf-8") # type: ignore[assignment]
  493. bound_prefix = self.__prefix.get(namespace)
  494. bound_namespace = self.__namespace.get(prefix)
  495. if override:
  496. if bound_prefix:
  497. self.__namespace.delete(bound_prefix)
  498. if bound_namespace:
  499. self.__prefix.delete(bound_namespace)
  500. self.__prefix[namespace] = prefix
  501. self.__namespace[prefix] = namespace
  502. else:
  503. self.__prefix[bound_namespace or namespace] = bound_prefix or prefix
  504. self.__namespace[bound_prefix or prefix] = bound_namespace or namespace
  505. def namespace(self, prefix: str) -> Optional[URIRef]:
  506. # NOTE on type error: this is because the variable is reused with
  507. # another type.
  508. # type error: Incompatible types in assignment (expression has type "bytes", variable has type "str")
  509. prefix = prefix.encode("utf-8") # type: ignore[assignment]
  510. ns = self.__namespace.get(prefix, None)
  511. if ns is not None:
  512. return URIRef(ns.decode("utf-8"))
  513. return None
  514. def prefix(self, namespace: URIRef) -> Optional[str]:
  515. # NOTE on type error: this is because the variable is reused with
  516. # another type.
  517. # type error: Incompatible types in assignment (expression has type "bytes", variable has type "URIRef")
  518. namespace = namespace.encode("utf-8") # type: ignore[assignment]
  519. prefix = self.__prefix.get(namespace, None)
  520. if prefix is not None:
  521. return prefix.decode("utf-8")
  522. return None
  523. def namespaces(self) -> Generator[Tuple[str, URIRef], None, None]:
  524. cursor = self.__namespace.cursor()
  525. results = []
  526. current = cursor.first()
  527. while current:
  528. prefix, namespace = current
  529. results.append((prefix.decode("utf-8"), namespace.decode("utf-8")))
  530. # Hack to stop 2to3 converting this to next(cursor)
  531. current = getattr(cursor, "next")()
  532. cursor.close()
  533. for prefix, namespace in results:
  534. yield prefix, URIRef(namespace)
  535. def contexts(
  536. self, triple: Optional[_TripleType] = None
  537. ) -> Generator[_ContextType, None, None]:
  538. _from_string = self._from_string
  539. _to_string = self._to_string
  540. # NOTE on type errors: context is lost because of how data is loaded
  541. # from the DB.
  542. if triple:
  543. s: str
  544. p: str
  545. o: str
  546. # type error: Incompatible types in assignment (expression has type "Node", variable has type "str")
  547. s, p, o = triple # type: ignore[assignment]
  548. # type error: Argument 1 has incompatible type "str"; expected "Node"
  549. s = _to_string(s) # type: ignore[arg-type]
  550. # type error: Argument 1 has incompatible type "str"; expected "Node"
  551. p = _to_string(p) # type: ignore[arg-type]
  552. # type error: Argument 1 has incompatible type "str"; expected "Node"
  553. o = _to_string(o) # type: ignore[arg-type]
  554. contexts = self.__indicies[0].get(bb("%s^%s^%s^%s^" % ("", s, p, o)))
  555. if contexts:
  556. for c in contexts.split("^".encode("latin-1")):
  557. if c:
  558. # type error: Incompatible types in "yield" (actual type "Node", expected type "Graph")
  559. yield _from_string(c) # type: ignore[misc]
  560. else:
  561. index = self.__contexts
  562. cursor = index.cursor()
  563. current = cursor.first()
  564. cursor.close()
  565. while current:
  566. key, value = current
  567. context = _from_string(key)
  568. # type error: Incompatible types in "yield" (actual type "Node", expected type "Graph")
  569. yield context # type: ignore[misc]
  570. cursor = index.cursor()
  571. try:
  572. cursor.set_range(key)
  573. # Hack to stop 2to3 converting this to next(cursor)
  574. current = getattr(cursor, "next")()
  575. except db.DBNotFoundError:
  576. current = None
  577. cursor.close()
  578. def add_graph(self, graph: Graph) -> None:
  579. self.__contexts.put(bb(self._to_string(graph)), b"")
  580. def remove_graph(self, graph: Graph):
  581. self.remove((None, None, None), graph)
  582. def _from_string(self, i: bytes) -> Node:
  583. k = self.__i2k.get(int(i))
  584. return self._loads(k)
  585. def _to_string(self, term: Node, txn: Optional[Any] = None) -> str:
  586. k = self._dumps(term)
  587. i = self.__k2i.get(k, txn=txn)
  588. if i is None:
  589. # weird behaviour from bsddb not taking a txn as a keyword argument
  590. # for append
  591. if self.transaction_aware:
  592. i = "%s" % self.__i2k.append(k, txn)
  593. else:
  594. i = "%s" % self.__i2k.append(k)
  595. self.__k2i.put(k, i.encode(), txn=txn)
  596. else:
  597. i = i.decode()
  598. return i
  599. def __lookup(
  600. self,
  601. spo: _TriplePatternType,
  602. context: Optional[_ContextType],
  603. txn: Optional[Any] = None,
  604. ) -> Tuple[db.DB, bytes, _FromKeyFunc, _ResultsFromKeyFunc]:
  605. subject, predicate, object = spo
  606. _to_string = self._to_string
  607. # NOTE on type errors: this is because the same variable is used with different types.
  608. if context is not None:
  609. # type error: Incompatible types in assignment (expression has type "str", variable has type "Optional[Graph]")
  610. context = _to_string(context, txn=txn) # type: ignore[assignment]
  611. i = 0
  612. if subject is not None:
  613. i += 1
  614. # type error: Incompatible types in assignment (expression has type "str", variable has type "Node")
  615. subject = _to_string(subject, txn=txn) # type: ignore[assignment]
  616. if predicate is not None:
  617. i += 2
  618. # type error: Incompatible types in assignment (expression has type "str", variable has type "Node")
  619. predicate = _to_string(predicate, txn=txn) # type: ignore[assignment]
  620. if object is not None:
  621. i += 4
  622. # type error: Incompatible types in assignment (expression has type "str", variable has type "Node")
  623. object = _to_string(object, txn=txn) # type: ignore[assignment]
  624. index, prefix_func, from_key, results_from_key = self.__lookup_dict[i]
  625. # print (subject, predicate, object), context, prefix_func, index
  626. # #DEBUG
  627. # type error: Argument 1 has incompatible type "Tuple[Node, Node, Node]"; expected "Tuple[str, str, str]"
  628. # type error: Argument 2 has incompatible type "Optional[Graph]"; expected "Optional[str]"
  629. prefix = bb("^".join(prefix_func((subject, predicate, object), context))) # type: ignore[arg-type]
  630. return index, prefix, from_key, results_from_key
  631. def to_key_func(i: int) -> _ToKeyFunc:
  632. def to_key(triple: Tuple[bytes, bytes, bytes], context: bytes) -> bytes:
  633. "Takes a string; returns key"
  634. return "^".encode("latin-1").join(
  635. (
  636. context,
  637. triple[i % 3],
  638. triple[(i + 1) % 3],
  639. triple[(i + 2) % 3],
  640. "".encode("latin-1"),
  641. )
  642. ) # "" to tac on the trailing ^
  643. return to_key
  644. def from_key_func(i: int) -> _FromKeyFunc:
  645. def from_key(key: bytes) -> Tuple[bytes, bytes, bytes, bytes]:
  646. "Takes a key; returns string"
  647. parts = key.split("^".encode("latin-1"))
  648. return (
  649. parts[0],
  650. parts[(3 - i + 0) % 3 + 1],
  651. parts[(3 - i + 1) % 3 + 1],
  652. parts[(3 - i + 2) % 3 + 1],
  653. )
  654. return from_key
  655. def results_from_key_func(
  656. i: int, from_string: Callable[[bytes], Node]
  657. ) -> _ResultsFromKeyFunc:
  658. def from_key(
  659. key: bytes,
  660. subject: Optional[Node],
  661. predicate: Optional[Node],
  662. object: Optional[Node],
  663. contexts_value: bytes,
  664. ) -> Tuple[Tuple[Node, Node, Node], Generator[Node, None, None]]:
  665. "Takes a key and subject, predicate, object; returns tuple for yield"
  666. parts = key.split("^".encode("latin-1"))
  667. if subject is None:
  668. # TODO: i & 1: # dis assemble and/or measure to see which is faster
  669. # subject is None or i & 1
  670. s = from_string(parts[(3 - i + 0) % 3 + 1])
  671. else:
  672. s = subject
  673. if predicate is None: # i & 2:
  674. p = from_string(parts[(3 - i + 1) % 3 + 1])
  675. else:
  676. p = predicate
  677. if object is None: # i & 4:
  678. o = from_string(parts[(3 - i + 2) % 3 + 1])
  679. else:
  680. o = object
  681. return (
  682. (s, p, o),
  683. (from_string(c) for c in contexts_value.split("^".encode("latin-1")) if c),
  684. )
  685. return from_key
  686. # TODO: Remove unused
  687. def readable_index(i: int) -> str:
  688. # type error: Unpacking a string is disallowed
  689. s, p, o = "?" * 3 # type: ignore[misc]
  690. if i & 1:
  691. s = "s"
  692. if i & 2:
  693. p = "p"
  694. if i & 4:
  695. o = "o"
  696. return "%s,%s,%s" % (s, p, o)