Browse Source

Merge branch 'master' into development

Stefano Cossu 5 years ago
parent
commit
8d9b863ac9
60 changed files with 3832 additions and 1429 deletions
  1. 16 1
      .gitignore
  2. 3 2
      .gitmodules
  3. 3 1
      .travis.yml
  4. 3 0
      MANIFEST.in
  5. 23 22
      README.rst
  6. 1 1
      docs/api.rst
  7. 4 4
      docs/apidoc/lakesuperior.model.rst
  8. 99 0
      docs/structures.rst
  9. 1 0
      ext/collections-c
  10. 9 8
      lakesuperior/api/admin.py
  11. 40 41
      lakesuperior/api/resource.py
  12. 282 0
      lakesuperior/cy_include/collections.pxd
  13. 20 0
      lakesuperior/cy_include/spookyhash.pxd
  14. 64 51
      lakesuperior/endpoints/ldp.py
  15. 0 0
      lakesuperior/model/__init__.pxd
  16. 29 0
      lakesuperior/model/base.pxd
  17. 11 0
      lakesuperior/model/base.pyx
  18. 30 0
      lakesuperior/model/callbacks.pxd
  19. 54 0
      lakesuperior/model/callbacks.pyx
  20. 11 11
      lakesuperior/model/ldp/ldp_factory.py
  21. 2 2
      lakesuperior/model/ldp/ldp_nr.py
  22. 1 1
      lakesuperior/model/ldp/ldp_rs.py
  23. 57 46
      lakesuperior/model/ldp/ldpr.py
  24. 0 0
      lakesuperior/model/rdf/__init__.pxd
  25. 0 0
      lakesuperior/model/rdf/__init__.py
  26. 37 0
      lakesuperior/model/rdf/graph.pxd
  27. 613 0
      lakesuperior/model/rdf/graph.pyx
  28. 41 0
      lakesuperior/model/rdf/term.pxd
  29. 191 0
      lakesuperior/model/rdf/term.pyx
  30. 19 0
      lakesuperior/model/rdf/triple.pxd
  31. 41 0
      lakesuperior/model/rdf/triple.pyx
  32. 0 0
      lakesuperior/model/structures/__init__.pxd
  33. 0 0
      lakesuperior/model/structures/__init__.py
  34. 34 0
      lakesuperior/model/structures/hash.pxd
  35. 53 0
      lakesuperior/model/structures/hash.pyx
  36. 37 0
      lakesuperior/model/structures/keyset.pxd
  37. 364 0
      lakesuperior/model/structures/keyset.pyx
  38. 2 1
      lakesuperior/store/base_lmdb_store.pxd
  39. 50 5
      lakesuperior/store/base_lmdb_store.pyx
  40. 0 0
      lakesuperior/store/ldp_rs/__init__.pxd
  41. 0 53
      lakesuperior/store/ldp_rs/lmdb_store.py
  42. 45 0
      lakesuperior/store/ldp_rs/lmdb_triplestore.pxd
  43. 106 746
      lakesuperior/store/ldp_rs/lmdb_triplestore.pyx
  44. 47 39
      lakesuperior/store/ldp_rs/rsrc_centric_layout.py
  45. 0 41
      lakesuperior/store/ldp_rs/term.pxd
  46. 0 134
      lakesuperior/store/ldp_rs/term.pyx
  47. 13 0
      lakesuperior/toolbox.py
  48. 112 29
      lakesuperior/util/benchmark.py
  49. 2 1
      requirements_dev.txt
  50. 15 0
      sandbox/NOTES
  51. 10 0
      sandbox/txn_openLogic.txt
  52. 112 46
      setup.py
  53. 850 0
      tests/0_data_structures/test_0_0_graph.py
  54. 121 29
      tests/1_store/test_1_0_lmdb_store.py
  55. 138 106
      tests/2_api/test_2_0_resource_api.py
  56. 11 6
      tests/2_api/test_2_1_admin_api.py
  57. 1 1
      tests/3_endpoints/test_3_0_ldp.py
  58. 4 1
      tests/3_endpoints/test_3_1_admin.py
  59. 0 0
      tests/3_endpoints/test_3_2_query.py
  60. 0 0
      tests/4_ancillary/test_4_0_toolbox.py

+ 16 - 1
.gitignore

@@ -107,9 +107,24 @@ venv.bak/
 .pytest_cache/
 .pytest_cache/
 
 
 # Default Lakesuperior data directories
 # Default Lakesuperior data directories
-/data
+lakesuperior/data/ldprs_store
+lakesuperior/data/ldpnr_store
 
 
 # Cython business.
 # Cython business.
+/cython_debug
 /lakesuperior/store/*.c
 /lakesuperior/store/*.c
+/lakesuperior/store/*.html
 /lakesuperior/store/ldp_rs/*.c
 /lakesuperior/store/ldp_rs/*.c
+/lakesuperior/store/ldp_rs/*.html
+/lakesuperior/model/*.c
+/lakesuperior/model/*/*.html
+/lakesuperior/model/*/*.c
+/lakesuperior/model/*.html
+/lakesuperior/util/*.c
+/lakesuperior/util/*.html
 !ext/lib
 !ext/lib
+
+# Vim CTags file.
+tags
+
+!.keep

+ 3 - 2
.gitmodules

@@ -1,11 +1,12 @@
 [submodule "ext/lmdb"]
 [submodule "ext/lmdb"]
     path = ext/lmdb
     path = ext/lmdb
     url = https://github.com/LMDB/lmdb.git
     url = https://github.com/LMDB/lmdb.git
-    branch = stable
 [submodule "ext/tpl"]
 [submodule "ext/tpl"]
     path = ext/tpl
     path = ext/tpl
     url = https://github.com/troydhanson/tpl.git
     url = https://github.com/troydhanson/tpl.git
-    branch = stable
 [submodule "ext/spookyhash"]
 [submodule "ext/spookyhash"]
     path = ext/spookyhash
     path = ext/spookyhash
     url = https://github.com/centaurean/spookyhash.git
     url = https://github.com/centaurean/spookyhash.git
+[submodule "ext/collections-c"]
+    path = ext/collections-c
+    url = https://github.com/srdja/Collections-C.git

+ 3 - 1
.travis.yml

@@ -3,12 +3,14 @@ language: python
 matrix:
 matrix:
     include:
     include:
     - python: 3.6
     - python: 3.6
+      dist: xenial
+      sudo: true
     - python: 3.7
     - python: 3.7
       dist: xenial
       dist: xenial
       sudo: true
       sudo: true
 
 
 install:
 install:
-  - pip install Cython==0.29
+  - pip install Cython==0.29.6 cymem
   - pip install -e .
   - pip install -e .
 script:
 script:
   - python setup.py test
   - python setup.py test

+ 3 - 0
MANIFEST.in

@@ -5,10 +5,13 @@ include ext/lmdb/libraries/liblmdb/mdb.c
 include ext/lmdb/libraries/liblmdb/lmdb.h
 include ext/lmdb/libraries/liblmdb/lmdb.h
 include ext/lmdb/libraries/liblmdb/midl.c
 include ext/lmdb/libraries/liblmdb/midl.c
 include ext/lmdb/libraries/liblmdb/midl.h
 include ext/lmdb/libraries/liblmdb/midl.h
+include ext/collections-c/src/*.c
+include ext/collections-c/src/include/*.h
 include ext/tpl/src/tpl.c
 include ext/tpl/src/tpl.c
 include ext/tpl/src/tpl.h
 include ext/tpl/src/tpl.h
 include ext/spookyhash/src/*.c
 include ext/spookyhash/src/*.c
 include ext/spookyhash/src/*.h
 include ext/spookyhash/src/*.h
+
 graft lakesuperior/data/bootstrap
 graft lakesuperior/data/bootstrap
 graft lakesuperior/endpoints/templates
 graft lakesuperior/endpoints/templates
 graft lakesuperior/etc.defaults
 graft lakesuperior/etc.defaults

+ 23 - 22
README.rst

@@ -3,43 +3,44 @@ Lakesuperior
 
 
 |build status| |docs| |pypi| |codecov|
 |build status| |docs| |pypi| |codecov|
 
 
-Lakesuperior is an alternative `Fedora
-Repository <http://fedorarepository.org>`__ implementation.
+Lakesuperior is a Linked Data repository software. It is capable of storing and
+managing  large volumes of files and their metadata regardless of their
+format, size, ethnicity, gender identity or expression.
 
 
-Fedora is a mature repository software system historically adopted by
-major cultural heritage institutions. It exposes an
-`LDP <https://www.w3.org/TR/ldp-primer/>`__ endpoint to manage
-any type of binary files and their metadata in Linked Data format.
+Lakesuperior is an alternative `Fedora Repository
+<http://fedorarepository.org>`__ implementation. Fedora is a mature repository
+software system historically adopted by major cultural heritage institutions
+which extends the `Linked Data Platform <https://www.w3.org/TR/ldp-primer/>`__
+protocol.
 
 
 Guiding Principles
 Guiding Principles
 ------------------
 ------------------
 
 
-Lakesuperior aims at being an uncomplicated, efficient Fedora 4
-implementation.
+Lakesuperior aims at being a reliable and efficient Fedora 4 implementation.
 
 
 Its main goals are:
 Its main goals are:
 
 
 -  **Reliability:** Based on solid technologies with stability in mind.
 -  **Reliability:** Based on solid technologies with stability in mind.
 -  **Efficiency:** Small memory and CPU footprint, high scalability.
 -  **Efficiency:** Small memory and CPU footprint, high scalability.
--  **Ease of management:** Tools to perform monitoring and maintenance
-   included.
+-  **Ease of management:** Tools to perform migration, monitoring and
+   maintenance included.
 -  **Simplicity of design:** Straight-forward architecture, robustness
 -  **Simplicity of design:** Straight-forward architecture, robustness
    over features.
    over features.
 
 
 Key features
 Key features
 ------------
 ------------
 
 
--  Drop-in replacement for Fedora4
--  Very stable persistence layer based on
-   `LMDB <https://symas.com/lmdb/>`__ and filesystem. Fully
-   ACID-compliant writes guarantee consistency of data.
--  Term-based search and SPARQL Query API + UI
--  No performance penalty for storing many resources under the same
-   container, or having one resource link to many URIs
--  Extensible provenance metadata tracking
--  Multi-modal access: HTTP (REST), command line interface and native Python
-   API.
--  Fits in a pocket: you can carry 50M triples in an 8Gb memory stick.
+- Stores binary files and RDF metadata in one repository.
+- Multi-modal access: REST/LDP, command line and native Python API.
+- (`almost <fcrepo4_deltas>`_) Drop-in replacement for Fedora4
+- Very stable persistence layer based on
+  `LMDB <https://symas.com/lmdb/>`__ and filesystem. Fully
+  ACID-compliant writes guarantee consistency of data.
+- Term-based search and SPARQL Query API + UI
+- No performance penalty for storing many resources under the same
+  container, or having one resource link to many URIs
+- Extensible provenance metadata tracking
+- Fits in a pocket: you can carry 50M triples in an 8Gb memory stick.
 
 
 Installation & Documentation
 Installation & Documentation
 ----------------------------
 ----------------------------
@@ -50,7 +51,7 @@ With Docker::
     cd lakesuperior
     cd lakesuperior
     docker-compose up
     docker-compose up
 
 
-With pip (assuming you are familiar with it)::
+With pip (requires a C compiler to be installed)::
 
 
     pip install lakesuperior
     pip install lakesuperior
 
 

+ 1 - 1
docs/api.rst

@@ -10,7 +10,7 @@ The Lakesuperior API modules of most interest for a client are:
 - :mod:`lakesupeiror.api.query`
 - :mod:`lakesupeiror.api.query`
 - :mod:`lakesuperior.api.admin`
 - :mod:`lakesuperior.api.admin`
 
 
-:mod:`lakesuperior.model.ldpr` is used to manipulate resources.
+:mod:`lakesuperior.model.ldp.ldpr` is used to manipulate resources.
 
 
 The full API docs are listed below.
 The full API docs are listed below.
 
 

+ 4 - 4
docs/apidoc/lakesuperior.model.rst

@@ -7,7 +7,7 @@ Submodules
 lakesuperior\.model\.ldp\_factory module
 lakesuperior\.model\.ldp\_factory module
 ----------------------------------------
 ----------------------------------------
 
 
-.. automodule:: lakesuperior.model.ldp_factory
+.. automodule:: lakesuperior.model.ldp.ldp_factory
     :members:
     :members:
     :undoc-members:
     :undoc-members:
     :show-inheritance:
     :show-inheritance:
@@ -15,7 +15,7 @@ lakesuperior\.model\.ldp\_factory module
 lakesuperior\.model\.ldp\_nr module
 lakesuperior\.model\.ldp\_nr module
 -----------------------------------
 -----------------------------------
 
 
-.. automodule:: lakesuperior.model.ldp_nr
+.. automodule:: lakesuperior.model.ldp.ldp_nr
     :members:
     :members:
     :undoc-members:
     :undoc-members:
     :show-inheritance:
     :show-inheritance:
@@ -23,7 +23,7 @@ lakesuperior\.model\.ldp\_nr module
 lakesuperior\.model\.ldp\_rs module
 lakesuperior\.model\.ldp\_rs module
 -----------------------------------
 -----------------------------------
 
 
-.. automodule:: lakesuperior.model.ldp_rs
+.. automodule:: lakesuperior.model.ldp.ldp_rs
     :members:
     :members:
     :undoc-members:
     :undoc-members:
     :show-inheritance:
     :show-inheritance:
@@ -31,7 +31,7 @@ lakesuperior\.model\.ldp\_rs module
 lakesuperior\.model\.ldpr module
 lakesuperior\.model\.ldpr module
 --------------------------------
 --------------------------------
 
 
-.. automodule:: lakesuperior.model.ldpr
+.. automodule:: lakesuperior.model.ldp.ldpr
     :members:
     :members:
     :undoc-members:
     :undoc-members:
     :show-inheritance:
     :show-inheritance:

+ 99 - 0
docs/structures.rst

@@ -0,0 +1,99 @@
+Data Structure Internals
+========================
+
+**(Draft)**
+
+Lakesuperior has its own methods for handling in-memory graphs. These methods
+rely on C data structures and are therefore much faster than Python/RDFLib
+objects.
+
+The graph data model modules are in :py:module:`lakesuperior.model.graph`.
+
+The Graph Data Model
+--------------------
+
+Triples are stored in a C hash set. Each triple is represented by a pointer to
+a ``BufferTriple`` structure stored in a temporary memory pool. This pool is
+tied to the life cycle of the ``SimpleGraph`` object it belongs to.
+
+A triple structure contains three pointers to ``Buffer`` structures, which
+contain a serialized version of a RDF term. These structures are stored in the
+``SimpleGraph`` memory pool as well.
+
+Each ``SimpleGraph`` object has a ``_terms`` property and a ``_triples``
+property. These are C hash sets holding addresses of unique terms and
+triples inserted in the graph. If the same term is entered more than once,
+in any position in any triple, the first one entered is used and is pointed to
+by the triple. This makes the graph data structure very compact.
+
+In summary, the pointers can be represented this way::
+
+   <serialized term data in mem pool (x3)>
+         ^      ^      ^
+         |      |      |
+   <Term structures in mem pool (x3)>
+         ^      ^      ^
+         |      |      |
+   <Term struct addresses in _terms set (x3)>
+         ^      ^      ^
+         |      |      |
+   <Triple structure in mem pool>
+         ^
+         |
+   <address of triple in _triples set>
+
+Let's say we insert the following triples in a ``SimpleGraph``::
+
+   <urn:s:0> <urn:p:0> <urn:o:0>
+   <urn:s:0> <urn:p:1> <urn:o:1>
+   <urn:s:0> <urn:p:1> <urn:o:2>
+   <urn:s:0> <urn:p:0> <urn:o:0>
+
+The memory pool contains the following byte arrays  of raw data, displayed in
+the following list with their relative addresses (simplified to 8-bit
+addresses and fixed-length byte strings for readability)::
+
+   0x00     <urn:s:0>
+   0x09     <urn:p:0>
+   0x12     <urn:o:0>
+
+   0x1b     <urn:s:0>
+   0x24     <urn:p:1>
+   0x2d     <urn:o:1>
+
+   0x36     <urn:s:0>
+   0x3f     <urn:p:1>
+   0x48     <urn:o:2>
+
+   0x51     <urn:s:0>
+   0x5a     <urn:p:0>
+   0x63     <urn:o:0>
+
+However, the ``_terms`` set contains only ``Buffer`` structures pointing to
+unique addresses::
+
+   0x00
+   0x09
+   0x12
+   0x24
+   0x2d
+   0x48
+
+The other terms are just unutilized. They will be deallocated en masse when
+the ``SimpleGraph`` object is garbage collected.
+
+The ``_triples`` set would then contain 3 unique entries pointing to the unique
+term addresses::
+
+   0x00  0x09  0x12
+   0x00  0x24  0x2d
+   0x00  0x24  0x48
+
+(the actual addresses would actually belong to the structures pointing to the
+raw data, but this is just an illustrative example).
+
+The advantage of this approach is that the memory pool is contiguous and
+append-only (until it gets purged), so it's cheap to just add to it, while the
+sets that must maintain uniqueness and are the ones that most operations
+(lookup, adding, removing, slicing, copying, etc.) are done on, contain much
+less data and are therefore faster.

+ 1 - 0
ext/collections-c

@@ -0,0 +1 @@
+Subproject commit 719fd8c229b2fb369e9fc175fc97ad658108438c

+ 9 - 8
lakesuperior/api/admin.py

@@ -77,18 +77,19 @@ def fixity_check(uid):
         resource is not an LDP-NR.
         resource is not an LDP-NR.
     """
     """
     from lakesuperior.api import resource as rsrc_api
     from lakesuperior.api import resource as rsrc_api
-    from lakesuperior.model.ldp_factory import LDP_NR_TYPE
+    from lakesuperior.model.ldp.ldp_factory import LDP_NR_TYPE
 
 
     rsrc = rsrc_api.get(uid)
     rsrc = rsrc_api.get(uid)
-    if LDP_NR_TYPE not in rsrc.ldp_types:
-        raise IncompatibleLdpTypeError()
+    with env.app_globals.rdf_store.txn_ctx():
+        if LDP_NR_TYPE not in rsrc.ldp_types:
+            raise IncompatibleLdpTypeError()
 
 
-    ref_digest_term = rsrc.metadata.value(nsc['premis'].hasMessageDigest)
-    ref_digest_parts = ref_digest_term.split(':')
-    ref_cksum = ref_digest_parts[-1]
-    ref_cksum_algo = ref_digest_parts[-2]
+        ref_digest_term = rsrc.metadata.value(nsc['premis'].hasMessageDigest)
+        ref_digest_parts = ref_digest_term.split(':')
+        ref_cksum = ref_digest_parts[-1]
+        ref_cksum_algo = ref_digest_parts[-2]
 
 
-    calc_cksum = hashlib.new(ref_cksum_algo, rsrc.content.read()).hexdigest()
+        calc_cksum = hashlib.new(ref_cksum_algo, rsrc.content.read()).hexdigest()
 
 
     if calc_cksum != ref_cksum:
     if calc_cksum != ref_cksum:
         raise ChecksumValidationError(uid, ref_cksum, calc_cksum)
         raise ChecksumValidationError(uid, ref_cksum, calc_cksum)

+ 40 - 41
lakesuperior/api/resource.py

@@ -7,7 +7,7 @@ from threading import Lock, Thread
 
 
 import arrow
 import arrow
 
 
-from rdflib import Graph, Literal, URIRef
+from rdflib import Literal
 from rdflib.namespace import XSD
 from rdflib.namespace import XSD
 
 
 from lakesuperior.config_parser import config
 from lakesuperior.config_parser import config
@@ -15,8 +15,7 @@ from lakesuperior.exceptions import (
         InvalidResourceError, ResourceNotExistsError, TombstoneError)
         InvalidResourceError, ResourceNotExistsError, TombstoneError)
 from lakesuperior import env, thread_env
 from lakesuperior import env, thread_env
 from lakesuperior.globals import RES_DELETED, RES_UPDATED
 from lakesuperior.globals import RES_DELETED, RES_UPDATED
-from lakesuperior.model.ldp_factory import LDP_NR_TYPE, LdpFactory
-from lakesuperior.store.ldp_rs.lmdb_triplestore import SimpleGraph
+from lakesuperior.model.ldp.ldp_factory import LDP_NR_TYPE, LdpFactory
 
 
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -24,36 +23,36 @@ logger = logging.getLogger(__name__)
 __doc__ = """
 __doc__ = """
 Primary API for resource manipulation.
 Primary API for resource manipulation.
 
 
-Quickstart:
-
->>> # First import default configuration and globals—only done once.
->>> import lakesuperior.default_env
->>> from lakesuperior.api import resource
->>> # Get root resource.
->>> rsrc = resource.get('/')
->>> # Dump graph.
->>> set(rsrc.imr)
-{(rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://purl.org/dc/terms/title'),
-  rdflib.term.Literal('Repository Root')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#Container')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#RepositoryRoot')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#Resource')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://www.w3.org/ns/ldp#BasicContainer')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://www.w3.org/ns/ldp#Container')),
- (rdflib.term.URIRef('info:fcres/'),
-  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
-  rdflib.term.URIRef('http://www.w3.org/ns/ldp#RDFSource'))}
+Quickstart::
+
+    >>> # First import default configuration and globals—only done once.
+    >>> import lakesuperior.default_env
+    >>> from lakesuperior.api import resource
+    >>> # Get root resource.
+    >>> rsrc = resource.get('/')
+    >>> # Dump graph.
+    >>> set(rsrc.imr)
+    {(rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://purl.org/dc/terms/title'),
+      rdflib.term.Literal('Repository Root')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#Container')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#RepositoryRoot')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://fedora.info/definitions/v4/repository#Resource')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://www.w3.org/ns/ldp#BasicContainer')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://www.w3.org/ns/ldp#Container')),
+     (rdflib.term.URIRef('info:fcres/'),
+      rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
+      rdflib.term.URIRef('http://www.w3.org/ns/ldp#RDFSource'))}
 """
 """
 
 
 def transaction(write=False):
 def transaction(write=False):
@@ -200,13 +199,13 @@ def create(parent, slug=None, **kwargs):
     :param str parent: UID of the parent resource.
     :param str parent: UID of the parent resource.
     :param str slug: Tentative path relative to the parent UID.
     :param str slug: Tentative path relative to the parent UID.
     :param \*\*kwargs: Other parameters are passed to the
     :param \*\*kwargs: Other parameters are passed to the
-      :py:meth:`~lakesuperior.model.ldp_factory.LdpFactory.from_provided`
+      :py:meth:`~lakesuperior.model.ldp.ldp_factory.LdpFactory.from_provided`
       method.
       method.
 
 
-    :rtype: tuple(str, lakesuperior.model.ldpr.Ldpr)
+    :rtype: tuple(str, lakesuperior.model.ldp.ldpr.Ldpr)
     :return: A tuple of:
     :return: A tuple of:
         1. Event type (str): whether the resource was created or updated.
         1. Event type (str): whether the resource was created or updated.
-        2. Resource (lakesuperior.model.ldpr.Ldpr): The new or updated resource.
+        2. Resource (lakesuperior.model.ldp.ldpr.Ldpr): The new or updated resource.
     """
     """
     uid = LdpFactory.mint_uid(parent, slug)
     uid = LdpFactory.mint_uid(parent, slug)
     logger.debug('Minted UID for new resource: {}'.format(uid))
     logger.debug('Minted UID for new resource: {}'.format(uid))
@@ -224,13 +223,13 @@ def create_or_replace(uid, **kwargs):
 
 
     :param string uid: UID of the resource to be created or updated.
     :param string uid: UID of the resource to be created or updated.
     :param \*\*kwargs: Other parameters are passed to the
     :param \*\*kwargs: Other parameters are passed to the
-        :py:meth:`~lakesuperior.model.ldp_factory.LdpFactory.from_provided`
+        :py:meth:`~lakesuperior.model.ldp.ldp_factory.LdpFactory.from_provided`
         method.
         method.
 
 
-    :rtype: tuple(str, lakesuperior.model.ldpr.Ldpr)
+    :rtype: tuple(str, lakesuperior.model.ldp.ldpr.Ldpr)
     :return: A tuple of:
     :return: A tuple of:
         1. Event type (str): whether the resource was created or updated.
         1. Event type (str): whether the resource was created or updated.
-        2. Resource (lakesuperior.model.ldpr.Ldpr): The new or updated resource.
+        2. Resource (lakesuperior.model.ldp.ldpr.Ldpr): The new or updated resource.
     """
     """
     rsrc = LdpFactory.from_provided(uid, **kwargs)
     rsrc = LdpFactory.from_provided(uid, **kwargs)
     return rsrc.create_or_replace(), rsrc
     return rsrc.create_or_replace(), rsrc
@@ -274,8 +273,8 @@ def update_delta(uid, remove_trp, add_trp):
         add, as 3-tuples of RDFLib terms.
         add, as 3-tuples of RDFLib terms.
     """
     """
     rsrc = LdpFactory.from_stored(uid)
     rsrc = LdpFactory.from_stored(uid)
-    remove_trp = rsrc.check_mgd_terms(SimpleGraph(remove_trp))
-    add_trp = rsrc.check_mgd_terms(SimpleGraph(add_trp))
+    remove_trp = rsrc.check_mgd_terms(remove_trp)
+    add_trp = rsrc.check_mgd_terms(add_trp)
 
 
     return rsrc.modify(RES_UPDATED, remove_trp, add_trp)
     return rsrc.modify(RES_UPDATED, remove_trp, add_trp)
 
 

+ 282 - 0
lakesuperior/cy_include/collections.pxd

@@ -0,0 +1,282 @@
+from libc.stdint cimport uint32_t
+
+ctypedef void* (*mem_alloc_ft)(size_t size)
+ctypedef void* (*mem_calloc_ft)(size_t blocks, size_t size)
+ctypedef void (*mem_free_ft)(void* block)
+ctypedef size_t (*hash_ft)(const void* key, int l, uint32_t seed)
+ctypedef int (*key_compare_ft)(const void* key1, const void* key2)
+
+
+cdef extern from "common.h":
+
+    enum cc_stat:
+        CC_OK
+        CC_ERR_ALLOC
+        CC_ERR_INVALID_CAPACITY
+        CC_ERR_INVALID_RANGE
+        CC_ERR_MAX_CAPACITY
+        CC_ERR_KEY_NOT_FOUND
+        CC_ERR_VALUE_NOT_FOUND
+        CC_ERR_OUT_OF_RANGE
+        CC_ITER_END
+
+    key_compare_ft CC_CMP_STRING
+    key_compare_ft CC_CMP_POINTER
+#
+#    int cc_common_cmp_str(const void* key1, const void* key2)
+#
+#    int cc_common_cmp_ptr(const void* key1, const void* key2)
+
+cdef extern from "array.h":
+
+    ctypedef struct Array:
+        pass
+
+    ctypedef struct ArrayConf:
+        size_t          capacity
+        float           exp_factor
+        mem_alloc_ft  mem_alloc
+        mem_calloc_ft mem_calloc
+        mem_free_ft   mem_free
+
+    ctypedef struct ArrayIter:
+        Array* ar
+        size_t index
+        bint last_removed
+
+#    ctypedef struct ArrayZipIter:
+#        Array* ar1
+#        Array* ar2
+#        size_t index
+#        bint last_removed
+#
+    cc_stat array_new(Array** out)
+
+    cc_stat array_new_conf(ArrayConf* conf, Array** out)
+
+    void array_conf_init(ArrayConf* conf)
+
+    void array_destroy(Array* ar)
+
+#    ctypedef void (*_array_destroy_cb_cb_ft)(void*)
+#
+#    void array_destroy_cb(Array* ar, _array_destroy_cb_cb_ft cb)
+#
+    cc_stat array_add(Array* ar, void* element)
+#
+#    #cc_stat array_add_at(Array* ar, void* element, size_t index)
+#
+#    cc_stat array_replace_at(Array* ar, void* element, size_t index, void** out)
+#
+#    cc_stat array_swap_at(Array* ar, size_t index1, size_t index2)
+#
+#    cc_stat array_remove(Array* ar, void* element, void** out)
+#
+#    cc_stat array_remove_at(Array* ar, size_t index, void** out)
+#
+#    cc_stat array_remove_last(Array* ar, void** out)
+#
+#    void array_remove_all(Array* ar)
+#
+#    void array_remove_all_free(Array* ar)
+#
+#    cc_stat array_get_at(Array* ar, size_t index, void** out)
+#
+#    cc_stat array_get_last(Array* ar, void** out)
+#
+#    cc_stat array_subarray(Array* ar, size_t from_, size_t to, Array** out)
+#
+#    cc_stat array_copy_shallow(Array* ar, Array** out)
+#
+#    ctypedef void* (*_array_copy_deep_cp_ft)(void*)
+#
+#    cc_stat array_copy_deep(Array* ar, _array_copy_deep_cp_ft cp, Array** out)
+#
+#    void array_reverse(Array* ar)
+#
+#    cc_stat array_trim_capacity(Array* ar)
+#
+#    size_t array_contains(Array* ar, void* element)
+#
+#    ctypedef int (*_array_contains_value_cmp_ft)(void*, void*)
+#
+#    size_t array_contains_value(Array* ar, void* element, _array_contains_value_cmp_ft cmp)
+#
+#    size_t array_size(Array* ar)
+#
+#    size_t array_capacity(Array* ar)
+#
+#    cc_stat array_index_of(Array* ar, void* element, size_t* index)
+#
+#    ctypedef int (*_array_sort_cmp_ft)(void*, void*)
+#
+#    void array_sort(Array* ar, _array_sort_cmp_ft cmp)
+#
+#    ctypedef void (*_array_map_fn_ft)(void*)
+#
+#    void array_map(Array* ar, _array_map_fn_ft fn)
+#
+#    ctypedef void (*_array_reduce_fn_ft)(void*, void*, void*)
+#
+#    void array_reduce(Array* ar, _array_reduce_fn_ft fn, void* result)
+#
+#    ctypedef bint (*_array_filter_mut_predicate_ft)(void*)
+#
+#    cc_stat array_filter_mut(Array* ar, _array_filter_mut_predicate_ft predicate)
+#
+#    ctypedef bint (*_array_filter_predicate_ft)(void*)
+#
+#    cc_stat array_filter(Array* ar, _array_filter_predicate_ft predicate, Array** out)
+#
+    void array_iter_init(ArrayIter* iter, Array* ar)
+
+    cc_stat array_iter_next(ArrayIter* iter, void** out)
+#
+#    cc_stat array_iter_remove(ArrayIter* iter, void** out)
+#
+#    cc_stat array_iter_add(ArrayIter* iter, void* element)
+#
+#    cc_stat array_iter_replace(ArrayIter* iter, void* element, void** out)
+#
+#    size_t array_iter_index(ArrayIter* iter)
+#
+#    void array_zip_iter_init(ArrayZipIter* iter, Array* a1, Array* a2)
+#
+#    cc_stat array_zip_iter_next(ArrayZipIter* iter, void** out1, void** out2)
+#
+#    cc_stat array_zip_iter_add(ArrayZipIter* iter, void* e1, void* e2)
+#
+#    cc_stat array_zip_iter_remove(ArrayZipIter* iter, void** out1, void** out2)
+#
+#    cc_stat array_zip_iter_replace(ArrayZipIter* iter, void* e1, void* e2, void** out1, void** out2)
+#
+#    size_t array_zip_iter_index(ArrayZipIter* iter)
+#
+#    void** array_get_buffer(Array* ar)
+
+
+cdef extern from "hashtable.h":
+
+    ctypedef struct TableEntry:
+        void*       key
+        void*       value
+        size_t      hash
+        TableEntry* next
+
+    ctypedef struct HashTable:
+        pass
+
+    ctypedef struct HashTableConf:
+        float               load_factor
+        size_t              initial_capacity
+        int                 key_length
+        uint32_t            hash_seed
+
+        hash_ft           hash
+        key_compare_ft    key_compare
+        mem_alloc_ft  mem_alloc
+        mem_calloc_ft mem_calloc
+        mem_free_ft   mem_free
+
+    ctypedef struct HashTableIter:
+        HashTable* table
+        size_t bucket_index
+        TableEntry* prev_entry
+        TableEntry* next_entry
+
+    hash_ft GENERAL_HASH
+    hash_ft STRING_HASH
+    hash_ft POINTER_HASH
+
+#    size_t get_table_index(HashTable *table, void *key)
+#
+#    void hashtable_conf_init(HashTableConf* conf)
+#
+#    cc_stat hashtable_new(HashTable** out)
+#
+#    cc_stat hashtable_new_conf(HashTableConf* conf, HashTable** out)
+#
+#    void hashtable_destroy(HashTable* table)
+#
+#    cc_stat hashtable_add(HashTable* table, void* key, void* val)
+#
+#    cc_stat hashtable_get(HashTable* table, void* key, void** out)
+#
+#    cc_stat hashtable_remove(HashTable* table, void* key, void** out)
+#
+#    void hashtable_remove_all(HashTable* table)
+#
+#    bint hashtable_contains_key(HashTable* table, void* key)
+#
+#    size_t hashtable_size(HashTable* table)
+#
+#    size_t hashtable_capacity(HashTable* table)
+#
+#    cc_stat hashtable_get_keys(HashTable* table, Array** out)
+#
+#    cc_stat hashtable_get_values(HashTable* table, Array** out)
+#
+    size_t hashtable_hash_string(void* key, int len, uint32_t seed)
+
+    size_t hashtable_hash(void* key, int len, uint32_t seed)
+
+    size_t hashtable_hash_ptr(void* key, int len, uint32_t seed)
+#
+#    ctypedef void (*_hashtable_foreach_key_op_ft)(void*)
+#
+#    void hashtable_foreach_key(HashTable* table, _hashtable_foreach_key_op_ft op)
+#
+#    ctypedef void (*_hashtable_foreach_value_op_ft)(void*)
+#
+#    void hashtable_foreach_value(HashTable* table, _hashtable_foreach_value_op_ft op)
+#
+#    void hashtable_iter_init(HashTableIter* iter, HashTable* table)
+#
+#    cc_stat hashtable_iter_next(HashTableIter* iter, TableEntry** out)
+#
+#    cc_stat hashtable_iter_remove(HashTableIter* iter, void** out)
+
+
+cdef extern from "hashset.h":
+
+    ctypedef struct HashSet:
+        pass
+
+    ctypedef HashTableConf HashSetConf
+
+    ctypedef struct HashSetIter:
+        HashTableIter iter
+
+    void hashset_conf_init(HashSetConf* conf)
+
+    cc_stat hashset_new(HashSet** hs)
+
+    cc_stat hashset_new_conf(HashSetConf* conf, HashSet** hs)
+
+    void hashset_destroy(HashSet* set)
+
+    cc_stat hashset_add(HashSet* set, void* element)
+
+    cc_stat hashset_add_or_get(HashSet* set, void** element)
+
+    cc_stat hashset_remove(HashSet* set, void* element, void** out)
+
+    void hashset_remove_all(HashSet* set)
+
+    bint hashset_contains(HashSet* set, void* element)
+
+    cc_stat hashset_get(HashSet *set, void **element)
+
+    size_t hashset_size(HashSet* set)
+
+    size_t hashset_capacity(HashSet* set)
+
+    ctypedef void (*_hashset_foreach_op_ft)(void*)
+
+    void hashset_foreach(HashSet* set, _hashset_foreach_op_ft op)
+
+    void hashset_iter_init(HashSetIter* iter, HashSet* set)
+
+    cc_stat hashset_iter_next(HashSetIter* iter, void** out)
+
+    cc_stat hashset_iter_remove(HashSetIter* iter, void** out)

+ 20 - 0
lakesuperior/cy_include/spookyhash.pxd

@@ -0,0 +1,20 @@
+from libc.stdint cimport uint32_t, uint64_t
+
+cdef extern from 'spookyhash_api.h':
+
+    ctypedef struct spookyhash_context:
+        pass
+
+    void spookyhash_context_init(
+            spookyhash_context *context, uint64_t seed_1, uint64_t seed_2)
+    void spookyhash_update(
+            spookyhash_context *context, const void *input, size_t input_size)
+    void spookyhash_final(
+            spookyhash_context *context, uint64_t *hash_1, uint64_t *hash_2)
+
+    uint32_t spookyhash_32(const void *input, size_t input_size, uint32_t seed)
+    uint64_t spookyhash_64(const void *input, size_t input_size, uint64_t seed)
+    void spookyhash_128(
+            const void *input, size_t input_size, uint64_t *hash_1,
+            uint64_t *hash_2)
+

+ 64 - 51
lakesuperior/endpoints/ldp.py

@@ -25,10 +25,10 @@ from lakesuperior.exceptions import (
         ServerManagedTermError, InvalidResourceError, SingleSubjectError,
         ServerManagedTermError, InvalidResourceError, SingleSubjectError,
         ResourceExistsError, IncompatibleLdpTypeError)
         ResourceExistsError, IncompatibleLdpTypeError)
 from lakesuperior.globals import RES_CREATED
 from lakesuperior.globals import RES_CREATED
-from lakesuperior.model.ldp_factory import LdpFactory
-from lakesuperior.model.ldp_nr import LdpNr
-from lakesuperior.model.ldp_rs import LdpRs
-from lakesuperior.model.ldpr import Ldpr
+from lakesuperior.model.ldp.ldp_factory import LdpFactory
+from lakesuperior.model.ldp.ldp_nr import LdpNr
+from lakesuperior.model.ldp.ldp_rs import LdpRs
+from lakesuperior.model.ldp.ldpr import Ldpr
 from lakesuperior.toolbox import Toolbox
 from lakesuperior.toolbox import Toolbox
 
 
 
 
@@ -44,6 +44,8 @@ rdf_parsable_mimetypes = {
 }
 }
 """MIMEtypes that can be parsed into RDF."""
 """MIMEtypes that can be parsed into RDF."""
 
 
+store = env.app_globals.rdf_store
+
 rdf_serializable_mimetypes = {
 rdf_serializable_mimetypes = {
     #mt.name for mt in plugin.plugins()
     #mt.name for mt in plugin.plugins()
     #if mt.kind is serializer.Serializer and '/' in mt.name
     #if mt.kind is serializer.Serializer and '/' in mt.name
@@ -166,39 +168,42 @@ def get_resource(uid, out_fmt=None):
 
 
     rsrc = rsrc_api.get(uid, repr_options)
     rsrc = rsrc_api.get(uid, repr_options)
 
 
-    if out_fmt is None:
-        rdf_mimetype = _best_rdf_mimetype()
-        out_fmt = (
-                'rdf'
-                if isinstance(rsrc, LdpRs) or rdf_mimetype is not None
-                else 'non_rdf')
-    out_headers.update(_headers_from_metadata(rsrc, out_fmt))
-    uri = g.tbox.uid_to_uri(uid)
-
-    # RDF output.
-    if out_fmt == 'rdf':
-        if locals().get('rdf_mimetype', None) is None:
-            rdf_mimetype = DEFAULT_RDF_MIMETYPE
-        ggr = g.tbox.globalize_graph(rsrc.out_graph)
-        ggr.namespace_manager = nsm
-        return _negotiate_content(
-                ggr, rdf_mimetype, out_headers, uid=uid, uri=uri)
-
-    # Datastream.
-    else:
-        if not getattr(rsrc, 'local_path', False):
-            return ('{} has no binary content.'.format(rsrc.uid), 404)
-
-        logger.debug('Streaming out binary content.')
-        if request.range and request.range.units == 'bytes':
-            # Stream partial response.
-            # This is only true if the header is well-formed. Thanks, Werkzeug.
-            rsp = _parse_range_header(request.range.ranges, rsrc, out_headers)
+    with store.txn_ctx():
+        if out_fmt is None:
+            rdf_mimetype = _best_rdf_mimetype()
+            out_fmt = (
+                    'rdf'
+                    if isinstance(rsrc, LdpRs) or rdf_mimetype is not None
+                    else 'non_rdf')
+        out_headers.update(_headers_from_metadata(rsrc, out_fmt))
+        uri = g.tbox.uid_to_uri(uid)
+
+# RDF output.
+        if out_fmt == 'rdf':
+            if locals().get('rdf_mimetype', None) is None:
+                rdf_mimetype = DEFAULT_RDF_MIMETYPE
+            ggr = g.tbox.globalize_imr(rsrc.out_graph)
+            ggr.namespace_manager = nsm
+            return _negotiate_content(
+                    ggr, rdf_mimetype, out_headers, uid=uid, uri=uri)
+
+# Datastream.
         else:
         else:
-            rsp = make_response(send_file(
-                    rsrc.local_path, as_attachment=True,
-                    attachment_filename=rsrc.filename,
-                    mimetype=rsrc.mimetype), 200, out_headers)
+            if not getattr(rsrc, 'local_path', False):
+                return ('{} has no binary content.'.format(rsrc.uid), 404)
+
+            logger.debug('Streaming out binary content.')
+            if request.range and request.range.units == 'bytes':
+                # Stream partial response.
+                # This is only true if the header is well-formed. Thanks, Werkzeug.
+                rsp = _parse_range_header(
+                    request.range.ranges, rsrc, out_headers
+                )
+            else:
+                rsp = make_response(send_file(
+                        rsrc.local_path, as_attachment=True,
+                        attachment_filename=rsrc.filename,
+                        mimetype=rsrc.mimetype), 200, out_headers)
 
 
         # This seems necessary to prevent Flask from setting an
         # This seems necessary to prevent Flask from setting an
         # additional ETag.
         # additional ETag.
@@ -217,7 +222,7 @@ def get_version_info(uid):
     """
     """
     rdf_mimetype = _best_rdf_mimetype() or DEFAULT_RDF_MIMETYPE
     rdf_mimetype = _best_rdf_mimetype() or DEFAULT_RDF_MIMETYPE
     try:
     try:
-        gr = rsrc_api.get_version_info(uid)
+        imr = rsrc_api.get_version_info(uid)
     except ResourceNotExistsError as e:
     except ResourceNotExistsError as e:
         return str(e), 404
         return str(e), 404
     except InvalidResourceError as e:
     except InvalidResourceError as e:
@@ -225,7 +230,8 @@ def get_version_info(uid):
     except TombstoneError as e:
     except TombstoneError as e:
         return _tombstone_response(e, uid)
         return _tombstone_response(e, uid)
     else:
     else:
-        return _negotiate_content(g.tbox.globalize_graph(gr), rdf_mimetype)
+        with store.txn_ctx():
+            return _negotiate_content(g.tbox.globalize_imr(imr), rdf_mimetype)
 
 
 
 
 @ldp.route('/<path:uid>/fcr:versions/<ver_uid>', methods=['GET'])
 @ldp.route('/<path:uid>/fcr:versions/<ver_uid>', methods=['GET'])
@@ -238,7 +244,7 @@ def get_version(uid, ver_uid):
     """
     """
     rdf_mimetype = _best_rdf_mimetype() or DEFAULT_RDF_MIMETYPE
     rdf_mimetype = _best_rdf_mimetype() or DEFAULT_RDF_MIMETYPE
     try:
     try:
-        gr = rsrc_api.get_version(uid, ver_uid)
+        imr = rsrc_api.get_version(uid, ver_uid)
     except ResourceNotExistsError as e:
     except ResourceNotExistsError as e:
         return str(e), 404
         return str(e), 404
     except InvalidResourceError as e:
     except InvalidResourceError as e:
@@ -246,7 +252,8 @@ def get_version(uid, ver_uid):
     except TombstoneError as e:
     except TombstoneError as e:
         return _tombstone_response(e, uid)
         return _tombstone_response(e, uid)
     else:
     else:
-        return _negotiate_content(g.tbox.globalize_graph(gr), rdf_mimetype)
+        with store.txn_ctx():
+            return _negotiate_content(g.tbox.globalize_imr(imr), rdf_mimetype)
 
 
 
 
 @ldp.route('/<path:parent_uid>', methods=['POST'], strict_slashes=False)
 @ldp.route('/<path:parent_uid>', methods=['POST'], strict_slashes=False)
@@ -290,7 +297,8 @@ def post_resource(parent_uid):
         return str(e), 412
         return str(e), 412
 
 
     uri = g.tbox.uid_to_uri(rsrc.uid)
     uri = g.tbox.uid_to_uri(rsrc.uid)
-    rsp_headers.update(_headers_from_metadata(rsrc))
+    with store.txn_ctx():
+        rsp_headers.update(_headers_from_metadata(rsrc))
     rsp_headers['Location'] = uri
     rsp_headers['Location'] = uri
 
 
     if mimetype and kwargs.get('rdf_fmt') is None:
     if mimetype and kwargs.get('rdf_fmt') is None:
@@ -346,7 +354,8 @@ def put_resource(uid):
     except TombstoneError as e:
     except TombstoneError as e:
         return _tombstone_response(e, uid)
         return _tombstone_response(e, uid)
 
 
-    rsp_headers = _headers_from_metadata(rsrc)
+    with store.txn_ctx():
+        rsp_headers = _headers_from_metadata(rsrc)
     rsp_headers['Content-Type'] = 'text/plain; charset=utf-8'
     rsp_headers['Content-Type'] = 'text/plain; charset=utf-8'
 
 
     uri = g.tbox.uid_to_uri(uid)
     uri = g.tbox.uid_to_uri(uid)
@@ -397,7 +406,8 @@ def patch_resource(uid, is_metadata=False):
     except InvalidResourceError as e:
     except InvalidResourceError as e:
         return str(e), 415
         return str(e), 415
     else:
     else:
-        rsp_headers.update(_headers_from_metadata(rsrc))
+        with store.txn_ctx():
+            rsp_headers.update(_headers_from_metadata(rsrc))
         return '', 204, rsp_headers
         return '', 204, rsp_headers
 
 
 
 
@@ -455,7 +465,7 @@ def tombstone(uid):
     405.
     405.
     """
     """
     try:
     try:
-        rsrc = rsrc_api.get(uid)
+        rsrc_api.get(uid)
     except TombstoneError as e:
     except TombstoneError as e:
         if request.method == 'DELETE':
         if request.method == 'DELETE':
             if e.uid == uid:
             if e.uid == uid:
@@ -668,7 +678,7 @@ def _headers_from_metadata(rsrc, out_fmt='text/turtle'):
     """
     """
     Create a dict of headers from a metadata graph.
     Create a dict of headers from a metadata graph.
 
 
-    :param lakesuperior.model.ldpr.Ldpr rsrc: Resource to extract metadata
+    :param lakesuperior.model.ldp.ldpr.Ldpr rsrc: Resource to extract metadata
         from.
         from.
     """
     """
     rsp_headers = defaultdict(list)
     rsp_headers = defaultdict(list)
@@ -764,12 +774,14 @@ def _condition_hdr_match(uid, headers, safe=True):
         req_etags = [
         req_etags = [
                 et.strip('\'" ') for et in headers.get(cond_hdr).split(',')]
                 et.strip('\'" ') for et in headers.get(cond_hdr).split(',')]
 
 
-        try:
-            rsrc_meta = rsrc_api.get_metadata(uid)
-        except ResourceNotExistsError:
-            rsrc_meta = Imr(nsc['fcres'][uid])
+        with store.txn_ctx():
+            try:
+                rsrc_meta = rsrc_api.get_metadata(uid)
+            except ResourceNotExistsError:
+                rsrc_meta = Graph(uri=nsc['fcres'][uid])
+
+            digest_prop = rsrc_meta.value(nsc['premis'].hasMessageDigest)
 
 
-        digest_prop = rsrc_meta.value(nsc['premis'].hasMessageDigest)
         if digest_prop:
         if digest_prop:
             etag, _ = _digest_headers(digest_prop)
             etag, _ = _digest_headers(digest_prop)
             if cond_hdr == 'if-match':
             if cond_hdr == 'if-match':
@@ -793,7 +805,8 @@ def _condition_hdr_match(uid, headers, safe=True):
                 'if-unmodified-since': False
                 'if-unmodified-since': False
             }
             }
 
 
-        lastmod_str = rsrc_meta.value(nsc['fcrepo'].lastModified)
+        with store.txn_ctx():
+            lastmod_str = rsrc_meta.value(nsc['fcrepo'].lastModified)
         lastmod_ts = arrow.get(lastmod_str)
         lastmod_ts = arrow.get(lastmod_str)
 
 
         # If date is not in a RFC 5322 format
         # If date is not in a RFC 5322 format

+ 0 - 0
lakesuperior/model/__init__.pxd


+ 29 - 0
lakesuperior/model/base.pxd

@@ -0,0 +1,29 @@
+cimport lakesuperior.cy_include.cytpl as tpl
+
+ctypedef tpl.tpl_bin Buffer
+
+# NOTE This may change in the future, e.g. if a different key size is to
+# be forced.
+ctypedef size_t Key
+
+ctypedef Key DoubleKey[2]
+ctypedef Key TripleKey[3]
+ctypedef Key QuadKey[4]
+
+cdef enum:
+    KLEN = sizeof(Key)
+    DBL_KLEN = 2 * sizeof(Key)
+    TRP_KLEN = 3 * sizeof(Key)
+    QUAD_KLEN = 4 * sizeof(Key)
+
+    # "NULL" key, a value that is never user-provided. Used to mark special
+    # values (e.g. deleted records).
+    NULL_KEY = 0
+    # Value of first key inserted in an empty term database.
+    FIRST_KEY = 1
+
+cdef bytes buffer_dump(Buffer* buf)
+
+# "NULL" triple, a value that is never user-provided. Used to mark special
+# values (e.g. deleted records).
+cdef TripleKey NULL_TRP = [NULL_KEY, NULL_KEY, NULL_KEY]

+ 11 - 0
lakesuperior/model/base.pyx

@@ -0,0 +1,11 @@
+cdef bytes buffer_dump(const Buffer* buf):
+    """
+    Return a buffer's content as a string.
+
+    :param const Buffer* buf Pointer to a buffer to be read.
+
+    :rtype: str
+    """
+    cdef unsigned char* buf_stream = (<unsigned char*>buf.addr)
+    return buf_stream[:buf.sz]
+

+ 30 - 0
lakesuperior/model/callbacks.pxd

@@ -0,0 +1,30 @@
+from lakesuperior.model.base cimport Key, TripleKey
+
+cdef:
+    bint lookup_sk_cmp_fn(
+        const TripleKey* spok, const Key k1, const Key k2
+    )
+
+    bint lookup_pk_cmp_fn(
+        const TripleKey* spok, const Key k1, const Key k2
+    )
+
+    bint lookup_ok_cmp_fn(
+        const TripleKey* spok, const Key k1, const Key k2
+    )
+
+    bint lookup_skpk_cmp_fn(
+        const TripleKey* spok, const Key k1, const Key k2
+    )
+
+    bint lookup_skok_cmp_fn(
+        const TripleKey* spok, const Key k1, const Key k2
+    )
+
+    bint lookup_pkok_cmp_fn(
+        const TripleKey* spok, const Key k1, const Key k2
+    )
+
+    bint lookup_none_cmp_fn(
+        const TripleKey* spok, const Key k1, const Key k2
+    )

+ 54 - 0
lakesuperior/model/callbacks.pyx

@@ -0,0 +1,54 @@
+from lakesuperior.model.base cimport Key, TripleKey
+
+cdef inline bint lookup_sk_cmp_fn(
+    const TripleKey* spok, const Key k1, const Key k2
+):
+    """ Keyset lookup for S key. """
+    return spok[0][0] == k1
+
+
+cdef inline bint lookup_pk_cmp_fn(
+    const TripleKey* spok, const Key k1, const Key k2
+):
+    """ Keyset lookup for P key. """
+    return spok[0][1] == k1
+
+
+cdef inline bint lookup_ok_cmp_fn(
+    const TripleKey* spok, const Key k1, const Key k2
+):
+    """ Keyset lookup for O key. """
+    return spok[0][2] == k1
+
+
+cdef inline bint lookup_skpk_cmp_fn(
+    const TripleKey* spok, const Key k1, const Key k2
+):
+    """ Keyset lookup for S and P keys. """
+    return spok[0][0] == k1 and spok[0][1] == k2
+
+
+cdef inline bint lookup_skok_cmp_fn(
+    const TripleKey* spok, const Key k1, const Key k2
+):
+    """ Keyset lookup for S and O keys. """
+    return spok[0][0] == k1 and spok[0][2] == k2
+
+
+cdef inline bint lookup_pkok_cmp_fn(
+    const TripleKey* spok, const Key k1, const Key k2
+):
+    """ Keyset lookup for P and O keys. """
+    return spok[0][1] == k1 and spok[0][2] == k2
+
+
+cdef inline bint lookup_none_cmp_fn(
+    const TripleKey* spok, const Key k1, const Key k2
+):
+    """
+    Dummy callback for queries with all parameters unbound.
+
+    This function always returns ``True`` 
+    """
+    return True
+

+ 11 - 11
lakesuperior/model/ldp_factory.py → lakesuperior/model/ldp/ldp_factory.py

@@ -3,20 +3,19 @@ import logging
 from pprint import pformat
 from pprint import pformat
 from uuid import uuid4
 from uuid import uuid4
 
 
-from rdflib import Graph, parser
 from rdflib.resource import Resource
 from rdflib.resource import Resource
 from rdflib.namespace import RDF
 from rdflib.namespace import RDF
 
 
 from lakesuperior import env
 from lakesuperior import env
-from lakesuperior.model.ldpr import Ldpr
-from lakesuperior.model.ldp_nr import LdpNr
-from lakesuperior.model.ldp_rs import LdpRs, Ldpc, LdpDc, LdpIc
+from lakesuperior.model.ldp.ldpr import Ldpr
+from lakesuperior.model.ldp.ldp_nr import LdpNr
+from lakesuperior.model.ldp.ldp_rs import LdpRs, Ldpc, LdpDc, LdpIc
 from lakesuperior.config_parser import config
 from lakesuperior.config_parser import config
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.exceptions import (
 from lakesuperior.exceptions import (
         IncompatibleLdpTypeError, InvalidResourceError, ResourceExistsError,
         IncompatibleLdpTypeError, InvalidResourceError, ResourceExistsError,
         ResourceNotExistsError, TombstoneError)
         ResourceNotExistsError, TombstoneError)
-from lakesuperior.store.ldp_rs.lmdb_triplestore import Imr
+from lakesuperior.model.rdf.graph import Graph, from_rdf
 
 
 
 
 LDP_NR_TYPE = nsc['ldp'].NonRDFSource
 LDP_NR_TYPE = nsc['ldp'].NonRDFSource
@@ -37,7 +36,7 @@ class LdpFactory:
             raise InvalidResourceError(uid)
             raise InvalidResourceError(uid)
         if rdfly.ask_rsrc_exists(uid):
         if rdfly.ask_rsrc_exists(uid):
             raise ResourceExistsError(uid)
             raise ResourceExistsError(uid)
-        rsrc = Ldpc(uid, provided_imr=Imr(uri=nsc['fcres'][uid]))
+        rsrc = Ldpc(uid, provided_imr=Graph(uri=nsc['fcres'][uid]))
 
 
         return rsrc
         return rsrc
 
 
@@ -100,14 +99,15 @@ class LdpFactory:
         """
         """
         uri = nsc['fcres'][uid]
         uri = nsc['fcres'][uid]
         if rdf_data:
         if rdf_data:
-            data = set(Graph().parse(
-                data=rdf_data, format=rdf_fmt, publicID=nsc['fcres'][uid]))
+            provided_imr = from_rdf(
+                uri=uri, data=rdf_data, format=rdf_fmt,
+                publicID=nsc['fcres'][uid]
+            )
         elif graph:
         elif graph:
-            data = set(graph)
+            provided_imr = Graph(uri=uri, data={*graph})
         else:
         else:
-            data = set()
+            provided_imr = Graph(uri=uri)
 
 
-        provided_imr = Imr(uri=uri, data=data)
         #logger.debug('Provided graph: {}'.format(
         #logger.debug('Provided graph: {}'.format(
         #        pformat(set(provided_imr))))
         #        pformat(set(provided_imr))))
 
 

+ 2 - 2
lakesuperior/model/ldp_nr.py → lakesuperior/model/ldp/ldp_nr.py

@@ -8,8 +8,8 @@ from rdflib.term import URIRef, Literal, Variable
 
 
 from lakesuperior import env
 from lakesuperior import env
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
-from lakesuperior.model.ldpr import Ldpr
-from lakesuperior.model.ldp_rs import LdpRs
+from lakesuperior.model.ldp.ldpr import Ldpr
+from lakesuperior.model.ldp.ldp_rs import LdpRs
 
 
 
 
 nonrdfly = env.app_globals.nonrdfly
 nonrdfly = env.app_globals.nonrdfly

+ 1 - 1
lakesuperior/model/ldp_rs.py → lakesuperior/model/ldp/ldp_rs.py

@@ -5,7 +5,7 @@ from rdflib import Graph
 from lakesuperior import env
 from lakesuperior import env
 from lakesuperior.globals import RES_UPDATED
 from lakesuperior.globals import RES_UPDATED
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
-from lakesuperior.model.ldpr import Ldpr
+from lakesuperior.model.ldp.ldpr import Ldpr
 
 
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)

+ 57 - 46
lakesuperior/model/ldpr.py → lakesuperior/model/ldp/ldpr.py

@@ -12,8 +12,9 @@ from urllib.parse import urldefrag
 from uuid import uuid4
 from uuid import uuid4
 
 
 import arrow
 import arrow
+import rdflib
 
 
-from rdflib import Graph, URIRef, Literal
+from rdflib import URIRef, Literal
 from rdflib.compare import to_isomorphic
 from rdflib.compare import to_isomorphic
 from rdflib.namespace import RDF
 from rdflib.namespace import RDF
 
 
@@ -26,7 +27,7 @@ from lakesuperior.dictionaries.srv_mgd_terms import (
 from lakesuperior.exceptions import (
 from lakesuperior.exceptions import (
     InvalidResourceError, RefIntViolationError, ResourceNotExistsError,
     InvalidResourceError, RefIntViolationError, ResourceNotExistsError,
     ServerManagedTermError, TombstoneError)
     ServerManagedTermError, TombstoneError)
-from lakesuperior.store.ldp_rs.lmdb_triplestore import SimpleGraph, Imr
+from lakesuperior.model.rdf.graph import Graph
 from lakesuperior.store.ldp_rs.rsrc_centric_layout import VERS_CONT_LABEL
 from lakesuperior.store.ldp_rs.rsrc_centric_layout import VERS_CONT_LABEL
 from lakesuperior.toolbox import Toolbox
 from lakesuperior.toolbox import Toolbox
 
 
@@ -47,7 +48,7 @@ class Ldpr(metaclass=ABCMeta):
     **Note**: Even though LdpNr (which is a subclass of Ldpr) handles binary
     **Note**: Even though LdpNr (which is a subclass of Ldpr) handles binary
     files, it still has an RDF representation in the triplestore. Hence, some
     files, it still has an RDF representation in the triplestore. Hence, some
     of the RDF-related methods are defined in this class rather than in
     of the RDF-related methods are defined in this class rather than in
-    :class:`~lakesuperior.model.ldp_rs.LdpRs`.
+    :class:`~lakesuperior.model.ldp.ldp_rs.LdpRs`.
 
 
     **Note:** Only internal facing (``info:fcres``-prefixed) URIs are handled
     **Note:** Only internal facing (``info:fcres``-prefixed) URIs are handled
     in this class. Public-facing URI conversion is handled in the
     in this class. Public-facing URI conversion is handled in the
@@ -233,7 +234,7 @@ class Ldpr(metaclass=ABCMeta):
         :param v: New set of triples to populate the IMR with.
         :param v: New set of triples to populate the IMR with.
         :type v: set or rdflib.Graph
         :type v: set or rdflib.Graph
         """
         """
-        self._imr = Imr(self.uri, data=set(data))
+        self._imr = Graph(uri=self.uri, data=set(data))
 
 
 
 
     @imr.deleter
     @imr.deleter
@@ -266,8 +267,8 @@ class Ldpr(metaclass=ABCMeta):
         """
         """
         Set resource metadata.
         Set resource metadata.
         """
         """
-        if not isinstance(rsrc, Imr):
-            raise TypeError('Provided metadata is not an Imr object.')
+        if not isinstance(rsrc, Graph):
+            raise TypeError('Provided metadata is not a Graph object.')
         self._metadata = rsrc
         self._metadata = rsrc
 
 
 
 
@@ -276,7 +277,7 @@ class Ldpr(metaclass=ABCMeta):
         """
         """
         Retun a graph of the resource's IMR formatted for output.
         Retun a graph of the resource's IMR formatted for output.
         """
         """
-        out_gr = Graph(identifier=self.uri)
+        out_trp = set()
 
 
         for t in self.imr:
         for t in self.imr:
             if (
             if (
@@ -290,9 +291,9 @@ class Ldpr(metaclass=ABCMeta):
                 self._imr_options.get('incl_srv_mgd', True) or
                 self._imr_options.get('incl_srv_mgd', True) or
                 not self._is_trp_managed(t)
                 not self._is_trp_managed(t)
             ):
             ):
-                out_gr.add(t)
+                out_trp.add(t)
 
 
-        return out_gr
+        return Graph(uri=self.uri, data=out_trp)
 
 
 
 
     @property
     @property
@@ -304,7 +305,7 @@ class Ldpr(metaclass=ABCMeta):
             try:
             try:
                 self._version_info = rdfly.get_version_info(self.uid)
                 self._version_info = rdfly.get_version_info(self.uid)
             except ResourceNotExistsError as e:
             except ResourceNotExistsError as e:
-                self._version_info = Imr(uri=self.uri)
+                self._version_info = Graph(uri=self.uri)
 
 
         return self._version_info
         return self._version_info
 
 
@@ -422,8 +423,7 @@ class Ldpr(metaclass=ABCMeta):
 
 
         remove_trp = {
         remove_trp = {
             (self.uri, pred, None) for pred in self.delete_preds_on_replace}
             (self.uri, pred, None) for pred in self.delete_preds_on_replace}
-        add_trp = set(
-            self.provided_imr | self._containment_rel(create))
+        add_trp = {*self.provided_imr} | self._containment_rel(create)
 
 
         self.modify(ev_type, remove_trp, add_trp)
         self.modify(ev_type, remove_trp, add_trp)
 
 
@@ -462,7 +462,7 @@ class Ldpr(metaclass=ABCMeta):
             }
             }
 
 
         # Bury descendants.
         # Bury descendants.
-        from lakesuperior.model.ldp_factory import LdpFactory
+        from lakesuperior.model.ldp.ldp_factory import LdpFactory
         for desc_uri in rdfly.get_descendants(self.uid):
         for desc_uri in rdfly.get_descendants(self.uid):
             try:
             try:
                 desc_rsrc = LdpFactory.from_stored(
                 desc_rsrc = LdpFactory.from_stored(
@@ -513,7 +513,7 @@ class Ldpr(metaclass=ABCMeta):
         self.modify(RES_CREATED, remove_trp, add_trp)
         self.modify(RES_CREATED, remove_trp, add_trp)
 
 
         # Resurrect descendants.
         # Resurrect descendants.
-        from lakesuperior.model.ldp_factory import LdpFactory
+        from lakesuperior.model.ldp.ldp_factory import LdpFactory
         descendants = env.app_globals.rdfly.get_descendants(self.uid)
         descendants = env.app_globals.rdfly.get_descendants(self.uid)
         for desc_uri in descendants:
         for desc_uri in descendants:
             LdpFactory.from_stored(
             LdpFactory.from_stored(
@@ -583,50 +583,54 @@ class Ldpr(metaclass=ABCMeta):
 
 
         ver_gr = rdfly.get_imr(
         ver_gr = rdfly.get_imr(
             self.uid, ver_uid=ver_uid, incl_children=False)
             self.uid, ver_uid=ver_uid, incl_children=False)
-        self.provided_imr = Imr(uri=self.uri)
+        self.provided_imr = Graph(uri=self.uri)
 
 
         for t in ver_gr:
         for t in ver_gr:
             if not self._is_trp_managed(t):
             if not self._is_trp_managed(t):
-                self.provided_imr.add((self.uri, t[1], t[2]))
+                self.provided_imr.add(((self.uri, t[1], t[2]),))
             # @TODO Check individual objects: if they are repo-managed URIs
             # @TODO Check individual objects: if they are repo-managed URIs
             # and not existing or tombstones, they are not added.
             # and not existing or tombstones, they are not added.
 
 
         return self.create_or_replace(create_only=False)
         return self.create_or_replace(create_only=False)
 
 
 
 
-    def check_mgd_terms(self, gr):
+    def check_mgd_terms(self, trp):
         """
         """
         Check whether server-managed terms are in a RDF payload.
         Check whether server-managed terms are in a RDF payload.
 
 
-        :param SimpleGraph gr: The graph to validate.
+        :param SimpleGraph trp: The graph to validate.
         """
         """
-        offending_subjects = gr.terms('s') & srv_mgd_subjects
+        offending_subjects = (
+            {t[0] for t in trp if t[0] is not None} & srv_mgd_subjects
+        )
         if offending_subjects:
         if offending_subjects:
             if self.handling == 'strict':
             if self.handling == 'strict':
                 raise ServerManagedTermError(offending_subjects, 's')
                 raise ServerManagedTermError(offending_subjects, 's')
             else:
             else:
-                gr_set = set(gr)
                 for s in offending_subjects:
                 for s in offending_subjects:
                     logger.info('Removing offending subj: {}'.format(s))
                     logger.info('Removing offending subj: {}'.format(s))
-                    for t in gr_set:
+                    for t in trp:
                         if t[0] == s:
                         if t[0] == s:
-                            gr.remove(t)
+                            trp.remove(t)
 
 
-        offending_predicates = gr.terms('p') & srv_mgd_predicates
+        offending_predicates = (
+            {t[1] for t in trp if t[1] is not None} & srv_mgd_predicates
+        )
         # Allow some predicates if the resource is being created.
         # Allow some predicates if the resource is being created.
         if offending_predicates:
         if offending_predicates:
             if self.handling == 'strict':
             if self.handling == 'strict':
                 raise ServerManagedTermError(offending_predicates, 'p')
                 raise ServerManagedTermError(offending_predicates, 'p')
             else:
             else:
-                gr_set = set(gr)
                 for p in offending_predicates:
                 for p in offending_predicates:
                     logger.info('Removing offending pred: {}'.format(p))
                     logger.info('Removing offending pred: {}'.format(p))
-                    for t in gr_set:
+                    for t in trp:
                         if t[1] == p:
                         if t[1] == p:
-                            gr.remove(t)
+                            trp.remove(t)
 
 
-        types = {t[2] for t in gr if t[1] == RDF.type}
-        offending_types = types & srv_mgd_types
+        offending_types = (
+            {t[2] for t in trp if t[1] == RDF.type and t[2] is not None}
+            & srv_mgd_types
+        )
         if not self.is_stored:
         if not self.is_stored:
             offending_types -= self.smt_allow_on_create
             offending_types -= self.smt_allow_on_create
         if offending_types:
         if offending_types:
@@ -635,11 +639,11 @@ class Ldpr(metaclass=ABCMeta):
             else:
             else:
                 for to in offending_types:
                 for to in offending_types:
                     logger.info('Removing offending type: {}'.format(to))
                     logger.info('Removing offending type: {}'.format(to))
-                    gr.remove_triples((None, RDF.type, to))
+                    trp.remove_triples((None, RDF.type, to))
 
 
-        #logger.debug('Sanitized graph: {}'.format(gr.serialize(
+        #logger.debug('Sanitized graph: {}'.format(trp.serialize(
         #    format='turtle').decode('utf-8')))
         #    format='turtle').decode('utf-8')))
-        return gr
+        return trp
 
 
 
 
     def sparql_delta(self, qry_str):
     def sparql_delta(self, qry_str):
@@ -672,16 +676,15 @@ class Ldpr(metaclass=ABCMeta):
         qry_str = (
         qry_str = (
                 re.sub('<#([^>]+)>', '<{}#\\1>'.format(self.uri), qry_str)
                 re.sub('<#([^>]+)>', '<{}#\\1>'.format(self.uri), qry_str)
                 .replace('<>', '<{}>'.format(self.uri)))
                 .replace('<>', '<{}>'.format(self.uri)))
-        pre_gr = self.imr.graph
-        post_rdfgr = Graph(identifier=self.uri)
-        post_rdfgr += pre_gr
+        pre_gr = self.imr.as_rdflib()
+        post_gr = rdflib.Graph(identifier=self.uri)
+        post_gr |= pre_gr
 
 
-        post_rdfgr.update(qry_str)
-        post_gr = SimpleGraph(data=set(post_rdfgr))
+        post_gr.update(qry_str)
 
 
         # FIXME Fix and  use SimpleGraph's native subtraction operation.
         # FIXME Fix and  use SimpleGraph's native subtraction operation.
-        remove_gr = self.check_mgd_terms(SimpleGraph(pre_gr.data - post_gr.data))
-        add_gr = self.check_mgd_terms(SimpleGraph(post_gr.data - pre_gr.data))
+        remove_gr = self.check_mgd_terms(Graph(data=set(pre_gr - post_gr)))
+        add_gr = self.check_mgd_terms(Graph(data=set(post_gr - pre_gr)))
 
 
         return remove_gr, add_gr
         return remove_gr, add_gr
 
 
@@ -719,6 +722,12 @@ class Ldpr(metaclass=ABCMeta):
         rdfly.modify_rsrc(self.uid, remove_trp, add_trp)
         rdfly.modify_rsrc(self.uid, remove_trp, add_trp)
 
 
         self._clear_cache()
         self._clear_cache()
+        # Reload IMR because if we exit the LMDB txn we lose access to stored
+        # memory locations.
+        try:
+            self.imr
+        except:
+            pass
 
 
         if (
         if (
                 ev_type is not None and
                 ev_type is not None and
@@ -798,7 +807,7 @@ class Ldpr(metaclass=ABCMeta):
             logger.info(
             logger.info(
                 'Removing link to non-existent repo resource: {}'
                 'Removing link to non-existent repo resource: {}'
                 .format(obj))
                 .format(obj))
-            self.provided_imr.remove_triples((None, None, obj))
+            self.provided_imr.remove((None, None, obj))
 
 
 
 
     def _add_srv_mgd_triples(self, create=False):
     def _add_srv_mgd_triples(self, create=False):
@@ -808,8 +817,9 @@ class Ldpr(metaclass=ABCMeta):
         :param create: Whether the resource is being created.
         :param create: Whether the resource is being created.
         """
         """
         # Base LDP types.
         # Base LDP types.
-        for t in self.base_types:
-            self.provided_imr.add((self.uri, RDF.type, t))
+        self.provided_imr.add(
+            [(self.uri, RDF.type, t) for t in self.base_types]
+        )
 
 
         # Create and modify timestamp.
         # Create and modify timestamp.
         if create:
         if create:
@@ -856,7 +866,7 @@ class Ldpr(metaclass=ABCMeta):
         a LDP-NR has "children" under ``fcr:versions``) by setting this to
         a LDP-NR has "children" under ``fcr:versions``) by setting this to
         True.
         True.
         """
         """
-        from lakesuperior.model.ldp_factory import LdpFactory
+        from lakesuperior.model.ldp.ldp_factory import LdpFactory
 
 
         if '/' in self.uid.lstrip('/'):
         if '/' in self.uid.lstrip('/'):
             # Traverse up the hierarchy to find the parent.
             # Traverse up the hierarchy to find the parent.
@@ -886,8 +896,9 @@ class Ldpr(metaclass=ABCMeta):
         # Only update parent if the resource is new.
         # Only update parent if the resource is new.
         if create:
         if create:
             add_gr = Graph()
             add_gr = Graph()
-            add_gr.add(
-                (nsc['fcres'][parent_uid], nsc['ldp'].contains, self.uri))
+            add_gr.add({
+                (nsc['fcres'][parent_uid], nsc['ldp'].contains, self.uri)
+            })
             parent_rsrc.modify(RES_UPDATED, add_trp=add_gr)
             parent_rsrc.modify(RES_UPDATED, add_trp=add_gr)
 
 
         # Direct or indirect container relationship.
         # Direct or indirect container relationship.
@@ -900,7 +911,7 @@ class Ldpr(metaclass=ABCMeta):
 
 
         :param rdflib.resource.Resouce cont_rsrc:  The container resource.
         :param rdflib.resource.Resouce cont_rsrc:  The container resource.
         """
         """
-        cont_p = cont_rsrc.metadata.terms('p')
+        cont_p = cont_rsrc.metadata.terms_by_type('p')
 
 
         logger.info('Checking direct or indirect containment.')
         logger.info('Checking direct or indirect containment.')
         logger.debug('Parent predicates: {}'.format(cont_p))
         logger.debug('Parent predicates: {}'.format(cont_p))
@@ -908,7 +919,7 @@ class Ldpr(metaclass=ABCMeta):
         add_trp = {(self.uri, nsc['fcrepo'].hasParent, cont_rsrc.uri)}
         add_trp = {(self.uri, nsc['fcrepo'].hasParent, cont_rsrc.uri)}
 
 
         if self.MBR_RSRC_URI in cont_p and self.MBR_REL_URI in cont_p:
         if self.MBR_RSRC_URI in cont_p and self.MBR_REL_URI in cont_p:
-            from lakesuperior.model.ldp_factory import LdpFactory
+            from lakesuperior.model.ldp.ldp_factory import LdpFactory
 
 
             s = cont_rsrc.metadata.value(self.MBR_RSRC_URI)
             s = cont_rsrc.metadata.value(self.MBR_RSRC_URI)
             p = cont_rsrc.metadata.value(self.MBR_REL_URI)
             p = cont_rsrc.metadata.value(self.MBR_REL_URI)

+ 0 - 0
lakesuperior/model/rdf/__init__.pxd


+ 0 - 0
lakesuperior/model/rdf/__init__.py


+ 37 - 0
lakesuperior/model/rdf/graph.pxd

@@ -0,0 +1,37 @@
+from libc.stdint cimport uint32_t, uint64_t
+
+from cymem.cymem cimport Pool
+
+cimport lakesuperior.cy_include.collections as cc
+
+from lakesuperior.model.base cimport Key, TripleKey
+from lakesuperior.model.rdf.triple cimport BufferTriple
+from lakesuperior.model.structures.keyset cimport Keyset
+from lakesuperior.store.ldp_rs cimport lmdb_triplestore
+
+# Callback for an iterator.
+ctypedef void (*lookup_callback_fn_t)(
+    Graph gr, const TripleKey* spok_p, void* ctx
+)
+
+cdef class Graph:
+    cdef:
+        readonly lmdb_triplestore.LmdbTriplestore store
+        public Keyset keys
+        public object uri
+
+        cc.key_compare_ft term_cmp_fn
+        cc.key_compare_ft trp_cmp_fn
+
+        void _match_ptn_callback(
+            self, pattern, Graph gr, lookup_callback_fn_t callback_fn,
+            bint callback_cond=*, void* ctx=*
+        ) except *
+
+    cpdef Graph copy(self, str uri=*)
+    cpdef Graph empty_copy(self, str uri=*)
+    cpdef void set(self, tuple trp) except *
+
+
+cdef:
+    void add_trp_callback(Graph gr, const TripleKey* spok_p, void* ctx)

+ 613 - 0
lakesuperior/model/rdf/graph.pyx

@@ -0,0 +1,613 @@
+import logging
+
+import rdflib
+
+from lakesuperior import env
+
+from cpython.object cimport Py_LT, Py_EQ, Py_GT, Py_LE, Py_NE, Py_GE
+from libc.string cimport memcpy
+from libc.stdlib cimport free
+
+cimport lakesuperior.cy_include.collections as cc
+cimport lakesuperior.model.callbacks as cb
+cimport lakesuperior.model.structures.keyset as kset
+
+from lakesuperior.model.base cimport Key, TripleKey
+from lakesuperior.model.rdf cimport term
+from lakesuperior.model.rdf.triple cimport BufferTriple
+from lakesuperior.model.structures.hash cimport term_hash_seed32
+from lakesuperior.model.structures.keyset cimport Keyset
+
+logger = logging.getLogger(__name__)
+
+
+cdef class Graph:
+    """
+    Fast and simple implementation of a graph.
+
+    Most functions should mimic RDFLib's graph with less overhead. It uses
+    the same funny but functional slicing notation.
+
+    A Graph contains a :py:class:`lakesuperior.model.structures.keyset.Keyset`
+    at its core and is bound to a
+    :py:class:`~lakesuperior.store.ldp_rs.lmdb_triplestore.LmdbTriplestore`.
+    This makes lookups and boolean operations very efficient because all these
+    operations are performed on an array of integers.
+
+    In order to retrieve RDF values from a ``Graph``, the underlying store
+    must be looked up. This can be done in a different transaction than the
+    one used to create or otherwise manipulate the graph.
+
+    Every time a term is looked up or added to even a temporary graph, that
+    term is added to the store and creates a key. This is because in the
+    majority of cases that term is likely to be stored permanently anyway, and
+    it's more efficient to hash it and allocate it immediately. A cleanup
+    function to remove all orphaned terms (not in any triple or context index)
+    can be later devised to compact the database.
+
+    An instance of this class can also be converted to a ``rdflib.Graph``
+    instance.
+    """
+
+    def __cinit__(
+        self, store=None, size_t capacity=0, uri=None, set data=set()
+    ):
+        """
+        Initialize the graph, optionally from Python/RDFlib data.
+
+        When initializing a non-empty Graph, a store transaction must be
+        opened::
+
+            >>> from rdflib import URIRef
+            >>> from lakesuperior import env_setup, env
+            >>> store = env.app_globals.rdf_store
+            >>> # Or alternatively:
+            >>> # from lakesuperior.store.ldp_rs.lmdb_store import LmdbStore
+            >>> # store = LmdbStore('/tmp/test')
+            >>> trp = {(URIRef('urn:s:0'), URIRef('urn:p:0'), URIRef('urn:o:0'))}
+            >>> with store.txn_ctx():
+            >>>     gr = Graph(store, data=trp)
+
+        Similarly, any operation such as adding, changing or looking up triples
+        needs a store transaction.
+
+        Note that, even though any operation may involve adding new terms to
+        the store, a read-only transaction is sufficient. Lakesuperior will
+        open a write transaction automatically only if necessary and only for
+        the time needed to enter the new terms.
+
+        :type store: lakesuperior.store.ldp_rs.lmdb_triplestore.LmdbTriplestore
+        :param store: Triplestore where keys are mapped to terms. By default
+            this is the default application store
+            (``env.app_globals.rdf_store``).
+
+        :param size_t capacity: Initial number of allocated triples.
+
+        :param str uri: If specified, the graph becomes a named graph and can
+            utilize the :py:meth:`value()` method and special slicing notation.
+
+        :param set data: If specified, ``capacity`` is ignored and an initial key
+            set is created from a set of 3-tuples of :py:class:``rdflib.Term``
+            instances.
+        """
+        self.uri = rdflib.URIRef(uri) if uri else None
+
+        self.store = store if store is not None else env.app_globals.rdf_store
+        #logger.debug(f'Assigned store at {self.store.env_path}')
+
+        # Initialize empty data set.
+        if data:
+            # Populate with provided Python set.
+            self.keys = Keyset(len(data))
+            self.add(data)
+        else:
+            self.keys = Keyset(capacity)
+
+
+    ## PROPERTIES ##
+
+    property data:
+        def __get__(self):
+            """
+            Triple data as a Python/RDFlib set.
+
+            :rtype: set
+            """
+            cdef TripleKey spok
+
+            ret = set()
+
+            self.keys.seek()
+            while self.keys.get_next(&spok):
+                ret.add((
+                    self.store.from_key(spok[0]),
+                    self.store.from_key(spok[1]),
+                    self.store.from_key(spok[2])
+                ))
+
+            return ret
+
+
+    property capacity:
+        def __get__(self):
+            """
+            Total capacity of the underlying Keyset, in number of triples.
+            """
+            return self.keys.capacity
+
+
+    property txn_ctx:
+        def __get__(self):
+            """ Expose underlying store's ``txn_ctx``. """
+            return self.store.txn_ctx
+
+
+    ## MAGIC METHODS ##
+
+    def __len__(self):
+        """ Number of triples in the graph. """
+        return self.keys.size()
+
+
+    def __richcmp__(self, other, int op):
+        """ Comparators between ``Graph`` instances. """
+        if op == Py_LT:
+            raise NotImplementedError()
+        elif op == Py_EQ:
+            return len(self ^ other) == 0
+        elif op == Py_GT:
+            raise NotImplementedError()
+        elif op == Py_LE:
+            raise NotImplementedError()
+        elif op == Py_NE:
+            return len(self ^ other) != 0
+        elif op == Py_GE:
+            raise NotImplementedError()
+
+
+    def __repr__(self):
+        """
+        String representation of the graph.
+
+        This includes the subject URI, number of triples contained and the
+        memory address of the instance.
+        """
+        uri_repr = f', uri={self.uri}' if self.uri else ''
+        return (
+            f'<{self.__class__.__module__}.{self.__class__.__qualname__} '
+            f'@0x{id(self):02x} length={len(self)}{uri_repr}>'
+        )
+
+
+    def __str__(self):
+        """ String dump of the graph triples. """
+        return str(self.data)
+
+
+    def __add__(self, other):
+        """ Alias for set-theoretical union. """
+        return self.__or__(other)
+
+
+    def __iadd__(self, other):
+        """ Alias for in-place set-theoretical union. """
+        return self.__ior__(other)
+
+
+    def __sub__(self, other):
+        """ Set-theoretical subtraction. """
+        cdef Graph gr3 = self.empty_copy()
+
+        gr3.keys = kset.subtract(self.keys, other.keys)
+
+        return gr3
+
+
+    def __isub__(self, other):
+        """ In-place set-theoretical subtraction. """
+        self.keys = kset.subtract(self.keys, other.keys)
+
+        return self
+
+    def __and__(self, other):
+        """ Set-theoretical intersection. """
+        cdef Graph gr3 = self.empty_copy()
+
+        gr3.keys = kset.intersect(self.keys, other.keys)
+
+        return gr3
+
+
+    def __iand__(self, other):
+        """ In-place set-theoretical intersection. """
+        self.keys = kset.intersect(self.keys, other.keys)
+
+        return self
+
+
+    def __or__(self, other):
+        """ Set-theoretical union. """
+        cdef Graph gr3 = self.empty_copy()
+
+        gr3.keys = kset.merge(self.keys, other.keys)
+
+        return gr3
+
+
+    def __ior__(self, other):
+        """ In-place set-theoretical union. """
+        self.keys = kset.merge(self.keys, other.keys)
+
+        return self
+
+
+    def __xor__(self, other):
+        """ Set-theoretical exclusive disjunction (XOR). """
+        cdef Graph gr3 = self.empty_copy()
+
+        gr3.keys = kset.xor(self.keys, other.keys)
+
+        return gr3
+
+
+    def __ixor__(self, other):
+        """ In-place set-theoretical exclusive disjunction (XOR). """
+        self.keys = kset.xor(self.keys, other.keys)
+
+        return self
+
+
+    def __contains__(self, trp):
+        """
+        Whether the graph contains a triple.
+
+        :rtype: boolean
+        """
+        cdef TripleKey spok
+
+        spok = [
+            self.store.to_key(trp[0]),
+            self.store.to_key(trp[1]),
+            self.store.to_key(trp[2]),
+        ]
+
+        return self.keys.contains(&spok)
+
+
+    def __iter__(self):
+        """ Graph iterator. It iterates over the set triples. """
+        yield from self.data
+
+
+    # Slicing.
+
+    def __getitem__(self, item):
+        """
+        Slicing function.
+
+        It behaves similarly to `RDFLib graph slicing
+        <https://rdflib.readthedocs.io/en/stable/utilities.html#slicing-graphs>`__
+        """
+        if isinstance(item, slice):
+            s, p, o = item.start, item.stop, item.step
+            return self._slice(s, p, o)
+        elif self.uri and isinstance(item, rdflib.term.Identifier):
+            # If a Node is given, return all values for that predicate.
+            return self._slice(self.uri, item, None)
+        else:
+            raise TypeError(f'Wrong slice format: {item}.')
+
+
+    def __hash__(self):
+        """ TODO Not that great of a hash. """
+        return id(self)
+
+
+    ## BASIC PYTHON-ACCESSIBLE SET OPERATIONS ##
+
+    def value(self, p, strict=False):
+        """
+        Get an individual value.
+
+        :param rdflib.termNode p: Predicate to search for.
+        :param bool strict: If set to ``True`` the method raises an error if
+            more than one value is found. If ``False`` (the default) only
+            the first found result is returned.
+        :rtype: rdflib.term.Node
+        """
+        if not self.uri:
+            raise ValueError('Cannot use `value` on a non-named graph.')
+
+        # TODO use slice.
+        values = {trp[2] for trp in self.lookup((self.uri, p, None))}
+
+        if strict and len(values) > 1:
+            raise RuntimeError('More than one value found for {}, {}.'.format(
+                    self.uri, p))
+
+        for ret in values:
+            return ret
+
+        return None
+
+
+    def terms_by_type(self, type):
+        """
+        Get all terms of a type: subject, predicate or object.
+
+        :param str type: One of ``s``, ``p`` or ``o``.
+        """
+        i = 'spo'.index(type)
+        return {r[i] for r in self.data}
+
+
+    def add(self, triples):
+        """
+        Add triples to the graph.
+
+        This method checks for duplicates.
+
+        :param iterable triples: iterable of 3-tuple triples.
+        """
+        cdef:
+            TripleKey spok
+
+        for s, p, o in triples:
+            #logger.info(f'Adding {s} {p} {o} to store: {self.store}')
+            spok = [
+                self.store.to_key(s),
+                self.store.to_key(p),
+                self.store.to_key(o),
+            ]
+
+            self.keys.add(&spok, True)
+
+
+    def remove(self, pattern):
+        """
+        Remove triples by pattern.
+
+        The pattern used is similar to :py:meth:`LmdbTripleStore.delete`.
+        """
+        # create an empty copy of the current object.
+        new_gr = self.empty_copy()
+
+        # Reverse lookup: only triples not matching the pattern are added to
+        # the new set.
+        self._match_ptn_callback(
+            pattern, new_gr, add_trp_callback, False
+        )
+
+        # Replace the keyset.
+        self.keys = new_gr.keys
+
+
+    ## CYTHON-ACCESSIBLE BASIC METHODS ##
+
+    cpdef Graph copy(self, str uri=None):
+        """
+        Create copy of the graph with a different (or no) URI.
+
+        :param str uri: URI of the new graph. This should be different from
+            the original.
+        """
+        cdef Graph new_gr = Graph(self.store, self.capacity, uri=uri)
+
+        new_gr.keys = self.keys.copy()
+
+        return new_gr
+
+
+    cpdef Graph empty_copy(self, str uri=None):
+        """
+        Create an empty copy with same capacity and store binding.
+
+        :param str uri: URI of the new graph. This should be different from
+            the original.
+        """
+        return Graph(self.store, self.capacity, uri=uri)
+
+
+    cpdef void set(self, tuple trp) except *:
+        """
+        Set a single value for subject and predicate.
+
+        Remove all triples matching ``s`` and ``p`` before adding ``s p o``.
+        """
+        if None in trp:
+            raise ValueError(f'Invalid triple: {trp}')
+        self.remove((trp[0], trp[1], None))
+        self.add((trp,))
+
+
+    def as_rdflib(self):
+        """
+        Return the data set as an RDFLib Graph.
+
+        :rtype: rdflib.Graph
+        """
+        gr = rdflib.Graph(identifier=self.uri)
+        for trp in self.data:
+            gr.add(trp)
+
+        return gr
+
+
+    def _slice(self, s, p, o):
+        """
+        Return terms filtered by other terms.
+
+        This behaves like the rdflib.Graph slicing policy.
+        """
+        #logger.info(f'Slicing: {s} {p} {o}')
+        # If no terms are unbound, check for containment.
+        if s is not None and p is not None and o is not None: # s p o
+            return (s, p, o) in self
+
+        # If some terms are unbound, do a lookup.
+        res = self.lookup((s, p, o))
+        #logger.info(f'Slicing results: {res}')
+        if s is not None:
+            if p is not None: # s p ?
+                return {r[2] for r in res}
+
+            if o is not None: # s ? o
+                return {r[1] for r in res}
+
+            # s ? ?
+            return {(r[1], r[2]) for r in res}
+
+        if p is not None:
+            if o is not None: # ? p o
+                return {r[0] for r in res}
+
+            # ? p ?
+            return {(r[0], r[2]) for r in res}
+
+        if o is not None: # ? ? o
+            return {(r[0], r[1]) for r in res}
+
+        # ? ? ?
+        return res
+
+
+    def lookup(self, pattern):
+        """
+        Look up triples by a pattern.
+
+        This function converts RDFLib terms into the serialized format stored
+        in the graph's internal structure and compares them bytewise.
+
+        Any and all of the lookup terms msy be ``None``.
+
+        :rtype: Graph
+        "return: New Graph instance with matching triples.
+        """
+        cdef:
+            Graph res_gr = self.empty_copy()
+
+        self._match_ptn_callback(pattern, res_gr, add_trp_callback)
+        res_gr.keys.resize()
+
+        return res_gr
+
+
+    cdef void _match_ptn_callback(
+        self, pattern, Graph gr, lookup_callback_fn_t callback_fn,
+        bint callback_cond=True, void* ctx=NULL
+    ) except *:
+        """
+        Execute an arbitrary function on a list of triples matching a pattern.
+
+        The arbitrary function is applied to each triple found in the current
+        graph, and to a discrete graph that can be the current graph itself
+        or a different one.
+
+        :param tuple pattern: A 3-tuple of rdflib terms or None.
+        :param Graph gr: The graph instance to apply the callback function to.
+        :param lookup_callback_fn_t callback_fn: A callback function to be
+            applied to the target graph using the matching triples.
+        :param bint callback_cond: Whether to apply the callback function if
+            a match is found (``True``) or if it is not found (``False``).
+        :param void* ctx: Pointer to an arbitrary object that can be used by
+            the callback function.
+        """
+        cdef:
+            kset.key_cmp_fn_t cmp_fn
+            Key k1, k2, k3
+            TripleKey spok
+
+        s, p, o = pattern
+
+        #logger.info(f'Match Callback pattern: {pattern}')
+
+        self.keys.seek()
+        # Decide comparison logic outside the loop.
+        if all(pattern):
+            if callback_cond:
+                # Shortcut for 3-term match—only if callback_cond is True.
+                spok = [
+                    self.store.to_key(s),
+                    self.store.to_key(p),
+                    self.store.to_key(o),
+                ]
+                if self.keys.contains(&spok):
+                    callback_fn(gr, &spok, ctx)
+            else:
+                # For negative condition (i.e. "apply this function to all keys
+                # except the matching one"), the whole set must be scanned.
+                #logger.info('All terms bound and negative condition.')
+                k1 = self.store.to_key(s)
+                k2 = self.store.to_key(p)
+                k3 = self.store.to_key(o)
+                #logger.info(f'Keys to match: {k1} {k2} {k3}')
+                while self.keys.get_next(&spok):
+                    #logger.info(f'Verifying spok: {spok}')
+                    if k1 != spok[0] or k2 != spok[1] or k3 != spok[2]:
+                        #logger.info(f'Calling function for spok: {spok}')
+                        callback_fn(gr, &spok, ctx)
+            return
+
+        if s is not None:
+            k1 = self.store.to_key(s)
+            if p is not None:
+                k2 = self.store.to_key(p)
+                cmp_fn = cb.lookup_skpk_cmp_fn
+            elif o is not None:
+                k2 = self.store.to_key(o)
+                cmp_fn = cb.lookup_skok_cmp_fn
+            else:
+                cmp_fn = cb.lookup_sk_cmp_fn
+        elif p is not None:
+            k1 = self.store.to_key(p)
+            if o is not None:
+                k2 = self.store.to_key(o)
+                cmp_fn = cb.lookup_pkok_cmp_fn
+            else:
+                cmp_fn = cb.lookup_pk_cmp_fn
+        elif o is not None:
+            k1 = self.store.to_key(o)
+            cmp_fn = cb.lookup_ok_cmp_fn
+        else:
+            cmp_fn = cb.lookup_none_cmp_fn
+
+        # Iterate over serialized triples.
+        while self.keys.get_next(&spok):
+            if cmp_fn(&spok, k1, k2) == callback_cond:
+                callback_fn(gr, &spok, ctx)
+
+
+
+## FACTORY METHODS
+
+def from_rdf(store=None, uri=None, *args, **kwargs):
+    """
+    Create a Graph from a serialized RDF string.
+
+    This factory function takes the same arguments as
+    :py:meth:`rdflib.Graph.parse`.
+
+    :param store: see :py:meth:`Graph.__cinit__`.
+
+    :param uri: see :py:meth:`Graph.__cinit__`.
+
+    :param *args: Positional arguments passed to RDFlib's ``parse``.
+
+    :param *kwargs: Keyword arguments passed to RDFlib's ``parse``.
+
+    :rtype: Graph
+    """
+    gr = rdflib.Graph().parse(*args, **kwargs)
+
+    return Graph(store=store, uri=uri, data={*gr})
+
+
+## LOOKUP CALLBACK FUNCTIONS
+
+cdef inline void add_trp_callback(
+    Graph gr, const TripleKey* spok_p, void* ctx
+):
+    """
+    Add a triple to a graph as a result of a lookup callback.
+    """
+    gr.keys.add(spok_p)

+ 41 - 0
lakesuperior/model/rdf/term.pxd

@@ -0,0 +1,41 @@
+from cymem.cymem cimport Pool
+
+from lakesuperior.model.base cimport Buffer
+
+#cdef extern from "regex.h" nogil:
+#   ctypedef struct regmatch_t:
+#      int rm_so
+#      int rm_eo
+#   ctypedef struct regex_t:
+#      pass
+#   int REG_NOSUB, REG_NOMATCH
+#   int regcomp(regex_t* preg, const char* regex, int cflags)
+#   int regexec(
+#       const regex_t *preg, const char* string, size_t nmatch,
+#       regmatch_t pmatch[], int eflags
+#    )
+#   void regfree(regex_t* preg)
+
+
+ctypedef struct Term:
+    char type
+    char *data
+    char *datatype
+    char *lang
+
+cdef:
+    #int term_new(
+    #    Term* term, char type, char* data, char* datatype=*, char* lang=*
+    #) except -1
+    #regex_t uri_regex
+    # Temporary TPL variable.
+    #char* _pk
+
+    int serialize(const Term *term, Buffer *sterm, Pool pool=*) except -1
+    int deserialize(const Buffer *data, Term *term) except -1
+    int from_rdflib(term_obj, Term *term) except -1
+    int serialize_from_rdflib(term_obj, Buffer *data, Pool pool=*) except -1
+    object deserialize_to_rdflib(const Buffer *data)
+    object to_rdflib(const Term *term)
+    object to_bytes(const Term *term)
+

+ 191 - 0
lakesuperior/model/rdf/term.pyx

@@ -0,0 +1,191 @@
+from uuid import uuid4
+
+from rdflib import URIRef, BNode, Literal
+
+#from cpython.mem cimport PyMem_Malloc, PyMem_Free
+from libc.stdint cimport uint64_t
+from libc.stdlib cimport free
+from libc.string cimport memcpy
+
+from cymem.cymem cimport Pool
+
+from lakesuperior.cy_include cimport cytpl as tpl
+from lakesuperior.model.base cimport Buffer, buffer_dump
+
+
+DEF LSUP_TERM_TYPE_URIREF = 1
+DEF LSUP_TERM_TYPE_BNODE = 2
+DEF LSUP_TERM_TYPE_LITERAL = 3
+DEF LSUP_TERM_PK_FMT = b'csss' # Reflects the Term structure
+DEF LSUP_TERM_STRUCT_PK_FMT = b'S(' + LSUP_TERM_PK_FMT + b')'
+# URI parsing regular expression. Conforms to RFC3986.
+#DEF URI_REGEX_STR = (
+#    b'^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?'
+#)
+
+#cdef char* ptn = URI_REGEX_STR
+#regcomp(&uri_regex, ptn, REG_NOSUB)
+# Compile with no catch groups.
+# TODO This should be properly cleaned up on application shutdown:
+# regfree(&uri_regex)
+
+#cdef int term_new(
+#    Term* term, char type, char* data, char* datatype=NULL, char* lang=NULL
+#) except -1:
+#    if regexec(&uri_regex, data, 0, NULL, 0) == REG_NOMATCH:
+#        raise ValueError('Not a valid URI.')
+#    term.type = type
+#    term.data = (
+#        data # TODO use C UUID v4 (RFC 4122) generator
+#        if term.type == LSUP_TERM_TYPE_BNODE
+#        else data
+#    )
+#    if term.type == LSUP_TERM_TYPE_LITERAL:
+#        term.datatype = datatype
+#        term.lang = lang
+#
+#    return 0
+
+
+cdef int serialize(const Term *term, Buffer *sterm, Pool pool=None) except -1:
+    """
+    Serialize a Term into a binary buffer.
+
+    The returned result is dynamically allocated in the provided memory pool.
+    """
+    cdef:
+        unsigned char *addr
+        size_t sz
+
+    tpl.tpl_jot(tpl.TPL_MEM, &addr, &sz, LSUP_TERM_STRUCT_PK_FMT, term)
+    if pool is None:
+        sterm.addr = addr
+    else:
+        # addr is within this function scope. Must be copied to the cymem pool.
+        sterm.addr = pool.alloc(sz, 1)
+        if not sterm.addr:
+            raise MemoryError()
+        memcpy(sterm.addr, addr, sz)
+    sterm.sz = sz
+
+
+cdef int deserialize(const Buffer *data, Term *term) except -1:
+    """
+    Return a term from serialized binary data.
+    """
+    #print(f'Deserializing: {buffer_dump(data)}')
+    _pk = tpl.tpl_peek(
+            tpl.TPL_MEM | tpl.TPL_DATAPEEK, data[0].addr, data[0].sz,
+            LSUP_TERM_PK_FMT, &(term[0].type), &(term[0].data),
+            &(term[0].datatype), &(term[0].lang))
+
+    if _pk is NULL:
+        raise MemoryError('Error deserializing term.')
+    else:
+        free(_pk)
+
+
+cdef int from_rdflib(term_obj, Term *term) except -1:
+    """
+    Return a Term struct obtained from a Python/RDFLib term.
+    """
+    _data = str(term_obj).encode()
+    term[0].data = _data
+
+    if isinstance(term_obj, Literal):
+        _datatype = (getattr(term_obj, 'datatype') or '').encode()
+        _lang = (getattr(term_obj, 'language') or '').encode()
+        term[0].type = LSUP_TERM_TYPE_LITERAL
+        term[0].datatype = _datatype
+        term[0].lang = _lang
+    else:
+        term[0].datatype = NULL
+        term[0].lang = NULL
+        if isinstance(term_obj, URIRef):
+            term[0].type = LSUP_TERM_TYPE_URIREF
+        elif isinstance(term_obj, BNode):
+            term[0].type = LSUP_TERM_TYPE_BNODE
+        else:
+            raise ValueError(f'Unsupported term type: {type(term_obj)}')
+
+
+cdef int serialize_from_rdflib(
+        term_obj, Buffer *data, Pool pool=None
+    ) except -1:
+    """
+    Return a Buffer struct from a Python/RDFLib term.
+    """
+
+    cdef:
+        Term _term
+        void *addr
+        size_t sz
+
+    # From RDFlib
+    _data = str(term_obj).encode()
+    _term.data = _data
+
+    if isinstance(term_obj, Literal):
+        _datatype = (getattr(term_obj, 'datatype') or '').encode()
+        _lang = (getattr(term_obj, 'language') or '').encode()
+        _term.type = LSUP_TERM_TYPE_LITERAL
+        _term.datatype = _datatype
+        _term.lang = _lang
+    else:
+        _term.datatype = NULL
+        _term.lang = NULL
+        if isinstance(term_obj, URIRef):
+            _term.type = LSUP_TERM_TYPE_URIREF
+        elif isinstance(term_obj, BNode):
+            _term.type = LSUP_TERM_TYPE_BNODE
+        else:
+            raise ValueError(
+                f'Unsupported term type: {term_obj} {type(term_obj)}'
+            )
+
+    serialize(&_term, data, pool)
+
+
+cdef object to_rdflib(const Term *term):
+    """
+    Return an RDFLib term.
+    """
+    cdef str data = (<bytes>term.data).decode()
+    if term[0].type == LSUP_TERM_TYPE_LITERAL:
+        return Literal(
+            data,
+            datatype=term.datatype if not term.lang else None,
+            lang=term.lang or None
+        )
+    else:
+        if term.type == LSUP_TERM_TYPE_URIREF:
+            return URIRef(data)
+        elif term.type == LSUP_TERM_TYPE_BNODE:
+            return BNode(data)
+        else:
+            raise IOError(f'Unknown term type code: {term[0].type}')
+
+
+cdef object deserialize_to_rdflib(const Buffer *data):
+    """
+    Return a Python/RDFLib term from a serialized Cython term.
+    """
+    cdef Term t
+
+    deserialize(data, &t)
+
+    return to_rdflib(&t)
+
+
+cdef object to_bytes(const Term *term):
+    """
+    Return a Python bytes object of the serialized term.
+    """
+    cdef:
+        Buffer pk_t
+        unsigned char *bytestream
+
+    serialize(term, &pk_t)
+    bytestream = <unsigned char *>pk_t.addr
+
+    return <bytes>(bytestream)[:pk_t.sz]

+ 19 - 0
lakesuperior/model/rdf/triple.pxd

@@ -0,0 +1,19 @@
+#from lakesuperior.cy_include cimport cytpl as tpl
+from lakesuperior.model.base cimport Buffer
+from lakesuperior.model.rdf.term cimport Term
+
+# Triple of Term structs.
+ctypedef struct Triple:
+    Term *s
+    Term *p
+    Term *o
+
+# Triple of serialized terms.
+ctypedef struct BufferTriple:
+    Buffer *s
+    Buffer *p
+    Buffer *o
+
+#cdef:
+#    int serialize(tuple trp, tpl.tpl_bin *data) except -1
+#    tuple deserialize(tpl.tpl_bin data)

+ 41 - 0
lakesuperior/model/rdf/triple.pyx

@@ -0,0 +1,41 @@
+#from lakesuperior.store.ldp_rs cimport term
+#
+#cdef int serialize(tuple trp, tpl.tpl_bin *data) except -1:
+#    """
+#    Serialize a triple expressed as a tuple of RDFlib terms.
+#
+#    :param tuple trp: 3-tuple of RDFlib terms.
+#
+#    :rtype: Triple
+#    """
+#    cdef:
+#        Triple strp
+#        Term *s
+#        Term *p
+#        Term *o
+#
+#    strp.s = s
+#    strp.p = p
+#    strp.o = o
+#
+##    term.serialize(s)
+##    term.serialize(p)
+##    term.serialize(o)
+#
+#    return strp
+#
+#
+#cdef tuple deserialize(Triple strp):
+#    """
+#    Deserialize a ``Triple`` structure into a tuple of terms.
+#
+#    :rtype: tuple
+#    """
+#    pass
+##    s = term.deserialize(strp.s.addr, strp.s.sz)
+##    p = term.deserialize(strp.p.addr, strp.p.sz)
+##    o = term.deserialize(strp.o.addr, strp.o.sz)
+##
+##    return s, p, o
+#
+#

+ 0 - 0
lakesuperior/model/structures/__init__.pxd


+ 0 - 0
lakesuperior/model/structures/__init__.py


+ 34 - 0
lakesuperior/model/structures/hash.pxd

@@ -0,0 +1,34 @@
+from libc.stdint cimport uint32_t, uint64_t
+
+from lakesuperior.model.base cimport Buffer
+
+
+# Seed for computing the term hash.
+#
+# This is a 16-byte string that will be split up into two ``uint64``
+# numbers to make up the ``spookyhash_128`` seeds.
+#
+# TODO This should be made configurable.
+DEF _TERM_HASH_SEED = \
+        b'\x72\x69\x76\x65\x72\x72\x75\x6e\x2c\x20\x70\x61\x73\x74\x20\x45'
+
+cdef enum:
+    HLEN_32 = sizeof(uint32_t)
+    HLEN_64 = sizeof(uint64_t)
+    HLEN_128 = sizeof(uint64_t) * 2
+
+ctypedef uint32_t Hash32
+ctypedef uint64_t Hash64
+ctypedef uint64_t DoubleHash64[2]
+ctypedef unsigned char Hash128[HLEN_128]
+
+cdef:
+    uint32_t term_hash_seed32
+    uint64_t term_hash_seed64_1, term_hash_seed64_2
+    unsigned char TERM_HASH_SEED[16]
+
+    int hash32(const Buffer *message, Hash32 *hash) except -1
+    int hash64(const Buffer *message, Hash64 *hash) except -1
+    int hash128(const Buffer *message, Hash128 *hash) except -1
+
+TERM_HASH_SEED = _TERM_HASH_SEED

+ 53 - 0
lakesuperior/model/structures/hash.pyx

@@ -0,0 +1,53 @@
+from libc.stdint cimport uint32_t, uint64_t
+from libc.string cimport memcpy
+
+from lakesuperior.model.base cimport Buffer
+from lakesuperior.cy_include cimport spookyhash as sph
+
+
+memcpy(&term_hash_seed32, TERM_HASH_SEED, HLEN_32)
+memcpy(&term_hash_seed64_1, TERM_HASH_SEED, HLEN_64)
+memcpy(&term_hash_seed64_2, TERM_HASH_SEED + HLEN_64, HLEN_64)
+
+
+cdef inline int hash32(const Buffer *message, Hash32 *hash) except -1:
+    """
+    Get a 32-bit (unsigned int) hash value of a byte string.
+    """
+    cdef uint32_t seed = term_hash_seed64_1
+
+    hash[0] = sph.spookyhash_32(message[0].addr, message[0].sz, seed)
+
+
+cdef inline int hash64(const Buffer *message, Hash64 *hash) except -1:
+    """
+    Get a 64-bit (unsigned long) hash value of a byte string.
+    """
+    cdef uint64_t seed = term_hash_seed32
+
+    hash[0] = sph.spookyhash_64(message[0].addr, message[0].sz, seed)
+
+
+cdef inline int hash128(const Buffer *message, Hash128 *hash) except -1:
+    """
+    Get the hash value of a byte string with a defined size.
+
+    The hashing algorithm is `SpookyHash
+    <http://burtleburtle.net/bob/hash/spooky.html>`_ which produces 128-bit
+    (16-byte) digests.
+
+    Note that this returns a char array while the smaller functions return
+    numeric types (uint, ulong).
+
+    The initial seeds are determined in the application configuration.
+
+    :rtype: Hash128
+    """
+    cdef:
+        DoubleHash64 seed = [term_hash_seed64_1, term_hash_seed64_2]
+        Hash128 digest
+
+    sph.spookyhash_128(message[0].addr, message[0].sz, seed, seed + 1)
+
+    # This casts the 2 contiguous uint64_t's into a char[16] pointer.
+    hash[0] = <Hash128>seed

+ 37 - 0
lakesuperior/model/structures/keyset.pxd

@@ -0,0 +1,37 @@
+from lakesuperior.model.base cimport (
+    Key, Key, DoubleKey, TripleKey, Buffer
+)
+
+ctypedef bint (*key_cmp_fn_t)(
+    const TripleKey* spok, const Key k1, const Key k2
+)
+
+cdef class Keyset:
+    cdef:
+        TripleKey* data
+        size_t capacity
+        size_t cur # Index cursor used to look up values.
+        size_t free_i # Index of next free slot.
+        float expand_ratio # By how much storage is automatically expanded when
+                           # full. 1 means the size doubles, 0.5 a 50%
+                           # increase. 0 means that storage won't be
+                           # automatically expanded and adding above capacity
+                           # will raise an error.
+
+        void seek(self, size_t idx=*)
+        size_t size(self)
+        size_t tell(self)
+        bint get_next(self, TripleKey* item)
+        void add(self, const TripleKey* val, bint check_dup=*) except *
+        void remove(self, const TripleKey* val) except *
+        bint contains(self, const TripleKey* val) nogil
+        Keyset copy(self)
+        Keyset sparse_copy(self)
+        void resize(self, size_t size=*) except *
+        Keyset lookup(self, const Key sk, const Key pk, const Key ok)
+
+cdef:
+    Keyset merge(Keyset ks1, Keyset ks2)
+    Keyset subtract(Keyset ks1, Keyset ks2)
+    Keyset intersect(Keyset ks1, Keyset ks2)
+    Keyset xor(Keyset ks1, Keyset ks2)

+ 364 - 0
lakesuperior/model/structures/keyset.pyx

@@ -0,0 +1,364 @@
+import logging
+
+from libc.string cimport memcmp, memcpy
+from cpython.mem cimport PyMem_Malloc, PyMem_Realloc, PyMem_Free
+from cython.parallel import prange
+
+cimport lakesuperior.model.callbacks as cb
+
+from lakesuperior.model.base cimport NULL_TRP, TRP_KLEN, TripleKey
+
+
+logger = logging.getLogger(__name__)
+
+
+cdef class Keyset:
+    """
+    Pre-allocated set of ``TripleKey``s.
+
+    The set is not checked for duplicates all the time: e.g., when creating
+    from a single set of triples coming from the store, the duplicate check
+    is turned off for efficiency. When merging with other sets, duplicate
+    checking should be turned on.
+
+    Since this class is based on a contiguous block of memory, it is best to
+    do very little manipulation. Several operations involve copying the whole
+    data block, so e.g. bulk removal and intersection are much more efficient
+    than individual record operations.
+    """
+    def __cinit__(self, size_t capacity=0, expand_ratio=.5):
+        """
+        Initialize and allocate memory for the data set.
+
+        :param size_t capacity: Number of elements to be accounted for.
+        """
+        self.capacity = capacity
+        self.expand_ratio = expand_ratio
+        self.data = <TripleKey*>PyMem_Malloc(self.capacity * TRP_KLEN)
+        if capacity and not self.data:
+            raise MemoryError('Error allocating Keyset data.')
+
+        self.cur = 0
+        self.free_i = 0
+
+
+    def __dealloc__(self):
+        """
+        Free the memory.
+
+        This is called when the Python instance is garbage collected, which
+        makes it handy to safely pass a Keyset instance across functions.
+        """
+        PyMem_Free(self.data)
+
+
+    # Access methods.
+
+    cdef void seek(self, size_t idx=0):
+        """
+        Place the cursor at a certain index, 0 by default.
+        """
+        self.cur = idx
+
+
+    cdef size_t size(self):
+        """
+        Size of the object as the number of occupied data slots.
+
+        Note that this is different from :py:data:`capacity`_, which indicates
+        the number of allocated items in memory.
+        """
+        return self.free_i
+
+
+    cdef size_t tell(self):
+        """
+        Tell the position of the cursor in the keyset.
+        """
+        return self.cur
+
+
+    cdef inline bint get_next(self, TripleKey* val):
+        """
+        Get the current value and advance the cursor by 1.
+
+        :param void *val: Addres of value returned. It is NULL if
+            the end of the buffer was reached.
+
+        :rtype: bint
+        :return: True if a value was found, False if the end of the buffer
+            has been reached.
+        """
+        if self.cur >= self.free_i:
+            return False
+
+        val[0] = self.data[self.cur]
+        self.cur += 1
+
+        return True
+
+
+    cdef void add(self, const TripleKey* val, bint check_dup=False) except *:
+        """
+        Add a triple key to the array.
+        """
+        # Check for deleted triples and optionally duplicates.
+        if val[0] == NULL_TRP or (check_dup and self.contains(val)):
+            return
+
+        if self.free_i >= self.capacity:
+            if self.expand_ratio > 0:
+                # In some edge casees, a very small ratio may round down to a
+                # zero increase, so the baseline increase is 1 element.
+                self.resize(1 + <size_t>(self.capacity * (1 + self.expand_ratio)))
+            else:
+                raise MemoryError('No space left in key set.')
+
+        self.data[self.free_i] = val[0]
+
+        self.free_i += 1
+
+
+    cdef void remove(self, const TripleKey* val) except *:
+        """
+        Remove a triple key.
+
+        This method replaces a triple with NULL_TRP if found. It
+        does not reclaim space. Therefore, if many removal operations are
+        forseen, using :py:meth:`subtract`_ is advised.
+        """
+        cdef:
+            TripleKey stored_val
+
+        self.seek()
+        while self.get_next(&stored_val):
+            #logger.info(f'Looking up for removal: {stored_val}')
+            if memcmp(val, stored_val, TRP_KLEN) == 0:
+                memcpy(&stored_val, NULL_TRP, TRP_KLEN)
+                return
+
+
+    cdef bint contains(self, const TripleKey* val) nogil:
+        """
+        Whether a value exists in the set.
+        """
+        cdef size_t i
+
+        for i in range(self.free_i):
+            # o is least likely to match.
+            if (
+                val[0][2] == self.data[i][2] and
+                val[0][0] == self.data[i][0] and
+                val[0][1] == self.data[i][1]
+            ):
+                return True
+        return False
+
+
+    cdef Keyset copy(self):
+        """
+        Copy a Keyset.
+        """
+        cdef Keyset new_ks = Keyset(
+            self.capacity, expand_ratio=self.expand_ratio
+        )
+        memcpy(new_ks.data, self.data, self.capacity * TRP_KLEN)
+        new_ks.seek()
+        new_ks.free_i = self.free_i
+
+        return new_ks
+
+
+    cdef Keyset sparse_copy(self):
+        """
+        Copy a Keyset and plug holes.
+
+        ``NULL_TRP`` values left from removing triple keys are skipped in the
+        copy and the set is shrunk to its used size.
+        """
+        cdef:
+            TripleKey val
+            Keyset new_ks = Keyset(self.capacity, self.expand_ratio)
+
+        self.seek()
+        while self.get_next(&val):
+            if val != NULL_TRP:
+                new_ks.add(&val)
+
+        new_ks.resize()
+
+        return new_ks
+
+
+    cdef void resize(self, size_t size=0) except *:
+        """
+        Change the array capacity.
+
+        :param size_t size: The new capacity size. If not specified or 0, the
+            array is shrunk to the last used item. The resulting size
+            therefore will always be greater than 0. The only exception
+            to this is if the specified size is 0 and no items have been added
+            to the array, in which case the array will be effectively shrunk
+            to 0.
+        """
+        if not size:
+            size = self.free_i
+
+        tmp = <TripleKey*>PyMem_Realloc(self.data, size * TRP_KLEN)
+
+        if not tmp:
+            raise MemoryError('Could not reallocate Keyset data.')
+
+        self.data = tmp
+        self.capacity = size
+        self.seek()
+
+
+    cdef Keyset lookup(self, const Key sk, const Key pk, const Key ok):
+        """
+        Look up triple keys.
+
+        This works in a similar way that the ``Graph`` and ``LmdbStore``
+        methods work.
+
+        Any and all the terms may be NULL. A NULL term is treated as unbound.
+
+        :param const Key* sk: s key pointer.
+        :param const Key* pk: p key pointer.
+        :param const Key* ok: o key pointer.
+        """
+        cdef:
+            TripleKey spok
+            Keyset ret = Keyset(self.capacity)
+            Key k1, k2
+            key_cmp_fn_t cmp_fn
+
+        if sk and pk and ok: # s p o
+            pass # TODO
+
+        elif sk:
+            k1 = sk
+            if pk: # s p ?
+                k2 = pk
+                cmp_fn = cb.lookup_skpk_cmp_fn
+
+            elif ok: # s ? o
+                k2 = ok
+                cmp_fn = cb.lookup_skok_cmp_fn
+
+            else: # s ? ?
+                cmp_fn = cb.lookup_sk_cmp_fn
+
+        elif pk:
+            k1 = pk
+            if ok: # ? p o
+                k2 = ok
+                cmp_fn = cb.lookup_pkok_cmp_fn
+
+            else: # ? p ?
+                cmp_fn = cb.lookup_pk_cmp_fn
+
+        elif ok: # ? ? o
+            k1 = ok
+            cmp_fn = cb.lookup_ok_cmp_fn
+
+        else: # ? ? ?
+            return self.copy()
+
+        self.seek()
+        while self.get_next(&spok):
+            if cmp_fn(&spok, k1, k2):
+                ret.add(&spok)
+
+        ret.resize()
+
+        return ret
+
+
+
+## Boolean operations.
+
+cdef Keyset merge(Keyset ks1, Keyset ks2):
+    """
+    Create a Keyset by merging an``ks2`` Keyset with the current one.
+
+    :rtype: Keyset
+    """
+    cdef:
+        TripleKey val
+        Keyset ks3 = ks1.copy()
+
+    ks2.seek()
+    while ks2.get_next(&val):
+        ks3.add(&val, True)
+
+    ks3.resize()
+
+    return ks3
+
+
+cdef Keyset subtract(Keyset ks1, Keyset ks2):
+    """
+    Create a Keyset by subtracting an``ks2`` Keyset from the current one.
+
+    :rtype: Keyset
+    """
+    cdef:
+        TripleKey val
+        Keyset ks3 = Keyset(ks1.capacity)
+
+    ks1.seek()
+    while ks1.get_next(&val):
+        if val != NULL_TRP and not ks2.contains(&val):
+            ks3.add(&val)
+
+    ks3.resize()
+
+    return ks3
+
+
+cdef Keyset intersect(Keyset ks1, Keyset ks2):
+    """
+    Create a Keyset by intersection with an``ks2`` Keyset.
+
+    :rtype: Keyset
+    """
+    cdef:
+        TripleKey val
+        Keyset ks3 = Keyset(ks1.capacity)
+
+    ks1.seek()
+    while ks1.get_next(&val):
+        if val != NULL_TRP and ks2.contains(&val):
+            ks3.add(&val)
+
+    ks3.resize()
+
+    return ks3
+
+
+cdef Keyset xor(Keyset ks1, Keyset ks2):
+    """
+    Create a Keyset by disjunction (XOR) with an``ks2`` Keyset.
+
+    :rtype: Keyset
+    """
+    cdef:
+        TripleKey val
+        Keyset ks3 = Keyset(ks1.capacity + ks2.capacity)
+
+    ks1.seek()
+    while ks1.get_next(&val):
+        if val != NULL_TRP and not ks2.contains(&val):
+            ks3.add(&val)
+
+    ks2.seek()
+    while ks2.get_next(&val):
+        if val != NULL_TRP and not ks1.contains(&val):
+            ks3.add(&val)
+
+    ks3.resize()
+
+    return ks3
+
+

+ 2 - 1
lakesuperior/store/base_lmdb_store.pxd

@@ -1,4 +1,4 @@
-cimport lakesuperior.cy_include.cylmdb as lmdb
+from lakesuperior.cy_include cimport cylmdb as lmdb
 
 
 cdef:
 cdef:
     int rc
     int rc
@@ -13,6 +13,7 @@ cdef:
 cdef class BaseLmdbStore:
 cdef class BaseLmdbStore:
     cdef:
     cdef:
         readonly bint is_txn_open
         readonly bint is_txn_open
+        readonly bint is_txn_rw
         public bint _open
         public bint _open
         unsigned int _readers
         unsigned int _readers
         readonly str env_path
         readonly str env_path

+ 50 - 5
lakesuperior/store/base_lmdb_store.pyx

@@ -27,6 +27,12 @@ cdef void _check(int rc, str message='') except *:
         raise KeyNotFoundError()
         raise KeyNotFoundError()
     if rc == lmdb.MDB_KEYEXIST:
     if rc == lmdb.MDB_KEYEXIST:
         raise KeyExistsError()
         raise KeyExistsError()
+    if rc == errno.EINVAL:
+        raise InvalidParamError(
+            'Invalid LMDB parameter error.\n'
+            'Please verify that a transaction is open and valid for the '
+            'current operation.'
+        )
     if rc != lmdb.MDB_SUCCESS:
     if rc != lmdb.MDB_SUCCESS:
         out_msg = (
         out_msg = (
                 message + '\nInternal error ({}): '.format(rc)
                 message + '\nInternal error ({}): '.format(rc)
@@ -44,6 +50,9 @@ class KeyNotFoundError(LmdbError):
 class KeyExistsError(LmdbError):
 class KeyExistsError(LmdbError):
     pass
     pass
 
 
+class InvalidParamError(LmdbError):
+    pass
+
 
 
 
 
 cdef class BaseLmdbStore:
 cdef class BaseLmdbStore:
@@ -335,22 +344,46 @@ cdef class BaseLmdbStore:
         """
         """
         Transaction context manager.
         Transaction context manager.
 
 
+        Open and close a transaction for the duration of the functions in the
+        context. If a transaction has already been opened in the store, a new
+        one is opened only if the current transaction is read-only and the new
+        requested transaction is read-write.
+
+        If a new write transaction is opened, the old one is kept on hold until
+        the new transaction is closed, then restored. All cursors are
+        invalidated and must be restored as well if one needs to reuse them.
+
         :param bool write: Whether a write transaction is to be opened.
         :param bool write: Whether a write transaction is to be opened.
 
 
         :rtype: lmdb.Transaction
         :rtype: lmdb.Transaction
         """
         """
+        cdef lmdb.MDB_txn* hold_txn
+
+        will_open = False
+
         if not self.is_open:
         if not self.is_open:
             raise LmdbError('Store is not open.')
             raise LmdbError('Store is not open.')
 
 
+        # If another transaction is open, only open the new transaction if
+        # the current one is RO and the new one RW.
         if self.is_txn_open:
         if self.is_txn_open:
-            logger.debug(
-                    'Transaction is already active. Not opening another one.')
-            #logger.debug('before yield')
-            yield
-            #logger.debug('after yield')
+            if write:
+                will_open = not self.is_txn_rw
         else:
         else:
+            will_open = True
+
+        # If a new transaction needs to be opened and replace the old one,
+        # the old one must be put on hold and swapped out when the new txn
+        # is closed.
+        if will_open:
+            will_reset = self.is_txn_open
+
+        if will_open:
             #logger.debug('Beginning {} transaction.'.format(
             #logger.debug('Beginning {} transaction.'.format(
             #    'RW' if write else 'RO'))
             #    'RW' if write else 'RO'))
+            if will_reset:
+                hold_txn = self.txn
+
             try:
             try:
                 self._txn_begin(write=write)
                 self._txn_begin(write=write)
                 self.is_txn_rw = write
                 self.is_txn_rw = write
@@ -359,9 +392,21 @@ cdef class BaseLmdbStore:
                 #logger.debug('In txn_ctx, after yield')
                 #logger.debug('In txn_ctx, after yield')
                 self._txn_commit()
                 self._txn_commit()
                 #logger.debug('after _txn_commit')
                 #logger.debug('after _txn_commit')
+                if will_reset:
+                    lmdb.mdb_txn_reset(hold_txn)
+                    self.txn = hold_txn
+                    _check(lmdb.mdb_txn_renew(self.txn))
+                    self.is_txn_rw = False
             except:
             except:
                 self._txn_abort()
                 self._txn_abort()
                 raise
                 raise
+        else:
+            logger.info(
+                'Transaction is already active. Not opening another one.'
+            )
+            #logger.debug('before yield')
+            yield
+            #logger.debug('after yield')
 
 
 
 
     def begin(self, write=False):
     def begin(self, write=False):

+ 0 - 0
lakesuperior/store/ldp_rs/__init__.pxd


+ 0 - 53
lakesuperior/store/ldp_rs/lmdb_store.py

@@ -199,56 +199,3 @@ class LmdbStore(LmdbTriplestore, Store):
 
 
 
 
     ## PRIVATE METHODS ##
     ## PRIVATE METHODS ##
-
-    def _normalize_context(self, context):
-        """
-        Normalize a context parameter to conform to the model expectations.
-
-        :param context: Context URI or graph.
-        :type context: URIRef or Graph or None
-        """
-        if isinstance(context, Graph):
-            if context == self or isinstance(context.identifier, Variable):
-                context = None
-            else:
-                context = context.identifier
-                #logger.debug('Converted graph into URI: {}'.format(context))
-
-        return context
-
-
-    ## Convenience methods—not necessary for functioning but useful for
-    ## debugging.
-
-    #def _keys_in_ctx(self, pk_ctx):
-    #    """
-    #    Convenience method to list all keys in a context.
-
-    #    :param bytes pk_ctx: Pickled context URI.
-
-    #    :rtype: Iterator(tuple)
-    #    :return: Generator of triples.
-    #    """
-    #    with self.cur('c:spo') as cur:
-    #        if cur.set_key(pk_ctx):
-    #            tkeys = cur.iternext_dup()
-    #            return {self._key_to_triple(tk) for tk in tkeys}
-    #        else:
-    #            return set()
-
-
-    #def _ctx_for_key(self, tkey):
-    #    """
-    #    Convenience method to list all contexts that a key is in.
-
-    #    :param bytes tkey: Triple key.
-
-    #    :rtype: Iterator(rdflib.URIRef)
-    #    :return: Generator of context URIs.
-    #    """
-    #    with self.cur('spo:c') as cur:
-    #        if cur.set_key(tkey):
-    #            ctx = cur.iternext_dup()
-    #            return {self._unpickle(c) for c in ctx}
-    #        else:
-    #            return set()

+ 45 - 0
lakesuperior/store/ldp_rs/lmdb_triplestore.pxd

@@ -0,0 +1,45 @@
+cimport lakesuperior.cy_include.collections as cc
+cimport lakesuperior.cy_include.cylmdb as lmdb
+
+from lakesuperior.model.base cimport Key, DoubleKey, TripleKey, Buffer
+from lakesuperior.model.rdf.graph cimport Graph
+from lakesuperior.model.structures.keyset cimport Keyset
+from lakesuperior.store.base_lmdb_store cimport BaseLmdbStore
+
+cdef:
+    enum:
+        IDX_OP_ADD = 1
+        IDX_OP_REMOVE = -1
+
+    unsigned char lookup_rank[3]
+    unsigned char lookup_ordering[3][3]
+    unsigned char lookup_ordering_2bound[3][3]
+
+
+
+cdef class LmdbTriplestore(BaseLmdbStore):
+    cpdef dict stats(self)
+    cpdef size_t _len(self, context=*) except -1
+    cpdef void add(self, triple, context=*, quoted=*) except *
+    cpdef void add_graph(self, graph) except *
+    cpdef void _remove(self, tuple triple_pattern, context=*) except *
+    cpdef void _remove_graph(self, object gr_uri) except *
+    cpdef tuple all_namespaces(self)
+    cpdef Graph triple_keys(self, tuple triple_pattern, context=*, uri=*)
+
+    cdef:
+        void _index_triple(self, int op, TripleKey spok) except *
+        void _all_term_keys(self, term_type, cc.HashSet** tkeys) except *
+        void lookup_term(self, const Key tk, Buffer* data) except *
+        Graph _lookup(self, tuple triple_pattern)
+        Graph _lookup_1bound(self, unsigned char idx, Key luk)
+        Graph _lookup_2bound(
+            self, unsigned char idx1, unsigned char idx2, DoubleKey tks
+        )
+        object from_key(self, const Key tk)
+        Key to_key(self, term) except? 0
+        void all_contexts(self, Key** ctx, size_t* sz, triple=*) except *
+        Key _append(
+                self, Buffer *value,
+                unsigned char *dblabel=*, lmdb.MDB_txn *txn=*,
+                unsigned int flags=*) except? 0

File diff suppressed because it is too large
+ 106 - 746
lakesuperior/store/ldp_rs/lmdb_triplestore.pyx


+ 47 - 39
lakesuperior/store/ldp_rs/rsrc_centric_layout.py

@@ -9,7 +9,7 @@ from urllib.parse import urldefrag
 
 
 import arrow
 import arrow
 
 
-from rdflib import Dataset, Graph, Literal, URIRef, plugin
+from rdflib import Dataset, Literal, URIRef, plugin
 from rdflib.compare import to_isomorphic
 from rdflib.compare import to_isomorphic
 from rdflib.namespace import RDF
 from rdflib.namespace import RDF
 from rdflib.query import ResultException
 from rdflib.query import ResultException
@@ -24,7 +24,7 @@ from lakesuperior.dictionaries.srv_mgd_terms import  srv_mgd_subjects, \
 from lakesuperior.globals import ROOT_RSRC_URI
 from lakesuperior.globals import ROOT_RSRC_URI
 from lakesuperior.exceptions import (InvalidResourceError,
 from lakesuperior.exceptions import (InvalidResourceError,
         ResourceNotExistsError, TombstoneError, PathSegmentError)
         ResourceNotExistsError, TombstoneError, PathSegmentError)
-from lakesuperior.store.ldp_rs.lmdb_triplestore import SimpleGraph, Imr
+from lakesuperior.model.rdf.graph import Graph
 
 
 
 
 META_GR_URI = nsc['fcsystem']['meta']
 META_GR_URI = nsc['fcsystem']['meta']
@@ -217,13 +217,15 @@ class RsrcCentricLayout:
         fname = path.join(
         fname = path.join(
                 basedir, 'data', 'bootstrap', 'rsrc_centric_layout.sparql')
                 basedir, 'data', 'bootstrap', 'rsrc_centric_layout.sparql')
         with store.txn_ctx(True):
         with store.txn_ctx(True):
+            #import pdb; pdb.set_trace()
             with open(fname, 'r') as f:
             with open(fname, 'r') as f:
                 data = Template(f.read())
                 data = Template(f.read())
                 self.ds.update(data.substitute(timestamp=arrow.utcnow()))
                 self.ds.update(data.substitute(timestamp=arrow.utcnow()))
+        with store.txn_ctx():
             imr = self.get_imr('/', incl_inbound=False, incl_children=True)
             imr = self.get_imr('/', incl_inbound=False, incl_children=True)
 
 
-        gr = Graph(identifier=imr.uri)
-        gr += imr.data
+        #gr = Graph(identifier=imr.uri)
+        #gr += imr.data
         #checksum = to_isomorphic(gr).graph_digest()
         #checksum = to_isomorphic(gr).graph_digest()
         #digest = sha256(str(checksum).encode('ascii')).digest()
         #digest = sha256(str(checksum).encode('ascii')).digest()
 
 
@@ -249,10 +251,9 @@ class RsrcCentricLayout:
         :param rdflib.term.URIRef ctx: URI of the optional context. If None,
         :param rdflib.term.URIRef ctx: URI of the optional context. If None,
             all named graphs are queried.
             all named graphs are queried.
 
 
-        :rtype: SimpleGraph
+        :rtype: Graph
         """
         """
-        return SimpleGraph(
-                store=self.store, lookup=((subject, None, None), ctx))
+        return self.store.triple_keys((subject, None, None), ctx)
 
 
 
 
     def count_rsrc(self):
     def count_rsrc(self):
@@ -291,15 +292,18 @@ class RsrcCentricLayout:
         if not incl_children:
         if not incl_children:
             contexts.remove(nsc['fcstruct'][uid])
             contexts.remove(nsc['fcstruct'][uid])
 
 
-        imr = Imr(uri=nsc['fcres'][uid])
+        imr = Graph(self.store, uri=nsc['fcres'][uid])
 
 
         for ctx in contexts:
         for ctx in contexts:
-            imr |= SimpleGraph(
-                    lookup=((None, None, None), ctx), store=self.store).data
+            gr = self.store.triple_keys((None, None, None), ctx)
+            imr |= gr
 
 
         # Include inbound relationships.
         # Include inbound relationships.
         if incl_inbound and len(imr):
         if incl_inbound and len(imr):
-            imr |= set(self.get_inbound_rel(nsc['fcres'][uid]))
+            gr = Graph(
+                self.store, data={*self.get_inbound_rel(nsc['fcres'][uid])}
+            )
+            imr |= gr
 
 
         if strict:
         if strict:
             self._check_rsrc_status(imr)
             self._check_rsrc_status(imr)
@@ -331,10 +335,11 @@ class RsrcCentricLayout:
         logger.debug('Getting metadata for: {}'.format(uid))
         logger.debug('Getting metadata for: {}'.format(uid))
         if ver_uid:
         if ver_uid:
             uid = self.snapshot_uid(uid, ver_uid)
             uid = self.snapshot_uid(uid, ver_uid)
-        imr = Imr(
-                uri=nsc['fcres'][uid],
-                lookup=((None, None, None), nsc['fcadmin'][uid]),
-                store=self.store)
+        imr = self.store.triple_keys(
+            (None, None, None),
+            context=nsc['fcadmin'][uid],
+            uri=nsc['fcres'][uid]
+        )
 
 
         if strict:
         if strict:
             self._check_rsrc_status(imr)
             self._check_rsrc_status(imr)
@@ -353,9 +358,11 @@ class RsrcCentricLayout:
         # graph. If multiple user-provided graphs will be supported, this
         # graph. If multiple user-provided graphs will be supported, this
         # should use another query to get all of them.
         # should use another query to get all of them.
         uri = nsc['fcres'][uid]
         uri = nsc['fcres'][uid]
-        userdata = Imr(
-                uri=uri, lookup=((uri, None, None),nsc['fcmain'][uid]),
-                store=self.store)
+        userdata = self.store.triple_keys(
+            (None, None, None),
+            context=nsc['fcmain'][uid],
+            uri=uri
+        )
 
 
         return userdata
         return userdata
 
 
@@ -365,36 +372,34 @@ class RsrcCentricLayout:
         Get all metadata about a resource's versions.
         Get all metadata about a resource's versions.
 
 
         :param string uid: Resource UID.
         :param string uid: Resource UID.
-        :rtype: SimpleGraph
+        :rtype: Graph
         """
         """
         # **Note:** This pretty much bends the ontology—it replaces the graph
         # **Note:** This pretty much bends the ontology—it replaces the graph
         # URI with the subject URI. But the concepts of data and metadata in
         # URI with the subject URI. But the concepts of data and metadata in
         # Fedora are quite fluid anyways...
         # Fedora are quite fluid anyways...
 
 
-        # Result graph.
-        imr = SimpleGraph(lookup=(
-            (nsc['fcres'][uid], nsc['fcrepo'].hasVersion, None),
-                nsc['fcadmin'][uid]), store=self.store)
-
-        vmeta = Imr(uri=nsc['fcres'][uid])
+        vmeta = Graph(self.store, uri=nsc['fcres'][uid])
 
 
         #Get version graphs proper.
         #Get version graphs proper.
-        for vtrp in imr:
+        for vtrp in self.store.triple_keys(
+            (nsc['fcres'][uid], nsc['fcrepo'].hasVersion, None),
+            nsc['fcadmin'][uid]
+        ):
             # Add the hasVersion triple to the result graph.
             # Add the hasVersion triple to the result graph.
-            vmeta.add(vtrp)
-            vmeta_gr = SimpleGraph(
-                lookup=((
-                    None, nsc['foaf'].primaryTopic, vtrp[2]), HIST_GR_URI),
-                store=self.store)
+            vmeta.add((vtrp,))
+            vmeta_gr = self.store.triple_keys(
+                (None, nsc['foaf'].primaryTopic, vtrp[2]), HIST_GR_URI
+            )
             # Get triples in the meta graph filtering out undesired triples.
             # Get triples in the meta graph filtering out undesired triples.
             for vmtrp in vmeta_gr:
             for vmtrp in vmeta_gr:
-                for trp in SimpleGraph(lookup=((
-                        vmtrp[0], None, None), HIST_GR_URI), store=self.store):
+                for trp in self.store.triple_keys(
+                    (vmtrp[0], None, None), HIST_GR_URI
+                ):
                     if (
                     if (
                             (trp[1] != nsc['rdf'].type
                             (trp[1] != nsc['rdf'].type
                             or trp[2] not in self.ignore_vmeta_types)
                             or trp[2] not in self.ignore_vmeta_types)
                             and (trp[1] not in self.ignore_vmeta_preds)):
                             and (trp[1] not in self.ignore_vmeta_preds)):
-                        vmeta.add((vtrp[2], trp[1], trp[2]))
+                        vmeta.add(((vtrp[2], trp[1], trp[2]),))
 
 
         return vmeta
         return vmeta
 
 
@@ -414,6 +419,7 @@ class RsrcCentricLayout:
         :return: Inbound triples or subjects.
         :return: Inbound triples or subjects.
         """
         """
         # Only return non-historic graphs.
         # Only return non-historic graphs.
+        # TODO self.store.triple_keys?
         meta_gr = self.ds.graph(META_GR_URI)
         meta_gr = self.ds.graph(META_GR_URI)
         ptopic_uri = nsc['foaf'].primaryTopic
         ptopic_uri = nsc['foaf'].primaryTopic
 
 
@@ -439,8 +445,9 @@ class RsrcCentricLayout:
         ctx_uri = nsc['fcstruct'][uid]
         ctx_uri = nsc['fcstruct'][uid]
         cont_p = nsc['ldp'].contains
         cont_p = nsc['ldp'].contains
         def _recurse(dset, s, c):
         def _recurse(dset, s, c):
-            new_dset = SimpleGraph(
-                    lookup=((s, cont_p, None), c), store=self.store)[s : cont_p]
+            new_dset = self.store.triple_keys(
+                (s, cont_p, None), c
+            )[s : cont_p]
             #new_dset = set(ds.graph(c)[s : cont_p])
             #new_dset = set(ds.graph(c)[s : cont_p])
             for ss in new_dset:
             for ss in new_dset:
                 dset.add(ss)
                 dset.add(ss)
@@ -459,9 +466,9 @@ class RsrcCentricLayout:
             return _recurse(set(), subj_uri, ctx_uri)
             return _recurse(set(), subj_uri, ctx_uri)
         else:
         else:
             #return ds.graph(ctx_uri)[subj_uri : cont_p : ])
             #return ds.graph(ctx_uri)[subj_uri : cont_p : ])
-            return SimpleGraph(
-                    lookup=((subj_uri, cont_p, None), ctx_uri),
-                    store=self.store)[subj_uri : cont_p]
+            return self.store.triple_keys(
+                (subj_uri, cont_p, None), ctx_uri
+            )[subj_uri : cont_p]
 
 
 
 
     def get_last_version_uid(self, uid):
     def get_last_version_uid(self, uid):
@@ -670,6 +677,7 @@ class RsrcCentricLayout:
         """
         """
         Check if a resource is not existing or if it is a tombstone.
         Check if a resource is not existing or if it is a tombstone.
         """
         """
+        #import pdb; pdb.set_trace()
         uid = self.uri_to_uid(imr.uri)
         uid = self.uri_to_uid(imr.uri)
         if not len(imr):
         if not len(imr):
             raise ResourceNotExistsError(uid)
             raise ResourceNotExistsError(uid)

+ 0 - 41
lakesuperior/store/ldp_rs/term.pxd

@@ -1,41 +0,0 @@
-from libc.stdint cimport uint64_t
-
-# cdefs for serialize and deserialize methods
-cdef:
-    #unsigned char *pack_data
-    unsigned char term_type
-    unsigned char *pack_fmt
-    unsigned char *term_data
-    unsigned char *term_datatype
-    unsigned char *term_lang
-    #size_t pack_size
-
-    struct IdentifierTerm:
-        char type
-        unsigned char *data
-
-    struct LiteralTerm:
-        char type
-        unsigned char *data
-        unsigned char *datatype
-        unsigned char *lang
-
-    int serialize(term, unsigned char **pack_data, size_t *pack_size) except -1
-    deserialize(unsigned char *data, size_t size)
-
-
-# cdefs for hash methods
-DEF _HLEN = 16
-
-ctypedef uint64_t Hash_128[2]
-ctypedef unsigned char Hash[_HLEN]
-
-cdef:
-    uint64_t term_hash_seed1
-    uint64_t term_hash_seed2
-    unsigned char *term_hash_seed
-    size_t SEED_LEN
-    size_t HLEN
-
-    void hash_(
-        const unsigned char *message, size_t message_size, Hash *digest)

+ 0 - 134
lakesuperior/store/ldp_rs/term.pyx

@@ -1,134 +0,0 @@
-from rdflib import URIRef, BNode, Literal
-
-#from cpython.mem cimport PyMem_Malloc, PyMem_Free
-from libc.stdint cimport uint64_t
-from libc.stdlib cimport malloc, free
-from libc.string cimport memcpy
-
-#from lakesuperior.cy_include.cyspookyhash cimport spookyhash_128
-from lakesuperior.cy_include cimport cytpl as tpl
-
-
-DEF LSUP_TERM_TYPE_URIREF = 1
-DEF LSUP_TERM_TYPE_BNODE = 2
-DEF LSUP_TERM_TYPE_LITERAL = 3
-DEF LSUP_PK_FMT_ID = b'S(cs)'
-DEF LSUP_PK_FMT_LIT = b'S(csss)'
-
-
-DEF _SEED_LEN = 8
-DEF _HLEN = 16
-
-HLEN = _HLEN
-SEED_LEN = _SEED_LEN
-
-term_hash_seed = b'\xff\xf2Q\xf2j\x0bG\xc1\x8a}\xca\x92\x98^y\x12'
-"""
-Seed for computing the term hash.
-
-This is a 16-byte string that will be split up into two ``uint64``
-numbers to make up the ``spookyhash_128`` seeds.
-"""
-memcpy(&term_hash_seed1, term_hash_seed, SEED_LEN)
-memcpy(&term_hash_seed2, term_hash_seed + SEED_LEN, SEED_LEN)
-
-# We only need one function from spookyhash. No need for a pxd file.
-cdef extern from 'spookyhash_api.h':
-    void spookyhash_128(
-            const void *input, size_t input_size, uint64_t *hash_1,
-            uint64_t *hash_2)
-
-
-cdef int serialize(
-        term, unsigned char **pack_data, size_t *pack_size) except -1:
-    cdef:
-        bytes term_data = term.encode()
-        bytes term_datatype
-        bytes term_lang
-        IdentifierTerm id_t
-        LiteralTerm lit_t
-
-    if isinstance(term, Literal):
-        term_datatype = (getattr(term, 'datatype') or '').encode()
-        term_lang = (getattr(term, 'language') or '').encode()
-
-        lit_t.type = LSUP_TERM_TYPE_LITERAL
-        lit_t.data = term_data
-        lit_t.datatype = <unsigned char *>term_datatype
-        lit_t.lang = <unsigned char *>term_lang
-
-        tpl.tpl_jot(tpl.TPL_MEM, pack_data, pack_size, LSUP_PK_FMT_LIT, &lit_t)
-    else:
-        if isinstance(term, URIRef):
-            id_t.type = LSUP_TERM_TYPE_URIREF
-        elif isinstance(term, BNode):
-            id_t.type = LSUP_TERM_TYPE_BNODE
-        else:
-            raise ValueError(f'Unsupported term type: {type(term)}')
-        id_t.data = term_data
-        tpl.tpl_jot(tpl.TPL_MEM, pack_data, pack_size, LSUP_PK_FMT_ID, &id_t)
-
-
-cdef deserialize(const unsigned char *data, const size_t data_size):
-    cdef:
-        char term_type
-        char *fmt = NULL
-        char *_pk = NULL
-        unsigned char *term_data = NULL
-        unsigned char *term_lang = NULL
-        unsigned char *term_datatype = NULL
-
-    datatype = None
-    lang = None
-
-    fmt = tpl.tpl_peek(tpl.TPL_MEM, data, data_size)
-    try:
-        if fmt == LSUP_PK_FMT_LIT:
-            _pk = tpl.tpl_peek(
-                    tpl.TPL_MEM | tpl.TPL_DATAPEEK, data, data_size, b'csss',
-                    &term_type, &term_data, &term_datatype, &term_lang)
-            if len(term_datatype) > 0:
-                datatype = term_datatype.decode()
-            elif len(term_lang) > 0:
-                lang = term_lang.decode()
-
-            return Literal(term_data.decode(), datatype=datatype, lang=lang)
-
-        elif fmt == LSUP_PK_FMT_ID:
-            _pk = tpl.tpl_peek(
-                    tpl.TPL_MEM | tpl.TPL_DATAPEEK, data, data_size, b'cs',
-                    &term_type, &term_data)
-            uri = term_data.decode()
-            if term_type == LSUP_TERM_TYPE_URIREF:
-                return URIRef(uri)
-            elif term_type == LSUP_TERM_TYPE_BNODE:
-                return BNode(uri)
-            else:
-                raise IOError(f'Unknown term type code: {term_type}')
-        else:
-            msg = f'Unknown structure pack format: {fmt}'
-            raise IOError(msg)
-    finally:
-        free(term_data)
-        free(term_datatype)
-        free(term_lang)
-        free(_pk)
-        free(fmt)
-
-
-cdef inline void hash_(
-        const unsigned char *message, size_t message_size, Hash *digest):
-    """
-    Get the hash value of a serialized object.
-
-    The hashing algorithm is `SpookyHash
-    <http://burtleburtle.net/bob/hash/spooky.html>`_ which produces 128-bit
-    (16-byte) digests.
-
-    The initial seeds are determined in the application configuration.
-    """
-    cdef Hash_128 seed = [term_hash_seed1, term_hash_seed2]
-
-    spookyhash_128(message, message_size, seed, seed + 1)
-
-    memcpy(digest, seed, sizeof(Hash))

+ 13 - 0
lakesuperior/toolbox.py

@@ -194,6 +194,19 @@ class Toolbox:
         return s, p, o
         return s, p, o
 
 
 
 
+    def globalize_imr(self, imr):
+        '''
+        Globalize an Imr.
+
+        :rtype: rdflib.Graph
+        '''
+        g_gr = Graph(identifier=self.globalize_term(imr.uri))
+        for trp in imr:
+            g_gr.add(self.globalize_triple(trp))
+
+        return g_gr
+
+
     def globalize_graph(self, gr):
     def globalize_graph(self, gr):
         '''
         '''
         Globalize a graph.
         Globalize a graph.

+ 112 - 29
lakesuperior/util/benchmark.py

@@ -1,32 +1,52 @@
 #!/usr/bin/env python3
 #!/usr/bin/env python3
 
 
+import logging
 import sys
 import sys
 
 
+from os import path
 from uuid import uuid4
 from uuid import uuid4
 
 
 import arrow
 import arrow
 import click
 import click
+import rdflib
 import requests
 import requests
 
 
 from matplotlib import pyplot as plt
 from matplotlib import pyplot as plt
 
 
 from lakesuperior.util.generators import (
 from lakesuperior.util.generators import (
         random_image, random_graph, random_utf8_string)
         random_image, random_graph, random_utf8_string)
+from lakesuperior.exceptions import ResourceNotExistsError
 
 
 __doc__ = '''
 __doc__ = '''
 Benchmark script to measure write performance.
 Benchmark script to measure write performance.
 '''
 '''
 
 
+def_mode = 'ldp'
 def_endpoint = 'http://localhost:8000/ldp'
 def_endpoint = 'http://localhost:8000/ldp'
 def_ct = 10000
 def_ct = 10000
 def_parent = '/pomegranate'
 def_parent = '/pomegranate'
 def_gr_size = 200
 def_gr_size = 200
 
 
+logging.disable(logging.WARN)
+
 
 
 @click.command()
 @click.command()
+@click.option(
+    '--mode', '-m', default=def_mode,
+    help=(
+        'Mode of ingestion. One of `ldp`, `python`. With the former, the '
+        'HTTP/LDP web server is used. With the latter, the Python API is '
+        'used, in which case the server need not be running. '
+        f'Default: {def_endpoint}'
+    )
+)
 @click.option(
 @click.option(
     '--endpoint', '-e', default=def_endpoint,
     '--endpoint', '-e', default=def_endpoint,
-    help=f'LDP endpoint. Default: {def_endpoint}')
+    help=(
+        'LDP endpoint. Only meaningful with `ldp` mode. '
+        f'Default: {def_endpoint}'
+    )
+)
 @click.option(
 @click.option(
     '--count', '-c', default=def_ct,
     '--count', '-c', default=def_ct,
     help='Number of resources to ingest. Default: {def_ct}')
     help='Number of resources to ingest. Default: {def_ct}')
@@ -40,9 +60,12 @@ def_gr_size = 200
     help='Delete container resource and its children if already existing. By '
     help='Delete container resource and its children if already existing. By '
     'default, the container is not deleted and new resources are added to it.')
     'default, the container is not deleted and new resources are added to it.')
 @click.option(
 @click.option(
-    '--method', '-m', default='put',
-    help='HTTP method to use. Case insensitive. Either PUT '
-    f'or POST. Default: PUT')
+    '--method', '-X', default='put',
+    help=(
+        'HTTP method to use. Case insensitive. Either PUT or POST. '
+        'Default: PUT'
+    )
+)
 @click.option(
 @click.option(
     '--graph-size', '-s', default=def_gr_size,
     '--graph-size', '-s', default=def_gr_size,
     help=f'Number of triples in each graph. Default: {def_gr_size}')
     help=f'Number of triples in each graph. Default: {def_gr_size}')
@@ -52,47 +75,78 @@ def_gr_size = 200
     '`n` (only  LDP-NR, i.e. binaries), or `b` (50/50% of both). '
     '`n` (only  LDP-NR, i.e. binaries), or `b` (50/50% of both). '
     'Default: r')
     'Default: r')
 @click.option(
 @click.option(
-    '--graph', '-g', is_flag=True, help='Plot a graph of ingest timings. '
+    '--plot', '-P', is_flag=True, help='Plot a graph of ingest timings. '
     'The graph figure is displayed on screen with basic manipulation and save '
     'The graph figure is displayed on screen with basic manipulation and save '
     'options.')
     'options.')
 
 
 def run(
 def run(
-        endpoint, count, parent, method, delete_container,
-        graph_size, resource_type, graph):
-
-    container_uri = endpoint + parent
+    mode, endpoint, count, parent, method, delete_container,
+    graph_size, resource_type, plot
+):
+    """
+    Run the benchmark.
+    """
 
 
     method = method.lower()
     method = method.lower()
     if method not in ('post', 'put'):
     if method not in ('post', 'put'):
-        raise ValueError(f'HTTP method not supported: {method}')
+        raise ValueError(f'Insertion method not supported: {method}')
+
+    mode = mode.lower()
+    if mode == 'ldp':
+        parent = '{}/{}'.format(endpoint.strip('/'), parent.strip('/'))
+
+        if delete_container:
+            print('Removing previously existing container.')
+            requests.delete(parent, headers={'prefer': 'no-tombstone'})
+        requests.put(parent)
 
 
-    if delete_container:
-        requests.delete(container_uri, headers={'prefer': 'no-tombstone'})
-    requests.put(container_uri)
+    elif mode == 'python':
+        from lakesuperior import env_setup
+        from lakesuperior.api import resource as rsrc_api
+
+        if delete_container:
+            try:
+                print('Removing previously existing container.')
+                rsrc_api.delete(parent, soft=False)
+            except ResourceNotExistsError:
+                pass
+        rsrc_api.create_or_replace(parent)
+    else:
+        raise ValueError(f'Mode not supported: {mode}')
 
 
-    print(f'Inserting {count} children under {container_uri}.')
 
 
     # URI used to establish an in-repo relationship. This is set to
     # URI used to establish an in-repo relationship. This is set to
     # the most recently created resource in each loop.
     # the most recently created resource in each loop.
-    ref = container_uri
+    ref = parent
+
+    print(f'Inserting {count} children under {parent}.')
 
 
     wclock_start = arrow.utcnow()
     wclock_start = arrow.utcnow()
-    if graph:
+    if plot:
         print('Results will be plotted.')
         print('Results will be plotted.')
         # Plot coordinates: X is request count, Y is request timing.
         # Plot coordinates: X is request count, Y is request timing.
         px = []
         px = []
         py = []
         py = []
         plt.xlabel('Requests')
         plt.xlabel('Requests')
         plt.ylabel('ms per request')
         plt.ylabel('ms per request')
-        plt.title('FCREPO Benchmark')
+        plt.title('Lakesuperior / FCREPO Benchmark')
 
 
     try:
     try:
         for i in range(1, count + 1):
         for i in range(1, count + 1):
-            url = '{}/{}'.format(container_uri, uuid4()) if method == 'put' \
-                    else container_uri
+            #import pdb; pdb.set_trace()
+            if mode == 'ldp':
+                dest = (
+                    f'{parent}/{uuid4()}' if method == 'put'
+                    else parent
+                )
+            else:
+                dest = (
+                    path.join(parent, str(uuid4()))
+                    if method == 'put' else parent
+                )
 
 
             if resource_type == 'r' or (resource_type == 'b' and i % 2 == 0):
             if resource_type == 'r' or (resource_type == 'b' and i % 2 == 0):
-                data = random_graph(graph_size, ref).serialize(format='ttl')
+                data = random_graph(graph_size, ref)
                 headers = {'content-type': 'text/turtle'}
                 headers = {'content-type': 'text/turtle'}
             else:
             else:
                 img = random_image(name=uuid4(), ts=16, ims=512)
                 img = random_image(name=uuid4(), ts=16, ims=512)
@@ -103,19 +157,21 @@ def run(
                         'content-disposition': 'attachment; filename="{}"'
                         'content-disposition': 'attachment; filename="{}"'
                             .format(uuid4())}
                             .format(uuid4())}
 
 
-            #import pdb; pdb.set_trace()
             # Start timing after generating the data.
             # Start timing after generating the data.
             ckpt = arrow.utcnow()
             ckpt = arrow.utcnow()
             if i == 1:
             if i == 1:
                 tcounter = ckpt - ckpt
                 tcounter = ckpt - ckpt
                 prev_tcounter = tcounter
                 prev_tcounter = tcounter
 
 
-            rsp = requests.request(method, url, data=data, headers=headers)
-            tdelta = arrow.utcnow() - ckpt
-            tcounter += tdelta
+            ref = (
+                _ingest_graph_ldp(
+                    method, dest, data.serialize(format='ttl'), headers, ref
+                )
+                if mode == 'ldp'
+                else _ingest_graph_py(method, dest, data, ref)
+            )
+            tcounter += (arrow.utcnow() - ckpt)
 
 
-            rsp.raise_for_status()
-            ref = rsp.headers['location']
             if i % 10 == 0:
             if i % 10 == 0:
                 avg10 = (tcounter - prev_tcounter) / 10
                 avg10 = (tcounter - prev_tcounter) / 10
                 print(
                 print(
@@ -123,7 +179,7 @@ def run(
                     f'Per resource: {avg10}')
                     f'Per resource: {avg10}')
                 prev_tcounter = tcounter
                 prev_tcounter = tcounter
 
 
-                if graph:
+                if plot:
                     px.append(i)
                     px.append(i)
                     # Divide by 1000 for µs → ms
                     # Divide by 1000 for µs → ms
                     py.append(avg10.microseconds // 1000)
                     py.append(avg10.microseconds // 1000)
@@ -136,7 +192,7 @@ def run(
     print(f'Total time spent ingesting resources: {tcounter}')
     print(f'Total time spent ingesting resources: {tcounter}')
     print(f'Average time per resource: {tcounter.total_seconds()/i}')
     print(f'Average time per resource: {tcounter.total_seconds()/i}')
 
 
-    if graph:
+    if plot:
         if resource_type == 'r':
         if resource_type == 'r':
             type_label = 'LDP-RS'
             type_label = 'LDP-RS'
         elif resource_type == 'n':
         elif resource_type == 'n':
@@ -144,12 +200,39 @@ def run(
         else:
         else:
             type_label = 'LDP-RS + LDP-NR'
             type_label = 'LDP-RS + LDP-NR'
         label = (
         label = (
-            f'{container_uri}; {method.upper()}; {graph_size} trp/graph; '
+            f'{parent}; {method.upper()}; {graph_size} trp/graph; '
             f'{type_label}')
             f'{type_label}')
         plt.plot(px, py, label=label)
         plt.plot(px, py, label=label)
         plt.legend()
         plt.legend()
         plt.show()
         plt.show()
 
 
 
 
+def _ingest_graph_ldp(method, uri, data, headers, ref):
+    """
+    Ingest the graph via HTTP/LDP.
+    """
+    rsp = requests.request(method, uri, data=data, headers=headers)
+    rsp.raise_for_status()
+    return rsp.headers['location']
+
+
+def _ingest_graph_py(method, dest, data, ref):
+    from lakesuperior.api import resource as rsrc_api
+
+    kwargs = {}
+    if isinstance(data, rdflib.Graph):
+        kwargs['graph'] = data
+    else:
+        kwargs['stream'] = data
+        kwargs['mimetype'] = 'image/png'
+
+    if method == 'put':
+        _, rsrc = rsrc_api.create_or_replace(dest, **kwargs)
+    else:
+        _, rsrc = rsrc_api.create(dest, **kwargs)
+
+    return rsrc.uid
+
+
 if __name__ == '__main__':
 if __name__ == '__main__':
     run()
     run()

+ 2 - 1
requirements_dev.txt

@@ -1,5 +1,5 @@
 CoilMQ>=1.0.1
 CoilMQ>=1.0.1
-Cython==0.29
+Cython==0.29.6
 Flask>=0.12.2
 Flask>=0.12.2
 HiYaPyCo>=0.4.11
 HiYaPyCo>=0.4.11
 Pillow>=4.3.0
 Pillow>=4.3.0
@@ -9,6 +9,7 @@ click-log>=0.2.1
 click>=6.7
 click>=6.7
 gevent>=1.3.6
 gevent>=1.3.6
 gunicorn>=19.7.1
 gunicorn>=19.7.1
+matplotlib
 numpy>=1.15.1
 numpy>=1.15.1
 pytest-flask
 pytest-flask
 pytest>=3.2.2
 pytest>=3.2.2

+ 15 - 0
sandbox/NOTES

@@ -0,0 +1,15 @@
+Uses for a graph:
+
+1. Create a graph from RDF input, manipulate or evaluate it, and output it as
+  serialized RDF (always detached) [NO USE CASE]
+2. Create a graph from RDF input, optionally manipulate it with other data from
+  the store or external RDF and store it (start detached, then convert keys;
+  or, start attached)
+3. Retrieve a graph from the store, optionally manipulate it, and output it as
+  serialized RDF (start attached, then detach)
+4. Retrieve a graph from the store, manipulate it, and put the changed graph
+  back in the store (always attached)
+
+Initially we might try to render the graph read-only when detached; this
+avoids implementing more complex operations such as add, remove and booleans.
+

+ 10 - 0
sandbox/txn_openLogic.txt

@@ -0,0 +1,10 @@
+txn_open    write       txn_rw      Open?
+n           -           -           y
+y           n           -           n
+y           y           y           n
+y           y           n           y
+
+txn_open    Open    Reset?
+n           y       n
+y           y       y
+

+ 112 - 46
setup.py

@@ -16,7 +16,9 @@ from os import path
 import lakesuperior
 import lakesuperior
 
 
 # Use this version to build C files from .pyx sources.
 # Use this version to build C files from .pyx sources.
-CYTHON_VERSION='0.29'
+CYTHON_VERSION='0.29.6'
+
+KLEN = 5 # TODO Move somewhere else (config?)
 
 
 try:
 try:
     import Cython
     import Cython
@@ -24,8 +26,16 @@ try:
 except ImportError:
 except ImportError:
     USE_CYTHON = False
     USE_CYTHON = False
 else:
 else:
-    if Cython.__version__ == CYTHON_VERSION:
+    cy_installed_version = Cython.__version__
+    if cy_installed_version == CYTHON_VERSION:
         USE_CYTHON = True
         USE_CYTHON = True
+    else:
+        raise ImportError(
+            f'The installed Cython version ({cy_installed_version}) '
+            f'does not match the required version ({CYTHON_VERSION}). '
+            'Please insstall the exact required Cython version in order to '
+            'generate the C sources.'
+        )
 
 
 
 
 # ``pytest_runner`` is referenced in ``setup_requires``.
 # ``pytest_runner`` is referenced in ``setup_requires``.
@@ -40,11 +50,17 @@ with open(readme_fpath, encoding='utf-8') as f:
     long_description = f.read()
     long_description = f.read()
 
 
 # Extensions directory.
 # Extensions directory.
+coll_src_dir = path.join('ext', 'collections-c', 'src')
 lmdb_src_dir = path.join('ext', 'lmdb', 'libraries', 'liblmdb')
 lmdb_src_dir = path.join('ext', 'lmdb', 'libraries', 'liblmdb')
-tpl_src_dir = path.join('ext', 'tpl', 'src')
 spookyhash_src_dir = path.join('ext', 'spookyhash', 'src')
 spookyhash_src_dir = path.join('ext', 'spookyhash', 'src')
+tpl_src_dir = path.join('ext', 'tpl', 'src')
 
 
-include_dirs = [lmdb_src_dir, tpl_src_dir, spookyhash_src_dir]
+include_dirs = [
+    path.join(coll_src_dir, 'include'),
+    lmdb_src_dir,
+    spookyhash_src_dir,
+    tpl_src_dir,
+]
 
 
 cy_include_dir = path.join('lakesuperior', 'cy_include')
 cy_include_dir = path.join('lakesuperior', 'cy_include')
 
 
@@ -59,57 +75,126 @@ else:
     ext = pxdext = 'c'
     ext = pxdext = 'c'
 
 
 extensions = [
 extensions = [
+    Extension(
+        'lakesuperior.model.base',
+        [
+            path.join(tpl_src_dir, 'tpl.c'),
+            path.join('lakesuperior', 'model', f'base.{ext}'),
+        ],
+        include_dirs=include_dirs,
+        extra_compile_args=['-fopenmp', '-g'],
+        extra_link_args=['-fopenmp', '-g']
+    ),
+    Extension(
+        'lakesuperior.model.callbacks',
+        [
+            path.join('lakesuperior', 'model', f'callbacks.{ext}'),
+        ],
+        include_dirs=include_dirs,
+        extra_compile_args=['-g'],
+        extra_link_args=['-g'],
+        #extra_compile_args=['-fopenmp'],
+        #extra_link_args=['-fopenmp']
+    ),
+    Extension(
+        'lakesuperior.model.structures.*',
+        [
+            path.join(spookyhash_src_dir, 'spookyhash.c'),
+            path.join(coll_src_dir, 'common.c'),
+            path.join(coll_src_dir, 'array.c'),
+            path.join(coll_src_dir, 'hashtable.c'),
+            path.join(coll_src_dir, 'hashset.c'),
+            path.join('lakesuperior', 'model', 'structures', f'*.{ext}'),
+        ],
+        include_dirs=include_dirs,
+        extra_compile_args=['-fopenmp', '-g'],
+        extra_link_args=['-fopenmp', '-g']
+    ),
     Extension(
     Extension(
         'lakesuperior.store.base_lmdb_store',
         'lakesuperior.store.base_lmdb_store',
         [
         [
+            path.join(coll_src_dir, 'common.c'),
+            path.join(coll_src_dir, 'array.c'),
+            path.join(coll_src_dir, 'hashtable.c'),
+            path.join(coll_src_dir, 'hashset.c'),
+            path.join(tpl_src_dir, 'tpl.c'),
             path.join(lmdb_src_dir, 'mdb.c'),
             path.join(lmdb_src_dir, 'mdb.c'),
             path.join(lmdb_src_dir, 'midl.c'),
             path.join(lmdb_src_dir, 'midl.c'),
             path.join('lakesuperior', 'store', f'base_lmdb_store.{ext}'),
             path.join('lakesuperior', 'store', f'base_lmdb_store.{ext}'),
         ],
         ],
         include_dirs=include_dirs,
         include_dirs=include_dirs,
+        extra_compile_args=['-g'],
+        extra_link_args=['-g'],
     ),
     ),
     Extension(
     Extension(
-        'lakesuperior.store.ldp_rs.term',
+        'lakesuperior.model.rdf.*',
         [
         [
             path.join(tpl_src_dir, 'tpl.c'),
             path.join(tpl_src_dir, 'tpl.c'),
+            path.join(spookyhash_src_dir, 'context.c'),
+            path.join(spookyhash_src_dir, 'globals.c'),
             path.join(spookyhash_src_dir, 'spookyhash.c'),
             path.join(spookyhash_src_dir, 'spookyhash.c'),
-            path.join('lakesuperior', 'store', 'ldp_rs', f'term.{ext}'),
+            path.join(coll_src_dir, 'common.c'),
+            path.join(coll_src_dir, 'array.c'),
+            path.join(coll_src_dir, 'hashtable.c'),
+            path.join(coll_src_dir, 'hashset.c'),
+            path.join('lakesuperior', 'model', 'rdf', f'*.{ext}'),
         ],
         ],
         include_dirs=include_dirs,
         include_dirs=include_dirs,
-        extra_compile_args=['-fopenmp'],
-        extra_link_args=['-fopenmp']
+        #extra_compile_args=['-fopenmp'],
+        #extra_link_args=['-fopenmp']
     ),
     ),
     Extension(
     Extension(
         'lakesuperior.store.ldp_rs.lmdb_triplestore',
         'lakesuperior.store.ldp_rs.lmdb_triplestore',
         [
         [
+            path.join(coll_src_dir, 'common.c'),
+            path.join(coll_src_dir, 'array.c'),
+            path.join(coll_src_dir, 'hashtable.c'),
+            path.join(coll_src_dir, 'hashset.c'),
             path.join(lmdb_src_dir, 'mdb.c'),
             path.join(lmdb_src_dir, 'mdb.c'),
             path.join(lmdb_src_dir, 'midl.c'),
             path.join(lmdb_src_dir, 'midl.c'),
             path.join(
             path.join(
                 'lakesuperior', 'store', 'ldp_rs', f'lmdb_triplestore.{ext}'),
                 'lakesuperior', 'store', 'ldp_rs', f'lmdb_triplestore.{ext}'),
         ],
         ],
         include_dirs=include_dirs,
         include_dirs=include_dirs,
-        extra_compile_args=['-fopenmp'],
-        extra_link_args=['-fopenmp']
+        extra_compile_args=['-g', '-fopenmp'],
+        extra_link_args=['-g', '-fopenmp']
     ),
     ),
-    # For testing.
-    #Extension(
-    #    '*',
-    #    [
-    #        #path.join(tpl_src_dir, 'tpl.c'),
-    #        path.join(
-    #            path.dirname(lakesuperior.basedir), 'sandbox', f'*.{ext}'),
-    #    ],
-    #    include_dirs=include_dirs,
-    #),
+]
+
+# Great reference read about dependency management:
+# https://caremad.io/posts/2013/07/setup-vs-requirement/
+install_requires = [
+    'CoilMQ',
+    'Flask',
+    'HiYaPyCo',
+    'PyYAML',
+    'arrow',
+    'click',
+    'click-log',
+    'cymem',
+    'gevent',
+    'gunicorn',
+    'rdflib',
+    'rdflib-jsonld',
+    'requests',
+    'requests-toolbelt',
+    'sphinx-rtd-theme',
+    'stomp.py',
 ]
 ]
 
 
 if USE_CYTHON:
 if USE_CYTHON:
-    extensions = cythonize(extensions, include_path=include_dirs, compiler_directives={
-        'language_level': 3,
-        'boundscheck': False,
-        'wraparound': False,
-        'profile': True,
-    })
+    extensions = cythonize(
+        extensions,
+        include_path=include_dirs,
+        annotate=True,
+        compiler_directives={
+            'language_level': 3,
+            'boundscheck': False,
+            'wraparound': False,
+            'profile': True,
+            'embedsignature': True
+        }
+    )
 
 
 
 
 setup(
 setup(
@@ -161,26 +246,7 @@ setup(
 
 
     packages=find_packages(exclude=['contrib', 'docs', 'tests']),
     packages=find_packages(exclude=['contrib', 'docs', 'tests']),
 
 
-    # Great reference read about dependency management:
-    # https://caremad.io/posts/2013/07/setup-vs-requirement/
-    install_requires=[
-        'CoilMQ',
-        'Flask',
-        'HiYaPyCo',
-        'PyYAML',
-        'arrow',
-        'cchardet',
-        'click',
-        'click-log',
-        'gevent',
-        'gunicorn',
-        'rdflib',
-        'rdflib-jsonld',
-        'requests',
-        'requests-toolbelt',
-        'sphinx-rtd-theme',
-        'stomp.py',
-    ],
+    install_requires=install_requires,
 
 
     setup_requires=[
     setup_requires=[
         'setuptools>=18.0',
         'setuptools>=18.0',

+ 850 - 0
tests/0_data_structures/test_0_0_graph.py

@@ -0,0 +1,850 @@
+import pdb
+import pytest
+
+from shutil import rmtree
+
+from rdflib import Graph, Namespace, URIRef
+
+from lakesuperior.model.rdf.graph import Graph
+from lakesuperior.store.ldp_rs.lmdb_store import LmdbStore
+
+
+@pytest.fixture(scope='class')
+def store():
+    """
+    Test LMDB store.
+
+    This store has a different life cycle than the one used for tests in higher
+    levels of the stack and is not bootstrapped (i.e. starts completely empty).
+    """
+    env_path = '/tmp/test_lmdbstore'
+    # Remove previous test DBs
+    rmtree(env_path, ignore_errors=True)
+    store = LmdbStore(env_path)
+    yield store
+    store.close()
+    store.destroy()
+
+
+@pytest.fixture(scope='class')
+def trp():
+    return (
+        (URIRef('urn:s:0'), URIRef('urn:p:0'), URIRef('urn:o:0')),
+        # Exact same as [0].
+        (URIRef('urn:s:0'), URIRef('urn:p:0'), URIRef('urn:o:0')),
+        # NOTE: s and o are in reversed order.
+        (URIRef('urn:o:0'), URIRef('urn:p:0'), URIRef('urn:s:0')),
+        (URIRef('urn:s:0'), URIRef('urn:p:1'), URIRef('urn:o:0')),
+        (URIRef('urn:s:0'), URIRef('urn:p:1'), URIRef('urn:o:1')),
+        (URIRef('urn:s:1'), URIRef('urn:p:1'), URIRef('urn:o:1')),
+        (URIRef('urn:s:1'), URIRef('urn:p:2'), URIRef('urn:o:2')),
+    )
+
+@pytest.mark.usefixtures('trp')
+@pytest.mark.usefixtures('store')
+class TestGraphInit:
+    """
+    Test initialization of graphs with different base data sets.
+    """
+    def test_empty(self, store):
+        """
+        Test creation of an empty graph.
+        """
+        # No transaction needed to init an empty graph.
+        gr = Graph(store)
+
+        # len() should not need a DB transaction open.
+        assert len(gr) == 0
+
+
+    def test_init_triples(self, trp, store):
+        """
+        Test creation using a Python set.
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            assert len(gr) == 6
+
+            for t in trp:
+                assert t in gr
+
+
+@pytest.mark.usefixtures('trp')
+@pytest.mark.usefixtures('store')
+class TestGraphLookup:
+    """
+    Test triple lookup.
+    """
+
+    def test_lookup_all_unbound(self, trp, store):
+        """
+        Test lookup ? ? ? (all unbound)
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((None, None, None))
+
+            assert len(flt_gr) == 6
+
+            assert trp[0] in flt_gr
+            assert trp[2] in flt_gr
+            assert trp[3] in flt_gr
+            assert trp[4] in flt_gr
+            assert trp[5] in flt_gr
+            assert trp[6] in flt_gr
+
+
+    def test_lookup_s(self, trp, store):
+        """
+        Test lookup s ? ?
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((URIRef('urn:s:0'), None, None))
+
+            assert len(flt_gr) == 3
+
+            assert trp[0] in flt_gr
+            assert trp[3] in flt_gr
+            assert trp[4] in flt_gr
+
+            assert trp[2] not in flt_gr
+            assert trp[5] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((URIRef('urn:s:8'), None, None))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_p(self, trp, store):
+        """
+        Test lookup ? p ?
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((None, URIRef('urn:p:0'), None))
+
+            assert len(flt_gr) == 2
+
+            assert trp[0] in flt_gr
+            assert trp[2] in flt_gr
+
+            assert trp[3] not in flt_gr
+            assert trp[4] not in flt_gr
+            assert trp[5] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((None, URIRef('urn:p:8'), None))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_o(self, trp, store):
+        """
+        Test lookup ? ? o
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((None, None, URIRef('urn:o:1')))
+
+            assert len(flt_gr) == 2
+
+            assert trp[4] in flt_gr
+            assert trp[5] in flt_gr
+
+            assert trp[0] not in flt_gr
+            assert trp[2] not in flt_gr
+            assert trp[3] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((None, None, URIRef('urn:o:8')))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_sp(self, trp, store):
+        """
+        Test lookup s p ?
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((URIRef('urn:s:0'), URIRef('urn:p:1'), None))
+
+            assert len(flt_gr) == 2
+
+            assert trp[3] in flt_gr
+            assert trp[4] in flt_gr
+
+            assert trp[0] not in flt_gr
+            assert trp[2] not in flt_gr
+            assert trp[5] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((URIRef('urn:s:0'), URIRef('urn:p:2'), None))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_so(self, trp, store):
+        """
+        Test lookup s ? o
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((URIRef('urn:s:0'), None, URIRef('urn:o:0')))
+
+            assert len(flt_gr) == 2
+
+            assert trp[0] in flt_gr
+            assert trp[3] in flt_gr
+
+            assert trp[2] not in flt_gr
+            assert trp[4] not in flt_gr
+            assert trp[5] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((URIRef('urn:s:0'), None, URIRef('urn:o:2')))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_po(self, trp, store):
+        """
+        Test lookup ? p o
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup((None, URIRef('urn:p:1'), URIRef('urn:o:1')))
+
+            assert len(flt_gr) == 2
+
+            assert trp[4] in flt_gr
+            assert trp[5] in flt_gr
+
+            assert trp[0] not in flt_gr
+            assert trp[2] not in flt_gr
+            assert trp[3] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup((None, URIRef('urn:p:1'), URIRef('urn:o:2')))
+
+            assert len(empty_flt_gr) == 0
+
+
+    def test_lookup_spo(self, trp, store):
+        """
+        Test lookup s p o
+        """
+        with store.txn_ctx():
+            gr = Graph(store, data=set(trp))
+
+            flt_gr = gr.lookup(
+                (URIRef('urn:s:1'), URIRef('urn:p:1'), URIRef('urn:o:1'))
+            )
+
+            assert len(flt_gr) == 1
+
+            assert trp[5] in flt_gr
+
+            assert trp[0] not in flt_gr
+            assert trp[2] not in flt_gr
+            assert trp[3] not in flt_gr
+            assert trp[4] not in flt_gr
+            assert trp[6] not in flt_gr
+
+            # Test for empty results.
+            empty_flt_gr = gr.lookup(
+                (URIRef('urn:s:1'), URIRef('urn:p:1'), URIRef('urn:o:2'))
+            )
+
+            assert len(empty_flt_gr) == 0
+
+
+@pytest.mark.usefixtures('trp')
+@pytest.mark.usefixtures('store')
+class TestGraphSlicing:
+    """
+    Test triple lookup.
+    """
+    # TODO
+    pass
+
+
+
+@pytest.mark.usefixtures('trp')
+@pytest.mark.usefixtures('store')
+class TestGraphOps:
+    """
+    Test various graph operations.
+    """
+    def test_len(self, trp, store):
+        """
+        Test the length of a graph with and without duplicates.
+        """
+        with store.txn_ctx():
+            gr = Graph(store)
+            assert len(gr) == 0
+
+            gr.add((trp[0],))
+            assert len(gr) == 1
+
+            gr.add((trp[1],)) # Same values
+            assert len(gr) == 1
+
+            gr.add((trp[2],))
+            assert len(gr) == 2
+
+            gr.add(trp)
+            assert len(gr) == 6
+
+
+    def test_dup(self, trp, store):
+        """
+        Test operations with duplicate triples.
+        """
+        with store.txn_ctx():
+            gr = Graph(store)
+
+            gr.add((trp[0],))
+            assert trp[1] in gr
+            assert trp[2] not in gr
+
+
+    def test_remove(self, trp, store):
+        """
+        Test adding and removing triples.
+        """
+        with store.txn_ctx():
+            gr = Graph(store)
+
+            gr.add(trp)
+            gr.remove(trp[0])
+            assert len(gr) == 5
+            assert trp[0] not in gr
+            assert trp[1] not in gr
+
+            # This is the duplicate triple.
+            gr.remove(trp[1])
+            assert len(gr) == 5
+
+            # This is the triple in reverse order.
+            gr.remove(trp[2])
+            assert len(gr) == 4
+
+            gr.remove(trp[4])
+            assert len(gr) == 3
+
+
+    def test_union(self, trp, store):
+        """
+        Test graph union.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:3]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr3 = gr1 | gr2
+
+            assert len(gr3) == 5
+            assert trp[0] in gr3
+            assert trp[4] in gr3
+
+
+    def test_ip_union(self, trp, store):
+        """
+        Test graph in-place union.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:3]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr1 |= gr2
+
+            assert len(gr1) == 5
+            assert trp[0] in gr1
+            assert trp[4] in gr1
+
+
+    def test_addition(self, trp, store):
+        """
+        Test graph addition.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:3]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr3 = gr1 + gr2
+
+            assert len(gr3) == 5
+            assert trp[0] in gr3
+            assert trp[4] in gr3
+
+
+    def test_ip_addition(self, trp, store):
+        """
+        Test graph in-place addition.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:3]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr1 += gr2
+
+            assert len(gr1) == 5
+            assert trp[0] in gr1
+            assert trp[4] in gr1
+
+
+    def test_subtraction(self, trp, store):
+        """
+        Test graph addition.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:4]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr3 = gr1 - gr2
+
+            assert len(gr3) == 1
+            assert trp[0] in gr3
+            assert trp[1] in gr3
+            assert trp[2] not in gr3
+            assert trp[3] not in gr3
+            assert trp[4] not in gr3
+
+            gr3 = gr2 - gr1
+
+            assert len(gr3) == 2
+            assert trp[0] not in gr3
+            assert trp[1] not in gr3
+            assert trp[2] not in gr3
+            assert trp[3] not in gr3
+            assert trp[4] in gr3
+            assert trp[5] in gr3
+
+
+    def test_ip_subtraction(self, trp, store):
+        """
+        Test graph in-place addition.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:4]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr1 -= gr2
+
+            assert len(gr1) == 1
+            assert trp[0] in gr1
+            assert trp[1] in gr1
+            assert trp[2] not in gr1
+            assert trp[3] not in gr1
+            assert trp[4] not in gr1
+
+
+
+    def test_intersect(self, trp, store):
+        """
+        Test graph intersextion.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:4]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr3 = gr1 & gr2
+
+            assert len(gr3) == 2
+            assert trp[2] in gr3
+            assert trp[3] in gr3
+            assert trp[0] not in gr3
+            assert trp[5] not in gr3
+
+
+    def test_ip_intersect(self, trp, store):
+        """
+        Test graph intersextion.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:4]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr1 &= gr2
+
+            assert len(gr1) == 2
+            assert trp[2] in gr1
+            assert trp[3] in gr1
+            assert trp[0] not in gr1
+            assert trp[5] not in gr1
+
+
+    def test_xor(self, trp, store):
+        """
+        Test graph intersextion.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:4]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr3 = gr1 ^ gr2
+
+            assert len(gr3) == 3
+            assert trp[2] not in gr3
+            assert trp[3] not in gr3
+            assert trp[0] in gr3
+            assert trp[5] in gr3
+
+
+    def test_ip_xor(self, trp, store):
+        """
+        Test graph intersextion.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:4]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr1 ^= gr2
+
+            assert len(gr1) == 3
+            assert trp[2] not in gr1
+            assert trp[3] not in gr1
+            assert trp[0] in gr1
+            assert trp[5] in gr1
+
+
+
+@pytest.mark.usefixtures('trp')
+@pytest.mark.usefixtures('store')
+class TestNamedGraphOps:
+    """
+    Test various operations on a named graph.
+    """
+    def test_len(self, trp, store):
+        """
+        Test the length of a graph with and without duplicates.
+        """
+        imr = Graph(store, uri='http://example.edu/imr01')
+        assert len(imr) == 0
+
+        with store.txn_ctx():
+            imr.add((trp[0],))
+            assert len(imr) == 1
+
+            imr.add((trp[1],)) # Same values
+            assert len(imr) == 1
+
+            imr.add((trp[2],))
+            assert len(imr) == 2
+
+            imr.add(trp)
+            assert len(imr) == 6
+
+
+    def test_dup(self, trp, store):
+        """
+        Test operations with duplicate triples.
+        """
+        imr = Graph(store, uri='http://example.edu/imr01')
+
+        with store.txn_ctx():
+            imr.add((trp[0],))
+            assert trp[1] in imr
+            assert trp[2] not in imr
+
+
+    def test_remove(self, trp, store):
+        """
+        Test adding and removing triples.
+        """
+        with store.txn_ctx():
+            imr = Graph(store, uri='http://example.edu/imr01', data={*trp})
+
+            imr.remove(trp[0])
+            assert len(imr) == 5
+            assert trp[0] not in imr
+            assert trp[1] not in imr
+
+            # This is the duplicate triple.
+            imr.remove(trp[1])
+            assert len(imr) == 5
+
+            # This is the triple in reverse order.
+            imr.remove(trp[2])
+            assert len(imr) == 4
+
+            imr.remove(trp[4])
+            assert len(imr) == 3
+
+
+    def test_union(self, trp, store):
+        """
+        Test graph union.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:3]})
+            gr2 = Graph(store, uri='http://example.edu/imr02', data={*trp[2:6]})
+
+            gr3 = gr1 | gr2
+
+            assert len(gr3) == 5
+            assert trp[0] in gr3
+            assert trp[4] in gr3
+
+            assert gr3.uri == None
+
+
+    def test_ip_union(self, trp, store):
+        """
+        Test graph in-place union.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:3]})
+            gr2 = Graph(store, uri='http://example.edu/imr02', data={*trp[2:6]})
+
+            gr1 |= gr2
+
+            assert len(gr1) == 5
+            assert trp[0] in gr1
+            assert trp[4] in gr1
+
+            assert gr1.uri == URIRef('http://example.edu/imr01')
+
+
+    def test_addition(self, trp, store):
+        """
+        Test graph addition.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:3]})
+            gr2 = Graph(store, uri='http://example.edu/imr02', data={*trp[2:6]})
+
+            gr3 = gr1 + gr2
+
+            assert len(gr3) == 5
+            assert trp[0] in gr3
+            assert trp[4] in gr3
+
+            assert gr3.uri == None
+
+
+    def test_ip_addition(self, trp, store):
+        """
+        Test graph in-place addition.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:3]})
+            gr2 = Graph(store, uri='http://example.edu/imr02', data={*trp[2:6]})
+
+            gr1 += gr2
+
+            assert len(gr1) == 5
+            assert trp[0] in gr1
+            assert trp[4] in gr1
+
+            assert gr1.uri == URIRef('http://example.edu/imr01')
+
+
+    def test_subtraction(self, trp, store):
+        """
+        Test graph addition.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:4]})
+            gr2 = Graph(store, uri='http://example.edu/imr02', data={*trp[2:6]})
+
+            gr3 = gr1 - gr2
+
+            assert len(gr3) == 1
+            assert trp[0] in gr3
+            assert trp[1] in gr3
+            assert trp[2] not in gr3
+            assert trp[3] not in gr3
+            assert trp[4] not in gr3
+
+            assert gr3.uri == None
+
+            gr3 = gr2 - gr1
+
+            assert len(gr3) == 2
+            assert trp[0] not in gr3
+            assert trp[1] not in gr3
+            assert trp[2] not in gr3
+            assert trp[3] not in gr3
+            assert trp[4] in gr3
+            assert trp[5] in gr3
+
+            assert gr3.uri == None
+
+
+    def test_ip_subtraction(self, trp, store):
+        """
+        Test graph in-place addition.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:4]})
+            gr2 = Graph(store, uri='http://example.edu/imr02', data={*trp[2:6]})
+
+            gr1 -= gr2
+
+            assert len(gr1) == 1
+            assert trp[0] in gr1
+            assert trp[1] in gr1
+            assert trp[2] not in gr1
+            assert trp[3] not in gr1
+            assert trp[4] not in gr1
+
+            assert gr1.uri == URIRef('http://example.edu/imr01')
+
+
+
+    def test_intersect(self, trp, store):
+        """
+        Test graph intersextion.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:4]})
+            gr2 = Graph(store, uri='http://example.edu/imr02', data={*trp[2:6]})
+
+            gr3 = gr1 & gr2
+
+            assert len(gr3) == 2
+            assert trp[2] in gr3
+            assert trp[3] in gr3
+            assert trp[0] not in gr3
+            assert trp[5] not in gr3
+
+            assert gr3.uri == None
+
+
+    def test_ip_intersect(self, trp, store):
+        """
+        Test graph intersextion.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:4]})
+            gr2 = Graph(store, uri='http://example.edu/imr02', data={*trp[2:6]})
+
+            gr1 &= gr2
+
+            assert len(gr1) == 2
+            assert trp[2] in gr1
+            assert trp[3] in gr1
+            assert trp[0] not in gr1
+            assert trp[5] not in gr1
+
+            assert gr1.uri == URIRef('http://example.edu/imr01')
+
+
+    def test_xor(self, trp, store):
+        """
+        Test graph intersextion.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:4]})
+            gr2 = Graph(store, uri='http://example.edu/imr02', data={*trp[2:6]})
+
+            gr3 = gr1 ^ gr2
+
+            assert len(gr3) == 3
+            assert trp[2] not in gr3
+            assert trp[3] not in gr3
+            assert trp[0] in gr3
+            assert trp[5] in gr3
+
+            assert gr3.uri == None
+
+
+    def test_ip_xor(self, trp, store):
+        """
+        Test graph intersextion.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:4]})
+            gr2 = Graph(store, uri='http://example.edu/imr02', data={*trp[2:6]})
+
+            gr1 ^= gr2
+
+            assert len(gr1) == 3
+            assert trp[2] not in gr1
+            assert trp[3] not in gr1
+            assert trp[0] in gr1
+            assert trp[5] in gr1
+
+            assert gr1.uri == URIRef('http://example.edu/imr01')
+
+
+@pytest.mark.usefixtures('trp')
+@pytest.mark.usefixtures('store')
+class TestHybridOps:
+    """
+    Test operations between IMR and graph.
+    """
+    def test_hybrid_union(self, trp, store):
+        """
+        Test hybrid IMR + graph union.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:3]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr3 = gr1 | gr2
+
+            assert len(gr3) == 5
+            assert trp[0] in gr3
+            assert trp[4] in gr3
+
+            assert isinstance(gr3, Graph)
+            assert gr3.uri == None
+
+            gr4 = gr2 | gr1
+
+            assert isinstance(gr4, Graph)
+
+            assert gr3 == gr4
+
+
+    def test_ip_union_imr(self, trp, store):
+        """
+        Test IMR + graph in-place union.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, uri='http://example.edu/imr01', data={*trp[:3]})
+            gr2 = Graph(store, data={*trp[2:6]})
+
+            gr1 |= gr2
+
+            assert len(gr1) == 5
+            assert trp[0] in gr1
+            assert trp[4] in gr1
+
+            assert gr1.uri == URIRef('http://example.edu/imr01')
+
+
+    def test_ip_union_gr(self, trp, store):
+        """
+        Test graph + IMR in-place union.
+        """
+        with store.txn_ctx():
+            gr1 = Graph(store, data={*trp[:3]})
+            gr2 = Graph(store, uri='http://example.edu/imr01', data={*trp[2:6]})
+
+            gr1 |= gr2
+
+            assert len(gr1) == 5
+            assert trp[0] in gr1
+            assert trp[4] in gr1
+
+            assert isinstance(gr1, Graph)

+ 121 - 29
tests/0_store/test_lmdb_store.py → tests/1_store/test_1_0_lmdb_store.py

@@ -1,12 +1,15 @@
+import pdb
 import pytest
 import pytest
 
 
 from os import path
 from os import path
 from shutil import rmtree
 from shutil import rmtree
 
 
-from rdflib import Graph, Namespace, URIRef
+from rdflib import Namespace, URIRef
 from rdflib.graph import DATASET_DEFAULT_GRAPH_ID as RDFLIB_DEFAULT_GRAPH_URI
 from rdflib.graph import DATASET_DEFAULT_GRAPH_ID as RDFLIB_DEFAULT_GRAPH_URI
 from rdflib.namespace import RDF, RDFS
 from rdflib.namespace import RDF, RDFS
 
 
+from lakesuperior.model.rdf.graph import Graph
+from lakesuperior.store.base_lmdb_store import LmdbError
 from lakesuperior.store.ldp_rs.lmdb_store import LmdbStore
 from lakesuperior.store.ldp_rs.lmdb_store import LmdbStore
 
 
 
 
@@ -67,6 +70,12 @@ class TestStoreInit:
         assert not path.exists(env_path + '-lock')
         assert not path.exists(env_path + '-lock')
 
 
 
 
+
+@pytest.mark.usefixtures('store')
+class TestTransactionContext:
+    '''
+    Tests for intializing and shutting down store and transactions.
+    '''
     def test_txn(self, store):
     def test_txn(self, store):
         '''
         '''
         Test opening and closing the main transaction.
         Test opening and closing the main transaction.
@@ -106,20 +115,80 @@ class TestStoreInit:
         '''
         '''
         Test rolling back a transaction.
         Test rolling back a transaction.
         '''
         '''
+        trp = (
+            URIRef('urn:nogo:s'), URIRef('urn:nogo:p'), URIRef('urn:nogo:o')
+        )
         try:
         try:
             with store.txn_ctx(True):
             with store.txn_ctx(True):
-                store.add((
-                    URIRef('urn:nogo:s'), URIRef('urn:nogo:p'),
-                    URIRef('urn:nogo:o')))
+                store.add(trp)
                 raise RuntimeError() # This should roll back the transaction.
                 raise RuntimeError() # This should roll back the transaction.
         except RuntimeError:
         except RuntimeError:
             pass
             pass
 
 
         with store.txn_ctx():
         with store.txn_ctx():
-            res = set(store.triples((None, None, None)))
+            res = set(store.triples(trp))
         assert len(res) == 0
         assert len(res) == 0
 
 
 
 
+    def test_nested_ro_txn(self, store):
+        """
+        Test two nested RO transactions.
+        """
+        trp = (URIRef('urn:s:0'), URIRef('urn:p:0'), URIRef('urn:o:0'))
+        with store.txn_ctx(True):
+            store.add(trp)
+        with store.txn_ctx():
+            with store.txn_ctx():
+                res = {*store.triples(trp)}
+                assert trp in {q[0] for q in res}
+            assert trp in {q[0] for q in res}
+
+
+    def test_nested_ro_txn_nowrite(self, store):
+        """
+        Test two nested RO transactions.
+        """
+        trp = (URIRef('urn:s:0'), URIRef('urn:p:0'), URIRef('urn:o:0'))
+        with pytest.raises(LmdbError):
+            with store.txn_ctx():
+                with store.txn_ctx():
+                    store.add(trp)
+
+
+    def test_nested_ro_rw_txn(self, store):
+        """
+        Test a RO transaction nested into a RW one.
+        """
+        trp = (URIRef('urn:s:1'), URIRef('urn:p:1'), URIRef('urn:o:1'))
+        with store.txn_ctx():
+            with store.txn_ctx(True):
+                store.add(trp)
+            # Outer txn should now see the new triple.
+            assert trp in {q[0] for q in store.triples(trp)}
+
+
+    def test_nested_rw_ro_txn(self, store):
+        """
+        Test that a RO transaction nested in a RW transaction can write.
+        """
+        trp = (URIRef('urn:s:2'), URIRef('urn:p:2'), URIRef('urn:o:2'))
+        with store.txn_ctx(True):
+            with store.txn_ctx():
+                store.add(trp)
+            assert trp in {q[0] for q in store.triples(trp)}
+
+
+    def test_nested_rw_rw_txn(self, store):
+        """
+        Test that a RW transaction nested in a RW transaction can write.
+        """
+        trp = (URIRef('urn:s:3'), URIRef('urn:p:3'), URIRef('urn:o:3'))
+        with store.txn_ctx(True):
+            with store.txn_ctx():
+                store.add(trp)
+            assert trp in {q[0] for q in store.triples(trp)}
+
+
 @pytest.mark.usefixtures('store')
 @pytest.mark.usefixtures('store')
 class TestBasicOps:
 class TestBasicOps:
     '''
     '''
@@ -257,6 +326,42 @@ class TestBasicOps:
 
 
 
 
 
 
+@pytest.mark.usefixtures('store', 'bogus_trp')
+class TestExtendedOps:
+    '''
+    Test additional store operations.
+    '''
+
+    def test_all_terms(self, store, bogus_trp):
+        """
+        Test the "all terms" mehods.
+        """
+        with store.txn_ctx(True):
+            for trp in bogus_trp:
+                store.add(trp)
+
+        with store.txn_ctx():
+            all_s = store.all_terms('s')
+            all_p = store.all_terms('p')
+            all_o = store.all_terms('o')
+
+        assert len(all_s) == 1
+        assert len(all_p) == 100
+        assert len(all_o) == 1000
+
+        assert URIRef('urn:test_mp:s1') in all_s
+        assert URIRef('urn:test_mp:s1') not in all_p
+        assert URIRef('urn:test_mp:s1') not in all_o
+
+        assert URIRef('urn:test_mp:p10') not in all_s
+        assert URIRef('urn:test_mp:p10') in all_p
+        assert URIRef('urn:test_mp:p10') not in all_o
+
+        assert URIRef('urn:test_mp:o99') not in all_s
+        assert URIRef('urn:test_mp:o99') not in all_p
+        assert URIRef('urn:test_mp:o99') in all_o
+
+
 @pytest.mark.usefixtures('store', 'bogus_trp')
 @pytest.mark.usefixtures('store', 'bogus_trp')
 class TestEntryCount:
 class TestEntryCount:
     '''
     '''
@@ -648,7 +753,7 @@ class TestContext:
 
 
         with store.txn_ctx(True):
         with store.txn_ctx(True):
             store.add_graph(gr_uri)
             store.add_graph(gr_uri)
-            assert gr_uri in {gr.identifier for gr in store.contexts()}
+            assert gr_uri in store.contexts()
 
 
 
 
     def test_add_graph_with_triple(self, store):
     def test_add_graph_with_triple(self, store):
@@ -663,7 +768,7 @@ class TestContext:
             store.add(trp, ctx_uri)
             store.add(trp, ctx_uri)
 
 
         with store.txn_ctx():
         with store.txn_ctx():
-            assert ctx_uri in {gr.identifier for gr in store.contexts(trp)}
+            assert ctx_uri in store.contexts(trp)
 
 
 
 
     def test_empty_context(self, store):
     def test_empty_context(self, store):
@@ -674,10 +779,10 @@ class TestContext:
 
 
         with store.txn_ctx(True):
         with store.txn_ctx(True):
             store.add_graph(gr_uri)
             store.add_graph(gr_uri)
-            assert gr_uri in {gr.identifier for gr in store.contexts()}
+            assert gr_uri in store.contexts()
         with store.txn_ctx(True):
         with store.txn_ctx(True):
             store.remove_graph(gr_uri)
             store.remove_graph(gr_uri)
-            assert gr_uri not in {gr.identifier for gr in store.contexts()}
+            assert gr_uri not in store.contexts()
 
 
 
 
     def test_context_ro_txn(self, store):
     def test_context_ro_txn(self, store):
@@ -697,10 +802,10 @@ class TestContext:
         # allow a lookup in the same transaction, but this does not seem to be
         # allow a lookup in the same transaction, but this does not seem to be
         # possible.
         # possible.
         with store.txn_ctx():
         with store.txn_ctx():
-            assert gr_uri in {gr.identifier for gr in store.contexts()}
+            assert gr_uri in store.contexts()
         with store.txn_ctx(True):
         with store.txn_ctx(True):
             store.remove_graph(gr_uri)
             store.remove_graph(gr_uri)
-            assert gr_uri not in {gr.identifier for gr in store.contexts()}
+            assert gr_uri not in store.contexts()
 
 
 
 
     def test_add_trp_to_ctx(self, store):
     def test_add_trp_to_ctx(self, store):
@@ -731,7 +836,7 @@ class TestContext:
             assert len(set(store.triples((None, None, None), gr_uri))) == 3
             assert len(set(store.triples((None, None, None), gr_uri))) == 3
             assert len(set(store.triples((None, None, None), gr2_uri))) == 1
             assert len(set(store.triples((None, None, None), gr2_uri))) == 1
 
 
-            assert gr2_uri in {gr.identifier for gr in store.contexts()}
+            assert gr2_uri in store.contexts()
             assert trp1 in _clean(store.triples((None, None, None)))
             assert trp1 in _clean(store.triples((None, None, None)))
             assert trp1 not in _clean(store.triples((None, None, None),
             assert trp1 not in _clean(store.triples((None, None, None),
                     RDFLIB_DEFAULT_GRAPH_URI))
                     RDFLIB_DEFAULT_GRAPH_URI))
@@ -747,11 +852,11 @@ class TestContext:
             res_no_ctx = store.triples(trp3)
             res_no_ctx = store.triples(trp3)
             res_ctx = store.triples(trp3, gr2_uri)
             res_ctx = store.triples(trp3, gr2_uri)
             for res in res_no_ctx:
             for res in res_no_ctx:
-                assert Graph(identifier=gr_uri) in res[1]
-                assert Graph(identifier=gr2_uri) in res[1]
+                assert Graph(uri=gr_uri) in res[1]
+                assert Graph(uri=gr2_uri) in res[1]
             for res in res_ctx:
             for res in res_ctx:
-                assert Graph(identifier=gr_uri) in res[1]
-                assert Graph(identifier=gr2_uri) in res[1]
+                assert Graph(uri=gr_uri) in res[1]
+                assert Graph(uri=gr2_uri) in res[1]
 
 
 
 
     def test_delete_from_ctx(self, store):
     def test_delete_from_ctx(self, store):
@@ -825,19 +930,6 @@ class TestContext:
             assert len(set(store.triples(trp3))) == 1
             assert len(set(store.triples(trp3))) == 1
 
 
 
 
-
-
-
-
-@pytest.mark.usefixtures('store')
-class TestTransactions:
-    '''
-    Tests for transaction handling.
-    '''
-    # @TODO Test concurrent reads and writes.
-    pass
-
-
 #@pytest.mark.usefixtures('store')
 #@pytest.mark.usefixtures('store')
 #class TestRdflib:
 #class TestRdflib:
 #    '''
 #    '''

+ 138 - 106
tests/1_api/test_resource_api.py → tests/2_api/test_2_0_resource_api.py

@@ -4,7 +4,7 @@ import pytest
 from io import BytesIO
 from io import BytesIO
 from uuid import uuid4
 from uuid import uuid4
 
 
-from rdflib import Graph, Literal, URIRef
+from rdflib import Literal, URIRef
 
 
 from lakesuperior import env
 from lakesuperior import env
 from lakesuperior.api import resource as rsrc_api
 from lakesuperior.api import resource as rsrc_api
@@ -13,8 +13,8 @@ from lakesuperior.exceptions import (
         IncompatibleLdpTypeError, InvalidResourceError, ResourceNotExistsError,
         IncompatibleLdpTypeError, InvalidResourceError, ResourceNotExistsError,
         TombstoneError)
         TombstoneError)
 from lakesuperior.globals import RES_CREATED, RES_UPDATED
 from lakesuperior.globals import RES_CREATED, RES_UPDATED
-from lakesuperior.model.ldpr import Ldpr
-from lakesuperior.store.ldp_rs.lmdb_triplestore import SimpleGraph, Imr
+from lakesuperior.model.ldp.ldpr import Ldpr
+from lakesuperior.model.rdf.graph import Graph, from_rdf
 
 
 
 
 @pytest.fixture(scope='module')
 @pytest.fixture(scope='module')
@@ -67,10 +67,13 @@ class TestResourceCRUD:
         The ``dcterms:title`` property should NOT be included.
         The ``dcterms:title`` property should NOT be included.
         """
         """
         gr = rsrc_api.get_metadata('/')
         gr = rsrc_api.get_metadata('/')
-        assert isinstance(gr, SimpleGraph)
+        assert isinstance(gr, Graph)
         assert len(gr) == 9
         assert len(gr) == 9
-        assert gr[gr.uri : nsc['rdf'].type : nsc['ldp'].Resource ]
-        assert not gr[gr.uri : nsc['dcterms'].title : "Repository Root"]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert gr[gr.uri : nsc['rdf'].type : nsc['ldp'].Resource ]
+            assert not gr[
+                gr.uri : nsc['dcterms'].title : Literal("Repository Root")
+            ]
 
 
 
 
     def test_get_root_node(self):
     def test_get_root_node(self):
@@ -83,9 +86,10 @@ class TestResourceCRUD:
         assert isinstance(rsrc, Ldpr)
         assert isinstance(rsrc, Ldpr)
         gr = rsrc.imr
         gr = rsrc.imr
         assert len(gr) == 10
         assert len(gr) == 10
-        assert gr[gr.uri : nsc['rdf'].type : nsc['ldp'].Resource ]
-        assert gr[
-            gr.uri : nsc['dcterms'].title : Literal('Repository Root')]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert gr[gr.uri : nsc['rdf'].type : nsc['ldp'].Resource ]
+            assert gr[
+                gr.uri : nsc['dcterms'].title : Literal('Repository Root')]
 
 
 
 
     def test_get_nonexisting_node(self):
     def test_get_nonexisting_node(self):
@@ -102,16 +106,18 @@ class TestResourceCRUD:
         """
         """
         uid = '/rsrc_from_graph'
         uid = '/rsrc_from_graph'
         uri = nsc['fcres'][uid]
         uri = nsc['fcres'][uid]
-        gr = Graph().parse(
-            data='<> a <http://ex.org/type#A> .', format='turtle',
-            publicID=uri)
+        with env.app_globals.rdf_store.txn_ctx():
+            gr = from_rdf(
+                data='<> a <http://ex.org/type#A> .', format='turtle',
+                publicID=uri)
         evt, _ = rsrc_api.create_or_replace(uid, graph=gr)
         evt, _ = rsrc_api.create_or_replace(uid, graph=gr)
 
 
         rsrc = rsrc_api.get(uid)
         rsrc = rsrc_api.get(uid)
-        assert rsrc.imr[
-                rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
-        assert rsrc.imr[
-                rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert rsrc.imr[
+                    rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
+            assert rsrc.imr[
+                    rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
 
 
 
 
     def test_create_ldp_nr(self):
     def test_create_ldp_nr(self):
@@ -124,38 +130,45 @@ class TestResourceCRUD:
                 uid, stream=BytesIO(data), mimetype='text/plain')
                 uid, stream=BytesIO(data), mimetype='text/plain')
 
 
         rsrc = rsrc_api.get(uid)
         rsrc = rsrc_api.get(uid)
-        assert rsrc.content.read() == data
+        with rsrc.imr.store.txn_ctx():
+            assert rsrc.content.read() == data
 
 
 
 
     def test_replace_rsrc(self):
     def test_replace_rsrc(self):
         uid = '/test_replace'
         uid = '/test_replace'
         uri = nsc['fcres'][uid]
         uri = nsc['fcres'][uid]
-        gr1 = Graph().parse(
-            data='<> a <http://ex.org/type#A> .', format='turtle',
-            publicID=uri)
+        with env.app_globals.rdf_store.txn_ctx():
+            gr1 = from_rdf(
+                data='<> a <http://ex.org/type#A> .', format='turtle',
+                publicID=uri
+            )
         evt, _ = rsrc_api.create_or_replace(uid, graph=gr1)
         evt, _ = rsrc_api.create_or_replace(uid, graph=gr1)
         assert evt == RES_CREATED
         assert evt == RES_CREATED
 
 
         rsrc = rsrc_api.get(uid)
         rsrc = rsrc_api.get(uid)
-        assert rsrc.imr[
-                rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
-        assert rsrc.imr[
-                rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
-
-        gr2 = Graph().parse(
-            data='<> a <http://ex.org/type#B> .', format='turtle',
-            publicID=uri)
+        with env.app_globals.rdf_store.txn_ctx():
+            assert rsrc.imr[
+                    rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
+            assert rsrc.imr[
+                    rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
+
+        with env.app_globals.rdf_store.txn_ctx():
+            gr2 = from_rdf(
+                data='<> a <http://ex.org/type#B> .', format='turtle',
+                publicID=uri
+            )
         #pdb.set_trace()
         #pdb.set_trace()
         evt, _ = rsrc_api.create_or_replace(uid, graph=gr2)
         evt, _ = rsrc_api.create_or_replace(uid, graph=gr2)
         assert evt == RES_UPDATED
         assert evt == RES_UPDATED
 
 
         rsrc = rsrc_api.get(uid)
         rsrc = rsrc_api.get(uid)
-        assert not rsrc.imr[
-                rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
-        assert rsrc.imr[
-                rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#B')]
-        assert rsrc.imr[
-                rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert not rsrc.imr[
+                    rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
+            assert rsrc.imr[
+                    rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#B')]
+            assert rsrc.imr[
+                    rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
 
 
 
 
     def test_replace_incompatible_type(self):
     def test_replace_incompatible_type(self):
@@ -167,9 +180,11 @@ class TestResourceCRUD:
         uid_rs = '/test_incomp_rs'
         uid_rs = '/test_incomp_rs'
         uid_nr = '/test_incomp_nr'
         uid_nr = '/test_incomp_nr'
         data = b'mock binary content'
         data = b'mock binary content'
-        gr = Graph().parse(
-            data='<> a <http://ex.org/type#A> .', format='turtle',
-            publicID=nsc['fcres'][uid_rs])
+        with env.app_globals.rdf_store.txn_ctx():
+            gr = from_rdf(
+                data='<> a <http://ex.org/type#A> .', format='turtle',
+                publicID=nsc['fcres'][uid_rs]
+            )
 
 
         rsrc_api.create_or_replace(uid_rs, graph=gr)
         rsrc_api.create_or_replace(uid_rs, graph=gr)
         rsrc_api.create_or_replace(
         rsrc_api.create_or_replace(
@@ -203,17 +218,18 @@ class TestResourceCRUD:
             (URIRef(uri), nsc['rdf'].type, nsc['foaf'].Organization),
             (URIRef(uri), nsc['rdf'].type, nsc['foaf'].Organization),
         }
         }
 
 
-        gr = Graph()
-        gr += init_trp
+        with env.app_globals.rdf_store.txn_ctx():
+            gr = Graph(data=init_trp)
         rsrc_api.create_or_replace(uid, graph=gr)
         rsrc_api.create_or_replace(uid, graph=gr)
         rsrc_api.update_delta(uid, remove_trp, add_trp)
         rsrc_api.update_delta(uid, remove_trp, add_trp)
         rsrc = rsrc_api.get(uid)
         rsrc = rsrc_api.get(uid)
 
 
-        assert rsrc.imr[
-                rsrc.uri : nsc['rdf'].type : nsc['foaf'].Organization]
-        assert rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joe Bob')]
-        assert not rsrc.imr[
-                rsrc.uri : nsc['rdf'].type : nsc['foaf'].Person]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert rsrc.imr[
+                    rsrc.uri : nsc['rdf'].type : nsc['foaf'].Organization]
+            assert rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joe Bob')]
+            assert not rsrc.imr[
+                    rsrc.uri : nsc['rdf'].type : nsc['foaf'].Person]
 
 
 
 
     def test_delta_update_wildcard(self):
     def test_delta_update_wildcard(self):
@@ -235,20 +251,21 @@ class TestResourceCRUD:
             (URIRef(uri), nsc['foaf'].name, Literal('Joan Knob')),
             (URIRef(uri), nsc['foaf'].name, Literal('Joan Knob')),
         }
         }
 
 
-        gr = Graph()
-        gr += init_trp
+        with env.app_globals.rdf_store.txn_ctx():
+            gr = Graph(data=init_trp)
         rsrc_api.create_or_replace(uid, graph=gr)
         rsrc_api.create_or_replace(uid, graph=gr)
         rsrc_api.update_delta(uid, remove_trp, add_trp)
         rsrc_api.update_delta(uid, remove_trp, add_trp)
         rsrc = rsrc_api.get(uid)
         rsrc = rsrc_api.get(uid)
 
 
-        assert rsrc.imr[
-                rsrc.uri : nsc['rdf'].type : nsc['foaf'].Person]
-        assert rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joan Knob')]
-        assert not rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joe Bob')]
-        assert not rsrc.imr[
-            rsrc.uri : nsc['foaf'].name : Literal('Joe Average Bob')]
-        assert not rsrc.imr[
-            rsrc.uri : nsc['foaf'].name : Literal('Joe 12oz Bob')]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert rsrc.imr[
+                    rsrc.uri : nsc['rdf'].type : nsc['foaf'].Person]
+            assert rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joan Knob')]
+            assert not rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joe Bob')]
+            assert not rsrc.imr[
+                rsrc.uri : nsc['foaf'].name : Literal('Joe Average Bob')]
+            assert not rsrc.imr[
+                rsrc.uri : nsc['foaf'].name : Literal('Joe 12oz Bob')]
 
 
 
 
     def test_sparql_update(self):
     def test_sparql_update(self):
@@ -272,19 +289,20 @@ class TestResourceCRUD:
         ver_uid = rsrc_api.create_version(uid, 'v1').split('fcr:versions/')[-1]
         ver_uid = rsrc_api.create_version(uid, 'v1').split('fcr:versions/')[-1]
 
 
         rsrc = rsrc_api.update(uid, update_str)
         rsrc = rsrc_api.update(uid, update_str)
-        assert (
-            (rsrc.uri, nsc['dcterms'].title, Literal('Original title.'))
-            not in set(rsrc.imr))
-        assert (
-            (rsrc.uri, nsc['dcterms'].title, Literal('Title #2.'))
-            in set(rsrc.imr))
-        assert (
-            (rsrc.uri, nsc['dcterms'].title, Literal('Title #3.'))
-            in set(rsrc.imr))
-        assert ((
-                URIRef(str(rsrc.uri) + '#h1'),
-                nsc['dcterms'].title, Literal('This is a hash.'))
-            in set(rsrc.imr))
+        with env.app_globals.rdf_store.txn_ctx():
+            assert (
+                (rsrc.uri, nsc['dcterms'].title, Literal('Original title.'))
+                not in set(rsrc.imr))
+            assert (
+                (rsrc.uri, nsc['dcterms'].title, Literal('Title #2.'))
+                in set(rsrc.imr))
+            assert (
+                (rsrc.uri, nsc['dcterms'].title, Literal('Title #3.'))
+                in set(rsrc.imr))
+            assert ((
+                    URIRef(str(rsrc.uri) + '#h1'),
+                    nsc['dcterms'].title, Literal('This is a hash.'))
+                in set(rsrc.imr))
 
 
 
 
     def test_create_ldp_dc_post(self, dc_rdf):
     def test_create_ldp_dc_post(self, dc_rdf):
@@ -297,8 +315,9 @@ class TestResourceCRUD:
 
 
         member_rsrc = rsrc_api.get('/member')
         member_rsrc = rsrc_api.get('/member')
 
 
-        assert nsc['ldp'].Container in dc_rsrc.ldp_types
-        assert nsc['ldp'].DirectContainer in dc_rsrc.ldp_types
+        with env.app_globals.rdf_store.txn_ctx():
+            assert nsc['ldp'].Container in dc_rsrc.ldp_types
+            assert nsc['ldp'].DirectContainer in dc_rsrc.ldp_types
 
 
 
 
     def test_create_ldp_dc_put(self, dc_rdf):
     def test_create_ldp_dc_put(self, dc_rdf):
@@ -311,8 +330,9 @@ class TestResourceCRUD:
 
 
         member_rsrc = rsrc_api.get('/member')
         member_rsrc = rsrc_api.get('/member')
 
 
-        assert nsc['ldp'].Container in dc_rsrc.ldp_types
-        assert nsc['ldp'].DirectContainer in dc_rsrc.ldp_types
+        with env.app_globals.rdf_store.txn_ctx():
+            assert nsc['ldp'].Container in dc_rsrc.ldp_types
+            assert nsc['ldp'].DirectContainer in dc_rsrc.ldp_types
 
 
 
 
     def test_add_dc_member(self, dc_rdf):
     def test_add_dc_member(self, dc_rdf):
@@ -326,8 +346,9 @@ class TestResourceCRUD:
         child_uid = rsrc_api.create(dc_uid, None).uid
         child_uid = rsrc_api.create(dc_uid, None).uid
         member_rsrc = rsrc_api.get('/member')
         member_rsrc = rsrc_api.get('/member')
 
 
-        assert member_rsrc.imr[
-            member_rsrc.uri: nsc['dcterms'].relation: nsc['fcres'][child_uid]]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert member_rsrc.imr[
+                member_rsrc.uri: nsc['dcterms'].relation: nsc['fcres'][child_uid]]
 
 
 
 
     def test_indirect_container(self, ic_rdf):
     def test_indirect_container(self, ic_rdf):
@@ -349,15 +370,17 @@ class TestResourceCRUD:
                 member_uid, rdf_data=ic_member_rdf, rdf_fmt='turtle')
                 member_uid, rdf_data=ic_member_rdf, rdf_fmt='turtle')
 
 
         ic_rsrc = rsrc_api.get(ic_uid)
         ic_rsrc = rsrc_api.get(ic_uid)
-        assert nsc['ldp'].Container in ic_rsrc.ldp_types
-        assert nsc['ldp'].IndirectContainer in ic_rsrc.ldp_types
-        assert nsc['ldp'].DirectContainer not in ic_rsrc.ldp_types
+        with env.app_globals.rdf_store.txn_ctx():
+            assert nsc['ldp'].Container in ic_rsrc.ldp_types
+            assert nsc['ldp'].IndirectContainer in ic_rsrc.ldp_types
+            assert nsc['ldp'].DirectContainer not in ic_rsrc.ldp_types
 
 
         member_rsrc = rsrc_api.get(member_uid)
         member_rsrc = rsrc_api.get(member_uid)
         top_cont_rsrc = rsrc_api.get(cont_uid)
         top_cont_rsrc = rsrc_api.get(cont_uid)
-        assert top_cont_rsrc.imr[
-            top_cont_rsrc.uri: nsc['dcterms'].relation:
-            nsc['fcres'][target_uid]]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert top_cont_rsrc.imr[
+                top_cont_rsrc.uri: nsc['dcterms'].relation:
+                nsc['fcres'][target_uid]]
 
 
 
 
 
 
@@ -387,7 +410,8 @@ class TestAdvancedDelete:
         rsrc_api.resurrect(uid)
         rsrc_api.resurrect(uid)
 
 
         rsrc = rsrc_api.get(uid)
         rsrc = rsrc_api.get(uid)
-        assert nsc['ldp'].Resource in rsrc.ldp_types
+        with env.app_globals.rdf_store.txn_ctx():
+            assert nsc['ldp'].Resource in rsrc.ldp_types
 
 
 
 
     def test_hard_delete(self):
     def test_hard_delete(self):
@@ -431,10 +455,12 @@ class TestAdvancedDelete:
         uid = '/test_soft_delete_children01'
         uid = '/test_soft_delete_children01'
         rsrc_api.resurrect(uid)
         rsrc_api.resurrect(uid)
         parent_rsrc = rsrc_api.get(uid)
         parent_rsrc = rsrc_api.get(uid)
-        assert nsc['ldp'].Resource in parent_rsrc.ldp_types
+        with env.app_globals.rdf_store.txn_ctx():
+            assert nsc['ldp'].Resource in parent_rsrc.ldp_types
         for i in range(3):
         for i in range(3):
             child_rsrc = rsrc_api.get('{}/child{}'.format(uid, i))
             child_rsrc = rsrc_api.get('{}/child{}'.format(uid, i))
-            assert nsc['ldp'].Resource in child_rsrc.ldp_types
+            with env.app_globals.rdf_store.txn_ctx():
+                assert nsc['ldp'].Resource in child_rsrc.ldp_types
 
 
 
 
     def test_hard_delete_children(self):
     def test_hard_delete_children(self):
@@ -513,24 +539,26 @@ class TestResourceVersioning:
         rsrc_api.create_or_replace(uid, rdf_data=rdf_data, rdf_fmt='turtle')
         rsrc_api.create_or_replace(uid, rdf_data=rdf_data, rdf_fmt='turtle')
         ver_uid = rsrc_api.create_version(uid, 'v1').split('fcr:versions/')[-1]
         ver_uid = rsrc_api.create_version(uid, 'v1').split('fcr:versions/')[-1]
         #FIXME Without this, the test fails.
         #FIXME Without this, the test fails.
-        set(rsrc_api.get_version(uid, ver_uid))
+        #set(rsrc_api.get_version(uid, ver_uid))
 
 
         rsrc_api.update(uid, update_str)
         rsrc_api.update(uid, update_str)
         current = rsrc_api.get(uid)
         current = rsrc_api.get(uid)
-        assert (
-            (current.uri, nsc['dcterms'].title, Literal('Title #2.'))
-            in current.imr)
-        assert (
-            (current.uri, nsc['dcterms'].title, Literal('Original title.'))
-            not in current.imr)
+        with env.app_globals.rdf_store.txn_ctx():
+            assert (
+                (current.uri, nsc['dcterms'].title, Literal('Title #2.'))
+                in current.imr)
+            assert (
+                (current.uri, nsc['dcterms'].title, Literal('Original title.'))
+                not in current.imr)
 
 
         v1 = rsrc_api.get_version(uid, ver_uid)
         v1 = rsrc_api.get_version(uid, ver_uid)
-        assert (
-            (v1.uri, nsc['dcterms'].title, Literal('Original title.'))
-            in set(v1))
-        assert (
-            (v1.uri, nsc['dcterms'].title, Literal('Title #2.'))
-            not in set(v1))
+        with env.app_globals.rdf_store.txn_ctx():
+            assert (
+                (v1.uri, nsc['dcterms'].title, Literal('Original title.'))
+                in set(v1))
+            assert (
+                (v1.uri, nsc['dcterms'].title, Literal('Title #2.'))
+                    not in set(v1))
 
 
 
 
     def test_revert_to_version(self):
     def test_revert_to_version(self):
@@ -543,9 +571,10 @@ class TestResourceVersioning:
         ver_uid = 'v1'
         ver_uid = 'v1'
         rsrc_api.revert_to_version(uid, ver_uid)
         rsrc_api.revert_to_version(uid, ver_uid)
         rev = rsrc_api.get(uid)
         rev = rsrc_api.get(uid)
-        assert (
-            (rev.uri, nsc['dcterms'].title, Literal('Original title.'))
-            in rev.imr)
+        with env.app_globals.rdf_store.txn_ctx():
+            assert (
+                (rev.uri, nsc['dcterms'].title, Literal('Original title.'))
+                in rev.imr)
 
 
 
 
     def test_versioning_children(self):
     def test_versioning_children(self):
@@ -567,18 +596,21 @@ class TestResourceVersioning:
         rsrc_api.create_or_replace(ch1_uid)
         rsrc_api.create_or_replace(ch1_uid)
         ver_uid = rsrc_api.create_version(uid, ver_uid).split('fcr:versions/')[-1]
         ver_uid = rsrc_api.create_version(uid, ver_uid).split('fcr:versions/')[-1]
         rsrc = rsrc_api.get(uid)
         rsrc = rsrc_api.get(uid)
-        assert nsc['fcres'][ch1_uid] in rsrc.imr[
-                rsrc.uri : nsc['ldp'].contains]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert nsc['fcres'][ch1_uid] in rsrc.imr[
+                    rsrc.uri : nsc['ldp'].contains]
 
 
         rsrc_api.create_or_replace(ch2_uid)
         rsrc_api.create_or_replace(ch2_uid)
         rsrc = rsrc_api.get(uid)
         rsrc = rsrc_api.get(uid)
-        assert nsc['fcres'][ch2_uid] in rsrc.imr[
-                rsrc.uri : nsc['ldp'].contains]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert nsc['fcres'][ch2_uid] in rsrc.imr[
+                    rsrc.uri : nsc['ldp'].contains]
 
 
         rsrc_api.revert_to_version(uid, ver_uid)
         rsrc_api.revert_to_version(uid, ver_uid)
         rsrc = rsrc_api.get(uid)
         rsrc = rsrc_api.get(uid)
-        assert nsc['fcres'][ch1_uid] in rsrc.imr[
-                rsrc.uri : nsc['ldp'].contains]
-        assert nsc['fcres'][ch2_uid] in rsrc.imr[
-                rsrc.uri : nsc['ldp'].contains]
+        with env.app_globals.rdf_store.txn_ctx():
+            assert nsc['fcres'][ch1_uid] in rsrc.imr[
+                    rsrc.uri : nsc['ldp'].contains]
+            assert nsc['fcres'][ch2_uid] in rsrc.imr[
+                    rsrc.uri : nsc['ldp'].contains]
 
 

+ 11 - 6
tests/1_api/test_admin_api.py → tests/2_api/test_2_1_admin_api.py

@@ -4,13 +4,14 @@ import pytest
 from io import BytesIO
 from io import BytesIO
 from uuid import uuid4
 from uuid import uuid4
 
 
-from rdflib import Graph, URIRef
+from rdflib import URIRef
 
 
 from lakesuperior import env
 from lakesuperior import env
 from lakesuperior.api import resource as rsrc_api
 from lakesuperior.api import resource as rsrc_api
 from lakesuperior.api import admin as admin_api
 from lakesuperior.api import admin as admin_api
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.exceptions import ChecksumValidationError
 from lakesuperior.exceptions import ChecksumValidationError
+from lakesuperior.model.rdf.graph import Graph, from_rdf
 
 
 
 
 @pytest.mark.usefixtures('db')
 @pytest.mark.usefixtures('db')
@@ -25,9 +26,12 @@ class TestAdminApi:
         """
         """
         uid1 = '/test_refint1'
         uid1 = '/test_refint1'
         uid2 = '/test_refint2'
         uid2 = '/test_refint2'
-        gr = Graph().parse(
-                data='<> <http://ex.org/ns#p1> <info:fcres{}> .'.format(uid1),
-                format='turtle', publicID=nsc['fcres'][uid2])
+        with env.app_globals.rdf_store.txn_ctx():
+            gr = from_rdf(
+                store=env.app_globals.rdf_store,
+                data=f'<> <http://ex.org/ns#p1> <info:fcres{uid1}> .',
+                format='turtle', publicID=nsc['fcres'][uid2]
+            )
         rsrc_api.create_or_replace(uid1, graph=gr)
         rsrc_api.create_or_replace(uid1, graph=gr)
 
 
         assert admin_api.integrity_check() == set()
         assert admin_api.integrity_check() == set()
@@ -76,8 +80,9 @@ class TestAdminApi:
 
 
         _, rsrc = rsrc_api.create_or_replace(uid, stream=content)
         _, rsrc = rsrc_api.create_or_replace(uid, stream=content)
 
 
-        with open(rsrc.local_path, 'wb') as fh:
-            fh.write(uuid4().bytes)
+        with env.app_globals.rdf_store.txn_ctx():
+            with open(rsrc.local_path, 'wb') as fh:
+                fh.write(uuid4().bytes)
 
 
         with pytest.raises(ChecksumValidationError):
         with pytest.raises(ChecksumValidationError):
             admin_api.fixity_check(uid)
             admin_api.fixity_check(uid)

+ 1 - 1
tests/2_endpoints/test_ldp.py → tests/3_endpoints/test_3_0_ldp.py

@@ -18,7 +18,7 @@ from rdflib.term import Literal, URIRef
 
 
 from lakesuperior import env
 from lakesuperior import env
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
-from lakesuperior.model.ldpr import Ldpr
+from lakesuperior.model.ldp.ldpr import Ldpr
 
 
 
 
 digest_algo = env.app_globals.config['application']['uuid']['algo']
 digest_algo = env.app_globals.config['application']['uuid']['algo']

+ 4 - 1
tests/2_endpoints/test_admin.py → tests/3_endpoints/test_3_1_admin.py

@@ -3,6 +3,7 @@ import pytest
 from io import BytesIO
 from io import BytesIO
 from uuid import uuid4
 from uuid import uuid4
 
 
+from lakesuperior import env
 from lakesuperior.api import resource as rsrc_api
 from lakesuperior.api import resource as rsrc_api
 
 
 
 
@@ -46,7 +47,9 @@ class TestAdminApi:
 
 
         rsrc = rsrc_api.get(f'/{uid}')
         rsrc = rsrc_api.get(f'/{uid}')
 
 
-        with open(rsrc.local_path, 'wb') as fh:
+        with env.app_globals.rdf_store.txn_ctx():
+            fname = rsrc.local_path
+        with open(fname, 'wb') as fh:
             fh.write(uuid4().bytes)
             fh.write(uuid4().bytes)
 
 
         rsp = self.client.get(fix_path)
         rsp = self.client.get(fix_path)

+ 0 - 0
tests/2_endpoints/test_query.py → tests/3_endpoints/test_3_2_query.py


+ 0 - 0
tests/3_ancillary/test_toolbox.py → tests/4_ancillary/test_4_0_toolbox.py


Some files were not shown because too many files changed in this diff