aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bsfs/triple_store/base.py6
-rw-r--r--bsfs/triple_store/sparql/parse_filter.py307
-rw-r--r--bsfs/triple_store/sparql/sparql.py51
-rw-r--r--test/triple_store/sparql/test_parse_filter.py727
-rw-r--r--test/triple_store/sparql/test_sparql.py90
5 files changed, 1165 insertions, 16 deletions
diff --git a/bsfs/triple_store/base.py b/bsfs/triple_store/base.py
index 5ff9523..7e03714 100644
--- a/bsfs/triple_store/base.py
+++ b/bsfs/triple_store/base.py
@@ -113,9 +113,11 @@ class TripleStoreBase(abc.ABC):
def get(
self,
node_type: _schema.Node,
- query: ast.filter.FilterExpression,
+ query: typing.Optional[ast.filter.FilterExpression] = None,
) -> typing.Iterator[URI]:
- """Return guids of nodes of type *node_type* that match the *query*."""
+ """Return guids of nodes of type *node_type* that match the *query*.
+ Return all guids of the respective type if *query* is None.
+ """
@abc.abstractmethod
def exists(
diff --git a/bsfs/triple_store/sparql/parse_filter.py b/bsfs/triple_store/sparql/parse_filter.py
new file mode 100644
index 0000000..d4db0aa
--- /dev/null
+++ b/bsfs/triple_store/sparql/parse_filter.py
@@ -0,0 +1,307 @@
+"""
+
+Part of the BlackStar filesystem (bsfs) module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import typing
+
+# bsfs imports
+from bsfs import schema as bsc
+from bsfs.namespace import ns
+from bsfs.query import ast
+from bsfs.utils import URI, errors
+
+# exports
+__all__: typing.Sequence[str] = (
+ 'Filter',
+ )
+
+class _GenHopName():
+ """Generator that produces a new unique symbol name with each iteration."""
+
+ # Symbol name prefix.
+ prefix: str
+
+ # Current counter.
+ curr: int
+
+ def __init__(self, prefix: str = '?hop', start: int = 0):
+ self.prefix = prefix
+ self.curr = start - 1
+
+ def __next__(self):
+ """Generate and return the next unique name."""
+ self.curr += 1
+ return self.prefix + str(self.curr)
+
+
+class Filter():
+ """Translate `bsfs.query.ast.filter` structures into Sparql queries."""
+
+ # Current schema to validate against.
+ schema: bsc.Schema
+
+ # Generator that produces unique symbol names.
+ ngen: _GenHopName
+
+ # Vertex type.
+ T_VERTEX = typing.Union[bsc.Node, bsc.Literal]
+
+ def __init__(self, schema):
+ self.schema = schema
+ self.ngen = _GenHopName()
+
+ def __call__(
+ self,
+ root_type: bsc.Node,
+ root: typing.Optional[ast.filter.FilterExpression] = None,
+ ) -> str:
+ """
+ """
+ # check root_type
+ if not isinstance(root_type, bsc.Node):
+ raise errors.BackendError(f'expected Node, found {root_type}')
+ if root_type not in self.schema.nodes():
+ raise errors.ConsistencyError(f'node {root_type} is not in the schema')
+ # parse root
+ if root is None:
+ cond = ''
+ else:
+ cond = self._parse_filter_expression(root_type, root, '?ent')
+ # assemble query
+ return f'''
+ SELECT ?ent
+ WHERE {{
+ ?ent <{ns.rdf.type}>/<{ns.rdfs.subClassOf}>* <{root_type.uri}> .
+ {cond}
+ }}
+ '''
+
+ def _parse_filter_expression(self, type_: T_VERTEX, node: ast.filter.FilterExpression, head: str) -> str:
+ """Route *node* to the handler of the respective FilterExpression subclass."""
+ if isinstance(node, ast.filter.Is):
+ return self._is(type_, node, head)
+ if isinstance(node, ast.filter.Not):
+ return self._not(type_, node, head)
+ if isinstance(node, ast.filter.Has):
+ return self._has(type_, node, head)
+ if isinstance(node, ast.filter.Any):
+ return self._any(type_, node, head)
+ if isinstance(node, ast.filter.All):
+ return self._all(type_, node, head)
+ if isinstance(node, ast.filter.And):
+ return self._and(type_, node, head)
+ if isinstance(node, ast.filter.Or):
+ return self._or(type_, node, head)
+ if isinstance(node, ast.filter.Equals):
+ return self._equals(type_, node, head)
+ if isinstance(node, ast.filter.Substring):
+ return self._substring(type_, node, head)
+ if isinstance(node, ast.filter.StartsWith):
+ return self._starts_with(type_, node, head)
+ if isinstance(node, ast.filter.EndsWith):
+ return self._ends_with(type_, node, head)
+ if isinstance(node, ast.filter.LessThan):
+ return self._less_than(type_, node, head)
+ if isinstance(node, ast.filter.GreaterThan):
+ return self._greater_than(type_, node, head)
+ # invalid node
+ raise errors.BackendError(f'expected filter expression, found {node}')
+
+ def _parse_predicate_expression(
+ self,
+ type_: T_VERTEX,
+ node: ast.filter.PredicateExpression
+ ) -> typing.Tuple[str, T_VERTEX]:
+ """Route *node* to the handler of the respective PredicateExpression subclass."""
+ if isinstance(node, ast.filter.Predicate):
+ return self._predicate(type_, node)
+ if isinstance(node, ast.filter.OneOf):
+ return self._one_of(type_, node)
+ # invalid node
+ raise errors.BackendError(f'expected predicate expression, found {node}')
+
+ def _one_of(self, node_type: T_VERTEX, node: ast.filter.OneOf) -> typing.Tuple[str, T_VERTEX]:
+ """
+ """
+ if not isinstance(node_type, bsc.Node):
+ raise errors.BackendError(f'expected Node, found {node_type}')
+ # walk through predicates
+ suburi, rng = set(), None
+ for pred in node: # OneOf guarantees at least one expression
+ puri, subrng = self._parse_predicate_expression(node_type, pred)
+ # track predicate uris
+ suburi.add(puri)
+ try:
+ # check for more generic range
+ if rng is None or subrng > rng:
+ rng = subrng
+ # check range consistency
+ if not subrng <= rng and not subrng >= rng:
+ raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related')
+ except TypeError as err: # subrng and rng are not comparable
+ raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') from err
+ if rng is None:
+ # for mypy to be certain of the rng type
+ # if rng were None, we'd have gotten a TypeError above (None > None)
+ raise errors.UnreachableError()
+ # return joint predicate expression and next range
+ return '|'.join(suburi), rng
+
+ def _predicate(self, node_type: T_VERTEX, node: ast.filter.Predicate) -> typing.Tuple[str, T_VERTEX]:
+ """
+ """
+ # check node_type
+ if not isinstance(node_type, bsc.Node):
+ raise errors.BackendError(f'expected Node, found {node_type}')
+ # fetch predicate and its uri
+ puri = node.predicate
+ # get and check predicate, domain, and range
+ if not self.schema.has_predicate(puri):
+ raise errors.ConsistencyError(f'predicate {puri} is not in the schema')
+ pred = self.schema.predicate(puri)
+ if pred.range is None:
+ # FIXME: It is a design error that Predicates can have a None range...
+ raise errors.BackendError(f'predicate {pred} has no range')
+ dom, rng = pred.domain, pred.range
+ # encapsulate predicate uri
+ puri = f'<{puri}>' # type: ignore [assignment] # variable re-use confuses mypy
+ # apply reverse flag
+ if node.reverse:
+ puri = URI('^' + puri)
+ dom, rng = rng, dom # type: ignore [assignment] # variable re-use confuses mypy
+ # check path consistency
+ if not node_type <= dom:
+ raise errors.ConsistencyError(f'expected type {dom} or subtype thereof, found {node_type}')
+ # return predicate URI and next node type
+ return puri, rng
+
+ def _any(self, node_type: T_VERTEX, node: ast.filter.Any, head: str) -> str:
+ """
+ """
+ if not isinstance(node_type, bsc.Node):
+ raise errors.BackendError(f'expected Node, found {node_type}')
+ # parse predicate
+ pred, next_type = self._parse_predicate_expression(node_type, node.predicate)
+ # parse expression
+ nexthead = next(self.ngen)
+ expr = self._parse_filter_expression(next_type, node.expr, nexthead)
+ # combine results
+ return f'{head} {pred} {nexthead} . {expr}'
+
+ def _all(self, node_type: T_VERTEX, node: ast.filter.All, head: str) -> str:
+ """
+ """
+ # NOTE: All(P, E) := Not(Any(P, Not(E))) and EXISTS(P, ?)
+ if not isinstance(node_type, bsc.Node):
+ raise errors.BackendError(f'expected Node, found {node_type}')
+ # parse rewritten ast
+ expr = self._parse_filter_expression(node_type,
+ ast.filter.Not(
+ ast.filter.Any(node.predicate,
+ ast.filter.Not(node.expr))), head)
+ # parse predicate for existence constraint
+ pred, _ = self._parse_predicate_expression(node_type, node.predicate)
+ temphead = next(self.ngen)
+ # return existence and rewritten expression
+ return f'FILTER EXISTS {{ {head} {pred} {temphead} }} . ' + expr
+
+ def _and(self, node_type: T_VERTEX, node: ast.filter.And, head: str) -> str:
+ """
+ """
+ sub = [self._parse_filter_expression(node_type, expr, head) for expr in node]
+ return ' . '.join(sub)
+
+ def _or(self, node_type: T_VERTEX, node: ast.filter.Or, head: str) -> str:
+ """
+ """
+ # potential special case optimization:
+ # * ast: Or(Equals('foo'), Equals('bar'), ...)
+ # * query: VALUES ?head { "value1"^^<...> "value2"^^<...> "value3"^<...> ... }
+ sub = [self._parse_filter_expression(node_type, expr, head) for expr in node]
+ sub = ['{' + expr + '}' for expr in sub]
+ return ' UNION '.join(sub)
+
+ def _not(self, node_type: T_VERTEX, node: ast.filter.Not, head: str) -> str:
+ """
+ """
+ expr = self._parse_filter_expression(node_type, node.expr, head)
+ if isinstance(node_type, bsc.Literal):
+ return f'MINUS {{ {expr} }}'
+ # NOTE: for bsc.Node types, we must include at least one expression in the body of MINUS,
+ # otherwise the connection between the context and body of MINUS is lost.
+ # The simplest (and non-interfering) choice is a type statement.
+ return f'MINUS {{ {head} <{ns.rdf.type}>/<{ns.rdfs.subClassOf}>* <{node_type.uri}> . {expr} }}'
+
+ def _has(self, node_type: T_VERTEX, node: ast.filter.Has, head: str) -> str:
+ """
+ """
+ if not isinstance(node_type, bsc.Node):
+ raise errors.BackendError(f'expected Node, found {node_type}')
+ # parse predicate
+ pred, _ = self._parse_predicate_expression(node_type, node.predicate)
+ # get new heads
+ inner = next(self.ngen)
+ outer = next(self.ngen)
+ # predicate count expression (fetch number of predicates at *head*)
+ num_preds = f'{{ SELECT (COUNT(distinct {inner}) as {outer}) WHERE {{ {head} {pred} {inner} }} }}'
+ # count expression
+ # FIXME: We have to ensure that ns.xsd.integer is always known in the schema!
+ count_bounds = self._parse_filter_expression(self.schema.literal(ns.xsd.integer), node.count, outer)
+ # combine
+ return num_preds + ' . ' + count_bounds
+
+ def _is(self, node_type: T_VERTEX, node: ast.filter.Is, head: str) -> str:
+ """
+ """
+ if not isinstance(node_type, bsc.Node):
+ raise errors.BackendError(f'expected Node, found {node_type}')
+ return f'VALUES {head} {{ <{node.value}> }}'
+
+ def _equals(self, node_type: T_VERTEX, node: ast.filter.Equals, head: str) -> str:
+ """
+ """
+ if not isinstance(node_type, bsc.Literal):
+ raise errors.BackendError(f'expected Literal, found {node}')
+ return f'VALUES {head} {{ "{node.value}"^^<{node_type.uri}> }}'
+
+ def _substring(self, node_type: T_VERTEX, node: ast.filter.Substring, head: str) -> str:
+ """
+ """
+ if not isinstance(node_type, bsc.Literal):
+ raise errors.BackendError(f'expected Literal, found {node_type}')
+ return f'FILTER contains(str({head}), "{node.value}")'
+
+ def _starts_with(self, node_type: T_VERTEX, node: ast.filter.StartsWith, head: str) -> str:
+ """
+ """
+ if not isinstance(node_type, bsc.Literal):
+ raise errors.BackendError(f'expected Literal, found {node_type}')
+ return f'FILTER strstarts(str({head}), "{node.value}")'
+
+ def _ends_with(self, node_type: T_VERTEX, node: ast.filter.EndsWith, head: str) -> str:
+ """
+ """
+ if not isinstance(node_type, bsc.Literal):
+ raise errors.BackendError(f'expected Literal, found {node_type}')
+ return f'FILTER strends(str({head}), "{node.value}")'
+
+ def _less_than(self, node_type: T_VERTEX, node: ast.filter.LessThan, head: str) -> str:
+ """
+ """
+ if not isinstance(node_type, bsc.Literal):
+ raise errors.BackendError(f'expected Literal, found {node_type}')
+ equality = '=' if not node.strict else ''
+ return f'FILTER ({head} <{equality} {float(node.threshold)})'
+
+ def _greater_than(self, node_type: T_VERTEX, node: ast.filter.GreaterThan, head: str) -> str:
+ """
+ """
+ if not isinstance(node_type, bsc.Literal):
+ raise errors.BackendError(f'expected Literal, found {node_type}')
+ equality = '=' if not node.strict else ''
+ return f'FILTER ({head} >{equality} {float(node.threshold)})'
+
+## EOF ##
diff --git a/bsfs/triple_store/sparql/sparql.py b/bsfs/triple_store/sparql/sparql.py
index 7172f34..c3cbff6 100644
--- a/bsfs/triple_store/sparql/sparql.py
+++ b/bsfs/triple_store/sparql/sparql.py
@@ -15,6 +15,7 @@ from bsfs.query import ast
from bsfs.utils import errors, URI
# inner-module imports
+from . import parse_filter
from .. import base
@@ -86,11 +87,15 @@ class SparqlStore(base.TripleStoreBase):
# The local schema.
_schema: bsc.Schema
+ # Filter parser
+ _filter_parser: parse_filter.Filter
+
def __init__(self):
super().__init__(None)
self._graph = rdflib.Graph()
self._transaction = _Transaction(self._graph)
self._schema = bsc.Schema.Empty()
+ self._filter_parser = parse_filter.Filter(self._schema)
# NOTE: mypy and pylint complain about the **kwargs not being listed (contrasting super)
# However, not having it here is clearer since it's explicit that there are no arguments.
@@ -127,10 +132,17 @@ class SparqlStore(base.TripleStoreBase):
# get deleted classes
sub = self.schema - schema
- # remove predicate instances
for pred in sub.predicates:
+ # remove predicate instances
for src, trg in self._graph.subject_objects(rdflib.URIRef(pred.uri)):
self._transaction.remove((src, rdflib.URIRef(pred.uri), trg))
+ # remove predicate definition
+ if pred.parent is not None:
+ self._transaction.remove((
+ rdflib.URIRef(pred.uri),
+ rdflib.RDFS.subClassOf,
+ rdflib.URIRef(pred.parent.uri),
+ ))
# remove node instances
for node in sub.nodes:
@@ -144,17 +156,46 @@ class SparqlStore(base.TripleStoreBase):
self._transaction.remove((inst, pred, trg))
# remove instance
self._transaction.remove((inst, rdflib.RDF.type, rdflib.URIRef(node.uri)))
-
- # NOTE: Nothing to do for literals
+ # remove node definition
+ if node.parent is not None:
+ self._transaction.remove((
+ rdflib.URIRef(node.uri),
+ rdflib.RDFS.subClassOf,
+ rdflib.URIRef(node.parent.uri),
+ ))
+
+ for lit in sub.literals:
+ # remove literal definition
+ if lit.parent is not None:
+ self._transaction.remove((
+ rdflib.URIRef(lit.uri),
+ rdflib.RDFS.subClassOf,
+ rdflib.URIRef(lit.parent.uri),
+ ))
+
+ # add predicate, node, and literal hierarchies to the graph
+ for itm in itertools.chain(schema.predicates(), schema.nodes(), schema.literals()):
+ if itm.parent is not None:
+ self._transaction.add((rdflib.URIRef(itm.uri), rdflib.RDFS.subClassOf, rdflib.URIRef(itm.parent.uri)))
# commit instance changes
self.commit()
# migrate schema
self._schema = schema
+ self._filter_parser.schema = schema
- def get(self, node_type: bsc.Node, query: ast.filter.FilterExpression) -> typing.Iterator[URI]:
- raise NotImplementedError()
+ def get(
+ self,
+ node_type: bsc.Node,
+ query: typing.Optional[ast.filter.FilterExpression] = None,
+ ) -> typing.Iterator[URI]:
+ if node_type not in self.schema.nodes():
+ raise errors.ConsistencyError(f'{node_type} is not defined in the schema')
+ if not isinstance(query, ast.filter.FilterExpression):
+ raise TypeError(query)
+ for guid, in self._graph.query(self._filter_parser(node_type, query)):
+ yield URI(guid)
def _has_type(self, subject: URI, node_type: bsc.Node) -> bool:
"""Return True if *subject* is a node of class *node_type* or a subclass thereof."""
diff --git a/test/triple_store/sparql/test_parse_filter.py b/test/triple_store/sparql/test_parse_filter.py
new file mode 100644
index 0000000..bd19803
--- /dev/null
+++ b/test/triple_store/sparql/test_parse_filter.py
@@ -0,0 +1,727 @@
+"""
+
+Part of the bsfs test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import rdflib
+import unittest
+
+# bsie imports
+from bsfs import schema as _schema
+from bsfs.namespace import ns
+from bsfs.query import ast
+from bsfs.utils import errors
+
+# objects to test
+from bsfs.triple_store.sparql.parse_filter import Filter
+
+
+## code ##
+
+class TestParseFilter(unittest.TestCase):
+ def setUp(self):
+ # schema
+ self.schema = _schema.Schema.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Image rdfs:subClassOf bsfs:Entity .
+ bsfs:Tag rdfs:subClassOf bsfs:Node .
+
+ xsd:string rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:URI rdfs:subClassOf bsfs:Literal .
+
+ bse:comment rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:filesize rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bse:buddy rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Node ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:representative rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Tag ;
+ rdfs:range bsfs:Image ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:iso rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Image ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ ''')
+
+ # parser instance
+ self.parser = Filter(self.schema)
+
+ # graph to test queries
+ self.graph = rdflib.Graph()
+ # schema hierarchies
+ self.graph.add((rdflib.URIRef('http://bsfs.ai/schema/Entity'), rdflib.RDFS.subClassOf, rdflib.URIRef('http://bsfs.ai/schema/Node')))
+ self.graph.add((rdflib.URIRef('http://bsfs.ai/schema/Image'), rdflib.RDFS.subClassOf, rdflib.URIRef('http://bsfs.ai/schema/Entity')))
+ self.graph.add((rdflib.URIRef('http://bsfs.ai/schema/Tag'), rdflib.RDFS.subClassOf, rdflib.URIRef('http://bsfs.ai/schema/Node')))
+ # entities
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')))
+ # tags
+ self.graph.add((rdflib.URIRef('http://example.com/tag#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag')))
+ self.graph.add((rdflib.URIRef('http://example.com/tag#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag')))
+ # images
+ self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Image')))
+ self.graph.add((rdflib.URIRef('http://example.com/image#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Image')))
+ # node comments
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('Me, Myself, and I', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('hello world', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('hello world', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('Me, Myself, and I', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/tag#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('Me, Myself, and I', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/tag#4321'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('4321', datatype=rdflib.XSD.string)))
+ # entity filesizes
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.filesize), rdflib.Literal(1234, datatype=rdflib.XSD.integer)))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.filesize), rdflib.Literal(4321, datatype=rdflib.XSD.integer)))
+ self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.filesize), rdflib.Literal(1234, datatype=rdflib.XSD.integer)))
+ self.graph.add((rdflib.URIRef('http://example.com/image#4321'), rdflib.URIRef(ns.bse.filesize), rdflib.Literal(4321, datatype=rdflib.XSD.integer)))
+ # entity tags
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.tag), rdflib.URIRef('http://example.com/tag#1234')))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.tag), rdflib.URIRef('http://example.com/tag#4321')))
+ self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.tag), rdflib.URIRef('http://example.com/tag#1234')))
+ # tag representatives
+ self.graph.add((rdflib.URIRef('http://example.com/tag#1234'), rdflib.URIRef(ns.bse.representative), rdflib.URIRef('http://example.com/image#1234')))
+ self.graph.add((rdflib.URIRef('http://example.com/tag#4321'), rdflib.URIRef(ns.bse.representative), rdflib.URIRef('http://example.com/image#4321')))
+ # entity buddies
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.buddy), rdflib.URIRef('http://example.com/image#1234')))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.buddy), rdflib.URIRef('http://example.com/image#4321')))
+ # image iso
+ self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.iso), rdflib.Literal(1234, datatype=rdflib.XSD.integer)))
+ self.graph.add((rdflib.URIRef('http://example.com/image#4321'), rdflib.URIRef(ns.bse.iso), rdflib.Literal(4321, datatype=rdflib.XSD.integer)))
+
+
+ def test_routing(self):
+ self.assertRaises(errors.BackendError, self.parser._parse_filter_expression, '1234', None, '')
+ self.assertRaises(errors.BackendError, self.parser._parse_predicate_expression, '1234', None)
+
+ def test_call(self):
+ # NOTE: The individual ast components are considered in the respective tests. Here, we test __call__ specifics.
+
+ # __call__ requires a valid root type
+ self.assertRaises(errors.BackendError, self.parser, self.schema.literal(ns.bsfs.Literal), None)
+ self.assertRaises(errors.ConsistencyError, self.parser, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), None)
+ # __call__ requires a parseable root
+ self.assertRaises(errors.BackendError, self.parser, self.schema.node(ns.bsfs.Entity), ast.filter.FilterExpression())
+ # __call__ returns an executable query
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.Is('http://example.com/entity#5678')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, {'http://example.com/entity#1234'})
+ # root is optional
+ q = self.parser(self.schema.node(ns.bsfs.Entity))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234', 'http://example.com/image#4321'})
+ q = self.parser(self.schema.node(ns.bsfs.Tag))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/tag#1234', 'http://example.com/tag#4321'})
+
+
+ def test_is(self):
+ # _is requires a node
+ self.assertRaises(errors.BackendError, self.parser._is, self.schema.literal(ns.bsfs.Literal), ast.filter.Is('http://example.com/entity#1234'), '?ent')
+ # a single Is statement
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Is('http://example.com/entity#1234'))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234'})
+ # an aggregate of Is statements
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.Is('http://example.com/entity#4321'),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321'})
+ # combined with other filters
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.Is('http://example.com/entity#4321'),
+ ),
+ ast.filter.Any(ns.bse.comment,
+ ast.filter.Equals('Me, Myself, and I')
+ ),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234'})
+ # as argument of Any/All
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ns.bse.tag, ast.filter.Is('http://example.com/tag#1234')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+
+
+ def test_equals(self):
+ # _equals requires a literal
+ self.assertRaises(errors.BackendError, self.parser._equals, self.schema.node(ns.bsfs.Entity), ast.filter.Equals('hello world'), '?ent')
+ # a single Equals statement
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321'})
+ # a single Equals statement that includes subtypes
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+ # an Equals statement on an integer
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.Equals(4321)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#4321'})
+
+
+ def test_substring(self):
+ # _substring requires a literal
+ self.assertRaises(errors.BackendError, self.parser._substring, self.schema.node(ns.bsfs.Entity), ast.filter.Substring('hello world'), '?ent')
+ # a single Substring statement
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.Substring('hello')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321'})
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.Substring('lo wo')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321'})
+ # a single Substring statement that includes subtypes
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.Substring('Myself')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+ # an Substring statement on an integer
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.Substring('32')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#4321'})
+
+
+ def test_starts_with(self):
+ # _starts_with requires a literal
+ self.assertRaises(errors.BackendError, self.parser._starts_with, self.schema.node(ns.bsfs.Entity), ast.filter.StartsWith('hello world'), '?ent')
+ # a single StartsWith statement
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('hello')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321'})
+ # a single StartsWith statement that includes subtypes
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('Me, Mys')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+ # an StartsWith statement on an integer
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.StartsWith(432)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#4321'})
+
+
+ def test_ends_with(self):
+ # _ends_with requires a literal
+ self.assertRaises(errors.BackendError, self.parser._ends_with, self.schema.node(ns.bsfs.Entity), ast.filter.EndsWith('hello world'), '?ent')
+ # a single EndsWith statement
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.EndsWith('orld')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321'})
+ # a single EndsWith statement that includes subtypes
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.EndsWith('and I')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+ # an EndsWith statement on an integer
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.EndsWith(321)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#4321'})
+
+
+ def test_less_than(self):
+ # _less_than requires a literal
+ self.assertRaises(errors.BackendError, self.parser._less_than, self.schema.node(ns.bsfs.Entity), ast.filter.LessThan(2000), '?ent')
+ # a single LessThan statement
+ q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.LessThan(2000)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/image#1234'})
+ # _less_than respects boundary
+ q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.LessThan(1234, strict=True)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+ q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.LessThan(1234, strict=False)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/image#1234'})
+ # a single LessThan statement that includes subtypes
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.LessThan(2000)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+ # an LessThan statement on a string
+ # always negative; note that http://example.com/tag#4321 is also not returned although its comment is a pure number
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.LessThan(10_000)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+
+
+ def test_greater_than(self):
+ # _greater_than requires a literal
+ self.assertRaises(errors.BackendError, self.parser._greater_than, self.schema.node(ns.bsfs.Entity), ast.filter.GreaterThan(2000), '?ent')
+ # a single GreaterThan statement
+ q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.GreaterThan(2000)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/image#4321'})
+ # _greater_than respects boundary
+ q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.GreaterThan(4321, strict=True)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+ q = self.parser(self.schema.node(ns.bsfs.Image), ast.filter.Any(ns.bse.iso, ast.filter.GreaterThan(4321, strict=False)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/image#4321'})
+ # a single GreaterThan statement that includes subtypes
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.filesize, ast.filter.GreaterThan(2000)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#4321'})
+ # an GreaterThan statement on a string
+ # always positive
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.comment, ast.filter.GreaterThan(0)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234'})
+
+
+ def test_and(self):
+ # And childs have to match the node type
+ self.assertRaises(errors.BackendError, self.parser,
+ self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.StartsWith('hello'),
+ ast.filter.EndsWith('world'),
+ ))
+ # no child produces an empty query
+ self.assertEqual(self.parser._and(
+ self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(), '?ent'), '')
+ # And can mix different conditions
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234'})
+ # all conditions have to match
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.Is('http://example.com/entity#4321'),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(4321)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('foobar')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+ # And can be nested
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.And(
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')),
+ ),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234'})
+
+
+ def test_or(self):
+ # Or childs have to match the node type
+ self.assertRaises(errors.BackendError, self.parser,
+ self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.StartsWith('hello'),
+ ast.filter.EndsWith('world'),
+ ))
+ # no child produces an empty query
+ self.assertEqual(self.parser._and(
+ self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(), '?ent'), '')
+ # Or can mix different conditions
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(4321)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234', 'http://example.com/entity#4321', 'http://example.com/image#4321'})
+ # at least one condition has to match
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/entity#5678'),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(8765)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('foobar')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(8765)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('foobar')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234'})
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/entity#5678'),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(4321)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('foobar')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#4321'})
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/entity#5678'),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(8765)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+ # Or can be nested
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.Or(
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(4321)),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')),
+ ),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234', 'http://example.com/entity#4321', 'http://example.com/image#4321'})
+
+
+
+ def test_any(self):
+ # _any requires a node
+ self.assertRaises(errors.BackendError, self.parser._any,
+ self.schema.literal(ns.bsfs.Literal),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)), '?ent')
+ # node type must match predicate's domain
+ self.assertRaises(errors.ConsistencyError, self.parser._any,
+ self.schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)), '?ent')
+ # predicate must be valid
+ self.assertRaises(errors.ConsistencyError, self.parser._any,
+ self.schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ns.bse.invalid, ast.filter.Equals(1234)), '?ent')
+ # _any returns a valid query
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+ # _any can be nested
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ns.bse.tag,
+ ast.filter.Any(ns.bse.representative,
+ ast.filter.Is('http://example.com/image#1234'))))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+
+
+ def test_all(self):
+ # All requires a Node
+ self.assertRaises(errors.BackendError, self.parser._all, self.schema.literal(ns.bsfs.Literal), None, '')
+ # All Nodes
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.All(ns.bse.tag, ast.filter.Is('http://example.com/tag#1234')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+ # All values
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.All(ns.bse.comment, ast.filter.Equals('hello world')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321'})
+ # All on value within Or branch
+ # entity#1234 is selected because all of its comments are in ("hello world", "Me, Myself, and I")
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.All(ns.bse.comment, ast.filter.Or(
+ ast.filter.Equals('hello world'),
+ ast.filter.Equals('Me, Myself, and I'))))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234'})
+ # All requires at least one predicate/value
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.All(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/image#1234'})
+ # All within a statement
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.All(ns.bse.tag, ast.filter.Is('http://example.com/tag#1234')), # entity#1234, image#1234
+ ast.filter.All(ns.bse.comment, ast.filter.Or( # entity#1234, entity#4321, image#1234
+ ast.filter.Equals('hello world'),
+ ast.filter.Equals('Me, Myself, and I'),
+ ))
+ )
+ )
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+ # All with reversed Predicate
+ q = self.parser(self.schema.node(ns.bsfs.Tag),
+ ast.filter.All(ast.filter.Predicate(ns.bse.tag, reverse=True), ast.filter.Is('http://example.com/entity#4321')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/tag#4321'})
+ # All with multiple predicates
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.All(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy), # entity#1234 (tag:tag#1234), entity#1234 (buddy:image#1234), image#1234(tag:tag#1234)
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')))) # entity#1234, image#1234, tag#1234
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+
+
+
+ def test_not(self):
+ # Not applies on conditions
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Not(ast.filter.Is('http://example.com/entity#1234')))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/image#1234', 'http://example.com/entity#4321', 'http://example.com/image#4321'})
+ # Not applies on conditions within branches
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ns.bse.comment, ast.filter.Not(ast.filter.Equals('Me, Myself, and I'))))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321'})
+ # Not applies on branches
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Not(ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I'))))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#4321'})
+ # Double Not cancel each other
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Not(ast.filter.Not(ast.filter.Is('http://example.com/entity#1234'))))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234'})
+ # Not works within aggregation (and)
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.Not(ast.filter.Is('http://example.com/entity#1234')),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321'})
+ # Not works within aggregation (or)
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Or(
+ ast.filter.Not(ast.filter.Is('http://example.com/entity#1234')),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')),
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234', 'http://example.com/image#4321'})
+ # Not works outside aggregation (and)
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Not(
+ ast.filter.And(
+ ast.filter.Is('http://example.com/entity#1234'),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world')),
+ )))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#1234', 'http://example.com/image#4321'})
+ # Not works outside aggregation (or)
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Not(
+ ast.filter.Or(
+ ast.filter.Is('http://example.com/entity#4321'),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('Me, Myself, and I')),
+ )))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/image#4321'})
+ # Not mixed with branch, aggregation, id, and value
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.And(
+ ast.filter.Not( # image#1234, image#4321
+ ast.filter.Or( # entity#4321, entity#1234
+ ast.filter.Is('http://example.com/entity#4321'),
+ ast.filter.Any(ns.bse.comment, ast.filter.Equals('hello world')),
+ )
+ ),
+ ast.filter.Any(ns.bse.comment, ast.filter.Not(ast.filter.Equals('foobar'))), # entity#1234, entity#4321, image#1234
+ ))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/image#1234'})
+
+
+ def test_has(self):
+ # Has requires Node
+ self.assertRaises(errors.BackendError, self.parser._has, self.schema.literal(ns.bsfs.Literal), None, '')
+ # Has with GreaterThan constraint
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Has(ns.bse.comment, ast.filter.GreaterThan(0)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234'})
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Has(ns.bse.comment, ast.filter.GreaterThan(1)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234'})
+ # Has with Equals constraint
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Has(ns.bse.comment, 1))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#1234'})
+ # Has with LessThan constraint
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Has(ns.bse.comment, ast.filter.LessThan(2)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#1234', 'http://example.com/image#4321'})
+ # Has with multiple constraints
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra1', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra2', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra3', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra4', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra5', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra1', datatype=rdflib.XSD.string)))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.comment), rdflib.Literal('extra2', datatype=rdflib.XSD.string)))
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Has(ns.bse.comment,
+ ast.filter.And(ast.filter.GreaterThan(1), ast.filter.LessThan(5))))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321'})
+ # Has with OneOf predicate
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Has(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy),
+ ast.filter.GreaterThan(1)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321'})
+ # Has with reversed predicate
+ q = self.parser(self.schema.node(ns.bsfs.Tag), ast.filter.Has(ast.filter.Predicate(ns.bse.tag, reverse=True),
+ ast.filter.GreaterThan(1)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/tag#1234'})
+
+
+ def test_one_of(self):
+ # _one_of expects a node
+ self.assertRaises(errors.BackendError, self.parser._one_of,
+ self.schema.literal(ns.bsfs.Literal),
+ ast.filter.OneOf(ast.filter.Predicate(ns.bse.filesize)))
+ # invalid predicate for node type raises an error
+ self.assertRaises(errors.ConsistencyError, self.parser._one_of,
+ self.schema.node(ns.bsfs.Node),
+ ast.filter.OneOf(ast.filter.Predicate(ns.bse.filesize)))
+ self.assertRaises(errors.ConsistencyError, self.parser,
+ self.schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ast.filter.OneOf(ast.filter.Predicate(ns.bse.filesize)), ast.filter.Equals(1234)))
+ self.assertRaises(errors.BackendError, self.parser._one_of,
+ self.schema.node(ns.bsfs.Node),
+ ast.filter.OneOf(ast.filter.Predicate(ns.bsfs.Predicate)))
+ # invalid predicate combinations raise an error
+ self.assertRaises(errors.ConsistencyError, self.parser._one_of,
+ self.schema.node(ns.bsfs.Node),
+ ast.filter.OneOf(
+ ast.filter.Predicate(ns.bse.filesize),
+ ast.filter.Predicate(ns.bse.representative)))
+ # _one_of returns the URI and range
+ q = self.parser._one_of(self.schema.node(ns.bsfs.Image),
+ ast.filter.OneOf(
+ ast.filter.Predicate(ns.bse.iso),
+ ast.filter.Predicate(ns.bse.filesize)))
+ self.assertTrue(q[0] == f'<{ns.bse.iso}>|<{ns.bse.filesize}>' or q[0] == f'<{ns.bse.filesize}>|<{ns.bse.iso}>')
+ self.assertEqual(q[1], self.schema.literal(ns.xsd.integer))
+ # OneOf can be nested
+ q = self.parser._one_of(self.schema.node(ns.bsfs.Image),
+ ast.filter.OneOf(
+ ast.filter.Predicate(ns.bse.iso),
+ ast.filter.OneOf(
+ ast.filter.Predicate(ns.bse.filesize))))
+ self.assertTrue(q[0] == f'<{ns.bse.iso}>|<{ns.bse.filesize}>' or q[0] == f'<{ns.bse.filesize}>|<{ns.bse.iso}>')
+ self.assertEqual(q[1], self.schema.literal(ns.xsd.integer))
+ # _one_of returns the most generic range
+ q = self.parser._one_of(self.schema.node(ns.bsfs.Entity),
+ ast.filter.OneOf(
+ ast.filter.Predicate(ns.bse.tag),
+ ast.filter.Predicate(ns.bse.buddy)))
+ self.assertTrue(q[0] == f'<{ns.bse.tag}>|<{ns.bse.buddy}>' or q[0] == f'<{ns.bse.buddy}>|<{ns.bse.tag}>')
+ self.assertEqual(q[1], self.schema.node(ns.bsfs.Node))
+ # domains must match the given type
+ self.assertRaises(errors.ConsistencyError, self.parser,
+ self.schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy),
+ ast.filter.Any(ast.filter.OneOf(ns.bse.filesize),
+ ast.filter.Equals(1234))))
+ # ranges must have the same type (Node/Literal)
+ self.assertRaises(errors.ConsistencyError, self.parser,
+ self.schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ast.filter.OneOf(ns.bse.tag, ns.bse.filesize),
+ ast.filter.Equals(1234)))
+ # ranges must be related
+ self.assertRaises(errors.ConsistencyError, self.parser,
+ self.schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ast.filter.OneOf(ns.bse.comment, ns.bse.filesize),
+ ast.filter.Equals(1234)))
+ # integration: _one_of returns a valid sparql query
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy),
+ ast.filter.Any(ast.filter.OneOf(ns.bse.comment),
+ ast.filter.Equals('Me, Myself, and I'))))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+
+
+ def test_predicate(self):
+ # predicate cannot be the root predicate (ns.bsfs.Predicate)
+ self.assertRaises(errors.BackendError, self.parser._predicate, self.schema.node(ns.bsfs.Node), ast.filter.Predicate(ns.bsfs.Predicate))
+ # _predicate expects a node
+ self.assertRaises(errors.BackendError, self.parser._predicate,
+ self.schema.literal(ns.bsfs.Literal),
+ ast.filter.Predicate(ns.bse.filesize))
+ # invalid predicate for node type raises an error
+ self.assertRaises(errors.ConsistencyError, self.parser._predicate,
+ self.schema.node(ns.bsfs.Node),
+ ast.filter.Predicate(ns.bse.filesize))
+ self.assertRaises(errors.ConsistencyError, self.parser,
+ self.schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ast.filter.Predicate(ns.bse.filesize), ast.filter.Equals(1234)))
+ # _predicate returns the URI and range
+ self.assertEqual(self.parser._predicate(self.schema.node(ns.bsfs.Entity), ast.filter.Predicate(ns.bse.filesize)),
+ (f'<{ns.bse.filesize}>', self.schema.literal(ns.xsd.integer)))
+ self.assertEqual(self.parser._predicate(self.schema.node(ns.bsfs.Entity), ast.filter.Predicate(ns.bse.tag)),
+ (f'<{ns.bse.tag}>', self.schema.node(ns.bsfs.Tag)))
+ # _predicate respects reverse flag
+ self.assertEqual(self.parser._predicate(self.schema.node(ns.bsfs.Tag), ast.filter.Predicate(ns.bse.tag, reverse=True)),
+ ('^<' + ns.bse.tag + '>', self.schema.node(ns.bsfs.Entity)))
+ # integration: _predicate returns a valid sparql query
+ q = self.parser(self.schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ns.bse.tag,
+ ast.filter.Any(ns.bse.representative,
+ ast.filter.Any(ns.bse.filesize,
+ ast.filter.Equals(1234)))))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/image#1234'})
+ q = self.parser(self.schema.node(ns.bsfs.Tag),
+ ast.filter.Any(ast.filter.Predicate(ns.bse.tag, reverse=True),
+ ast.filter.Any(ns.bse.filesize,
+ ast.filter.LessThan(2000))))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/tag#1234'})
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/triple_store/sparql/test_sparql.py b/test/triple_store/sparql/test_sparql.py
index 0bf664a..3d81de1 100644
--- a/test/triple_store/sparql/test_sparql.py
+++ b/test/triple_store/sparql/test_sparql.py
@@ -11,6 +11,7 @@ import unittest
# bsie imports
from bsfs import schema as _schema
from bsfs.namespace import ns
+from bsfs.query import ast
from bsfs.utils import errors, URI
# objects to test
@@ -59,6 +60,18 @@ class TestSparqlStore(unittest.TestCase):
bsfs:unique "true"^^xsd:boolean .
''')
+ self.schema_triples = {
+ # schema hierarchy
+ (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.xsd.string), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bse.comment), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.author), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ }
def test_essentials(self):
store = SparqlStore.Open()
@@ -155,7 +168,7 @@ class TestSparqlStore(unittest.TestCase):
store.set(curr.node(ns.bsfs.Entity), ent_ids, p_author,
{URI('http://example.com/me')})
# check instances
- instances = {
+ instances = self.schema_triples | {
# node instances
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
@@ -228,7 +241,16 @@ class TestSparqlStore(unittest.TestCase):
store.schema = curr
self.assertEqual(store.schema, curr)
# instances have not changed
- self.assertSetEqual(set(store._graph), instances)
+ self.assertSetEqual(set(store._graph), instances | {
+ # schema hierarchy
+ (rdflib.URIRef(ns.bsfs.Collection), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.xsd.boolean), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bse.shared), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.partOf), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef('http://bsfs.ai/schema/Tag#usedIn'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef('http://bsfs.ai/schema/Collection#tag'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef('http://bsfs.ai/schema/Tag#principal'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ })
# add some instances of the new classes
p_partOf = curr.predicate(ns.bse.partOf)
p_shared = curr.predicate(ns.bse.shared)
@@ -248,6 +270,14 @@ class TestSparqlStore(unittest.TestCase):
{URI('http://example.com/me/collection#1234')})
# new instances are now in the graph
self.assertSetEqual(set(store._graph), instances | {
+ # same old schema hierarchy
+ (rdflib.URIRef(ns.bsfs.Collection), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.xsd.boolean), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bse.shared), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.partOf), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef('http://bsfs.ai/schema/Tag#usedIn'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef('http://bsfs.ai/schema/Collection#tag'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef('http://bsfs.ai/schema/Tag#principal'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
# collections
(rdflib.URIRef('http://example.com/me/collection#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Collection)),
(rdflib.URIRef('http://example.com/me/collection#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Collection)),
@@ -316,6 +346,16 @@ class TestSparqlStore(unittest.TestCase):
self.assertEqual(store.schema, curr)
# instances of old classes were removed
self.assertSetEqual(set(store._graph), {
+ # schema hierarchy
+ (rdflib.URIRef(ns.bsfs.Entity), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
+ (rdflib.URIRef(ns.xsd.boolean), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bse.shared), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
+ (rdflib.URIRef('http://bsfs.ai/schema/Tag#principal'), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
# node instances
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
@@ -390,7 +430,7 @@ class TestSparqlStore(unittest.TestCase):
ent_ids = {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}
tag_ids = {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')}
# target instances
- instances = {
+ instances = self.schema_triples | {
# node instances
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
@@ -416,7 +456,7 @@ class TestSparqlStore(unittest.TestCase):
# rollback undoes previous changes
store.rollback()
- self.assertSetEqual(set(store._graph), set())
+ self.assertSetEqual(set(store._graph), self.schema_triples)
# add some data once more
store.create(ent_type, ent_ids)
@@ -456,7 +496,38 @@ class TestSparqlStore(unittest.TestCase):
})
def test_get(self):
- raise NotImplementedError()
+ # store setup
+ store = SparqlStore.Open()
+ store.schema = self.schema
+ ent_type = self.schema.node(ns.bsfs.Entity)
+ tag_type = self.schema.node(ns.bsfs.Tag)
+ ent_ids = {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}
+ tag_ids = {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')}
+ store.create(ent_type, ent_ids)
+ store.create(tag_type, tag_ids)
+ store.set(ent_type, ent_ids, self.schema.predicate(ns.bse.tag), tag_ids)
+ store.set(ent_type, {URI('http://example.com/me/entity#1234')}, self.schema.predicate(ns.bse.filesize), {1234})
+ store.set(ent_type, {URI('http://example.com/me/entity#4321')}, self.schema.predicate(ns.bse.filesize), {4321})
+ # node_type must be in the schema
+ self.assertRaises(errors.ConsistencyError, set, store.get(self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), ast.filter.IsIn(ent_ids)))
+ # query must be a filter expression
+ class Foo(): pass
+ self.assertRaises(TypeError, set, store.get(ent_type, 1234))
+ self.assertRaises(TypeError, set, store.get(ent_type, '1234'))
+ self.assertRaises(TypeError, set, store.get(ent_type, Foo()))
+ # run some queries
+ self.assertSetEqual(set(store.get(tag_type, ast.filter.IsIn(tag_ids))), tag_ids)
+ self.assertSetEqual(set(store.get(ent_type, ast.filter.Any(ns.bse.tag, ast.filter.IsIn(tag_ids)))), ent_ids)
+ self.assertSetEqual(set(store.get(ent_type, ast.filter.IsIn(tag_ids))), set())
+ # invalid queries raise error
+ self.assertRaises(errors.ConsistencyError, set, store.get(tag_type, ast.filter.Any(ns.bse.filesize, ast.filter.Equals(1234))))
+ self.assertRaises(errors.BackendError, set, store.get(ent_type, ast.filter.Equals('http://example.com/me/entity#1234')))
+ # run some more complex query
+ q = store.get(tag_type, ast.filter.Any(ast.filter.Predicate(ns.bse.tag, reverse=True),
+ ast.filter.Any(ns.bse.filesize,
+ ast.filter.LessThan(2000))))
+ self.assertSetEqual(set(q), tag_ids)
+
def test_exists(self):
# store setup
@@ -509,14 +580,15 @@ class TestSparqlStore(unittest.TestCase):
# can create some nodes
ent_type = store.schema.node(ns.bsfs.Entity)
store.create(ent_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')})
- self.assertSetEqual(set(store._graph), {
+ self.assertSetEqual(set(store._graph), self.schema_triples | {
+ # instances
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
})
# existing nodes are skipped
store.create(ent_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#5678')})
- self.assertSetEqual(set(store._graph), {
+ self.assertSetEqual(set(store._graph), self.schema_triples | {
# previous triples
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
@@ -527,7 +599,7 @@ class TestSparqlStore(unittest.TestCase):
# can create nodes of a different type
tag_type = store.schema.node(ns.bsfs.Tag)
store.create(tag_type, {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')})
- self.assertSetEqual(set(store._graph), {
+ self.assertSetEqual(set(store._graph), self.schema_triples | {
# previous triples
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
@@ -540,7 +612,7 @@ class TestSparqlStore(unittest.TestCase):
# creation does not change types of existing nodes
tag_type = store.schema.node(ns.bsfs.Tag)
store.create(tag_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')})
- self.assertSetEqual(set(store._graph), {
+ self.assertSetEqual(set(store._graph), self.schema_triples | {
# previous triples
(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
(rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),