ld-patch 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ module LD::Patch::Algebra
2
+
3
+ ##
4
+ # The LD Patch `constraint` operator.
5
+ #
6
+ # A constraint is a query operator which either ensures that there is a single input node ("!" operator) or finds a set of nodes for a given `path`, optionally filtering those nodes with a particular predicate value.
7
+ #
8
+ # @example existence of path solutions
9
+ # (constraint (path :p))
10
+ #
11
+ # Maps input terms to output terms using `(path :p)` returning those input terms that have at least a single solution.
12
+ #
13
+ # @example paths with property value
14
+ # (constraint (path :p) 1)
15
+ #
16
+ # Maps input terms to output terms using `(path :p)` and filters the input terms where the output term is `1`.
17
+ #
18
+ # @example unique terms
19
+ #
20
+ # (constraint unique)
21
+ #
22
+ # Returns the single term from the input terms if there is a single input term.
23
+ class Constraint < SPARQL::Algebra::Operator
24
+ include SPARQL::Algebra::Query
25
+ include SPARQL::Algebra::Evaluatable
26
+
27
+ NAME = :constraint
28
+
29
+ ##
30
+ # If the first operand is :unique
31
+ #
32
+ # @param [RDF::Queryable] queryable
33
+ # the graph or repository to write
34
+ # @param [Hash{Symbol => Object}] options
35
+ # any additional options
36
+ # @option options [Array<RDF::Term>] starting terms
37
+ # @return [RDF::Query::Solutions] solutions with `:term` mapping
38
+ def execute(queryable, options = {})
39
+ debug(options) {"Constraint"}
40
+ terms = Array(options.fetch(:terms))
41
+ op, value = operands
42
+
43
+ results = if op == :unique
44
+ terms.length == 1 ? terms : []
45
+ else
46
+ # op is a path, filter input terms based on the presense or absense of output terms. Additionally, if a constraint value is given, output terms must equal that value
47
+ terms.select do |term|
48
+ output_terms = op.execute(queryable, options.merge(terms: [term])).map(&:path)
49
+ output_terms = output_terms.select {|t| t == value} if value
50
+ !output_terms.empty?
51
+ end
52
+ end
53
+ RDF::Query::Solutions.new(results.map {|t| RDF::Query::Solution.new(path: t)})
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ module LD::Patch::Algebra
2
+
3
+ ##
4
+ # The LD Patch `cut` operator.
5
+ #
6
+ # The Cut operation is recursively remove triples from some starting node.
7
+ #
8
+ # @example
9
+ # (cut ?a)
10
+ #
11
+ class Cut < SPARQL::Algebra::Operator::Unary
12
+ include SPARQL::Algebra::Update
13
+ include SPARQL::Algebra::Evaluatable
14
+
15
+ NAME = :cut
16
+
17
+ ##
18
+ # Executes this upate on the given `writable` graph or repository.
19
+ #
20
+ # @param [RDF::Queryable] queryable
21
+ # the graph or repository to write
22
+ # @param [Hash{Symbol => Object}] options
23
+ # any additional options
24
+ # @return [RDF::Query::Solutions] A single solution including passed bindings with `var` bound to the solution.
25
+ # @raise [IOError]
26
+ # If no triples are identified, or the operand is an unbound variable or the operand is an unbound variable.
27
+ # @see http://www.w3.org/TR/sparql11-update/
28
+ def execute(queryable, options = {})
29
+ debug(options) {"Cut"}
30
+ bindings = options.fetch(:bindings)
31
+ solution = bindings.first
32
+ var = operand(0)
33
+
34
+ # Bind variable
35
+ raise LD::Patch::Error.new("Operand uses unbound variable #{var.inspect}", code: 400) unless solution.bound?(var)
36
+ var = solution[var]
37
+
38
+ cut_count = 0
39
+ # Get triples to delete using consice bounded description
40
+ queryable.concise_bounded_description(var) do |statement|
41
+ queryable.delete(statement)
42
+ cut_count += 1
43
+ end
44
+
45
+ # Also delete triples having var in the object position
46
+ queryable.query(object: var).each do |statement|
47
+ queryable.delete(statement)
48
+ cut_count += 1
49
+ end
50
+
51
+ raise LD::Patch::Error, "Cut removed no triples" unless cut_count > 0
52
+
53
+ bindings
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,59 @@
1
+ module LD::Patch::Algebra
2
+
3
+ ##
4
+ # The LD Patch `delete` operator (incuding `deleteExisting`).
5
+ #
6
+ # The Add operation is used to delete triples from the target graph with or without checking to see if the exist already.
7
+ #
8
+ # @example
9
+ # (add ((<a> <b> <c>)))
10
+ #
11
+ class Delete < SPARQL::Algebra::Operator::Unary
12
+ include SPARQL::Algebra::Update
13
+ include SPARQL::Algebra::Evaluatable
14
+
15
+ NAME = :delete
16
+
17
+ ##
18
+ # Executes this upate on the given `writable` graph or repository.
19
+ #
20
+ # @param [RDF::Queryable] queryable
21
+ # the graph or repository to write
22
+ # @param [Hash{Symbol => Object}] options
23
+ # any additional options
24
+ # @option options [Boolean] :existing
25
+ # Specifies that triples must already exist in the target graph
26
+ # @return [RDF::Query::Solutions] A single solution including passed bindings with `var` bound to the solution.
27
+ # @raise [Error]
28
+ # If `existing` is specified, and any triple is not found in the traget graph, or if unbound variables are used.
29
+ # @see http://www.w3.org/TR/sparql11-update/
30
+ def execute(queryable, options = {})
31
+ debug(options) {"Delete"}
32
+ bindings = options.fetch(:bindings)
33
+ solution = bindings.first
34
+
35
+ # Bind variables to triples
36
+ triples = operand(0).dup.replace_vars! do |var|
37
+ case var
38
+ when RDF::Query::Pattern
39
+ s = var.bind(solution)
40
+ raise LD::Patch::Error.new("Operand uses unbound pattern #{var.inspect}", code: 400) if s.variable?
41
+ s
42
+ when RDF::Query::Variable
43
+ raise LD::Patch::Error.new("Operand uses unbound variable #{var.inspect}", code: 400) unless solution.bound?(var)
44
+ solution[var]
45
+ end
46
+ end
47
+
48
+ # If `:new` is specified, verify that no triple in triples exists in queryable
49
+ if @options[:existing]
50
+ triples.each do |triple|
51
+ raise LD::Patch::Error, "Target graph does not contain triple #{triple.to_ntriples}" unless queryable.has_statement?(triple)
52
+ end
53
+ end
54
+
55
+ queryable.delete(*triples)
56
+ bindings
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,34 @@
1
+ module LD::Patch::Algebra
2
+
3
+ ##
4
+ # The LD Patch `index` operator.
5
+ #
6
+ # Presuming that the input term identifies an rdf:List, returns the list element indexted by the single operand, or an empty solution set
7
+ class Index < SPARQL::Algebra::Operator::Unary
8
+ include SPARQL::Algebra::Query
9
+
10
+ NAME = :index
11
+
12
+ ##
13
+ # Executes this upate on the given `writable` graph or repository.
14
+ #
15
+ # @param [RDF::Queryable] queryable
16
+ # the graph or repository to write
17
+ # @param [Hash{Symbol => Object}] options
18
+ # any additional options
19
+ # @option options [Array<RDF::Term>] starting terms
20
+ # @return [RDF::Query::Solutions] solutions with `:term` mapping
21
+ def execute(queryable, options = {})
22
+ debug(options) {"Index"}
23
+ terms = Array(options.fetch(:terms))
24
+ index = operand(0)
25
+
26
+ results = terms.map do |term|
27
+ list = RDF::List.new(term, queryable)
28
+ list.at(index.to_i)
29
+ end.flatten
30
+
31
+ RDF::Query::Solutions.new(results.map {|t| RDF::Query::Solution.new(path: t)})
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ module LD::Patch::Algebra
2
+
3
+ ##
4
+ # The LD Patch `patch` operator.
5
+ #
6
+ # Transactionally iterates over all operands building bindings along the way
7
+ class Patch < SPARQL::Algebra::Operator
8
+ include SPARQL::Algebra::Update
9
+
10
+ NAME = :patch
11
+
12
+ ##
13
+ # Executes this upate on the given `writable` graph or repository.
14
+ #
15
+ # @param [RDF::Queryable] queryable
16
+ # the graph or repository to write
17
+ # @param [Hash{Symbol => Object}] options
18
+ # any additional options
19
+ # @return [RDF::Queryable]
20
+ # Returns queryable.
21
+ # @raise [Error]
22
+ # If any error is caught along the way, and rolls back the transaction
23
+ def execute(queryable, options = {})
24
+ debug(options) {"Delete"}
25
+
26
+ # FIXME: due to insufficient transaction support, this is implemented by running through operands twice: the first using a clone of the graph, and the second acting on the graph directly
27
+ graph = RDF::Graph.new << queryable
28
+ loop do
29
+ operands.inject(RDF::Query::Solutions.new([RDF::Query::Solution.new])) do |bindings, op|
30
+ # Invoke operand using bindings from prvious operation
31
+ op.execute(graph, options.merge(bindings: bindings))
32
+ end
33
+
34
+ break if graph.equal?(queryable)
35
+ graph = queryable
36
+ end
37
+ queryable
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,71 @@
1
+ module LD::Patch::Algebra
2
+
3
+ ##
4
+ # The LD Patch `path` operator.
5
+ #
6
+ # The Path creates a closure over path operands querying `queryable` for terms having a relationship with the input `terms` based on each operand. The terms extracted from the first operand are used as inputs for the next operand until a final set of terms is found. These terms are returned as `RDF:Query::Solution` bound the the variable `?path`
7
+ #
8
+ # @example empty path
9
+ # (path)
10
+ #
11
+ # Returns input terms
12
+ #
13
+ # @example forward path
14
+ # (path :p)
15
+ #
16
+ # Queries `queryable` for objects where the input terms are subjects and the predicate is `:p`
17
+ #
18
+ # @example reverse path
19
+ # (path (reverse :p))
20
+ #
21
+ # Queries `queryable` for subjects where input terms are objects and the predicate is `:p`, by executing the `reverse` operand using input terms to get a set of output terms.
22
+ #
23
+ # @example constraint
24
+ # (path (constraint (path) :c, 1))
25
+ #
26
+ # Returns the input terms satisfying the constrant.
27
+ #
28
+ # @example chained path elements
29
+ # (path :p :q (constraint (path) :c, 1))
30
+ #
31
+ # Maps terms using `(path :p)`, using them as terms for `(path :q)`, then subsets these based on the constraint.
32
+ class Path < SPARQL::Algebra::Operator
33
+ include SPARQL::Algebra::Query
34
+ include SPARQL::Algebra::Evaluatable
35
+
36
+ NAME = :path
37
+
38
+ ##
39
+ # Executes this operator using the given variable `bindings` and a starting term, returning zero or more terms at the end of the path.
40
+ #
41
+ # @param [RDF::Queryable] queryable
42
+ # the graph or repository to query
43
+ # @param [Hash{Symbol => Object}] options ({})
44
+ # options passed from query
45
+ # @option options [Array<RDF::Term>] starting terms
46
+ # @return [RDF::Query::Solutions] solutions with `:term` mapping
47
+ def execute(queryable, options = {})
48
+ solutions = RDF::Query::Solutions.new
49
+
50
+ # Iterate updating terms, then create solutions from matched terms
51
+ operands.inject(Array(options.fetch(:terms))) do |terms, op|
52
+ case op
53
+ when RDF::URI
54
+ terms.map do |subject|
55
+ queryable.query(subject: subject, predicate: op).map(&:object)
56
+ end.flatten
57
+ when SPARQL::Algebra::Query
58
+ # Get path solutions for each term for op
59
+ op.execute(queryable, options.merge(terms: terms)).map do |soln|
60
+ soln.path
61
+ end.flatten
62
+ else
63
+ raise NotImplementedError, "Unknown path operand #{op.inspect}"
64
+ end
65
+ end.each do |term|
66
+ solutions << RDF::Query::Solution.new(path: term)
67
+ end
68
+ solutions
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,48 @@
1
+ module LD::Patch::Algebra
2
+ ##
3
+ # The LD Patch `prefix` operator.
4
+ #
5
+ # @example
6
+ # (prefix ((: <http://example/>))
7
+ # (graph ?g
8
+ # (bgp (triple ?s ?p ?o))))
9
+ #
10
+ # @see http://www.w3.org/TR/rdf-sparql-query/#QSynIRI
11
+ class Prefix < SPARQL::Algebra::Operator::Binary
12
+ include SPARQL::Algebra::Update
13
+
14
+ NAME = :prefix
15
+
16
+ ##
17
+ # Executes this query on the given `queryable` graph or repository.
18
+ # Really a pass-through, as this is a syntactic object used for providing
19
+ # context for URIs.
20
+ #
21
+ # @param [RDF::Queryable] queryable
22
+ # the graph or repository to query
23
+ # @param [Hash{Symbol => Object}] options
24
+ # any additional keyword options
25
+ # @yield [solution]
26
+ # each matching solution, statement or boolean
27
+ # @yieldparam [RDF::Statement, RDF::Query::Solution, Boolean] solution
28
+ # @yieldreturn [void] ignored
29
+ # @return [RDF::Query::Solutions]
30
+ # the resulting solution sequence
31
+ # @see http://www.w3.org/TR/rdf-sparql-query/#sparqlAlgebra
32
+ def execute(queryable, options = {}, &block)
33
+ debug(options) {"Prefix"}
34
+ @solutions = queryable.query(operands.last, options.merge(depth: options[:depth].to_i + 1), &block)
35
+ end
36
+
37
+ ##
38
+ # Returns an optimized version of this query.
39
+ #
40
+ # If optimize operands, and if the first two operands are both Queries, replace
41
+ # with the unique sum of the query elements
42
+ #
43
+ # @return [Union, RDF::Query] `self`
44
+ def optimize
45
+ operands.last.optimize
46
+ end
47
+ end # Prefix
48
+ end
@@ -0,0 +1,39 @@
1
+ module LD::Patch::Algebra
2
+
3
+ ##
4
+ # The LD Patch `reverse` operator
5
+ #
6
+ # Finds all the terms which are the subject of triples where the `operand` is the predicate and input terms are objects.
7
+ #
8
+ # @example
9
+ # (reverse :p)
10
+ #
11
+ # Queries `queryable` for subjects where input terms are objects and the predicate is `:p`, by executing the `reverse` operand using input terms to get a set of output terms.
12
+ class Reverse < SPARQL::Algebra::Operator::Unary
13
+ include SPARQL::Algebra::Query
14
+ include SPARQL::Algebra::Evaluatable
15
+
16
+ NAME = :reverse
17
+
18
+ ##
19
+ # Executes this upate on the given `writable` graph or repository.
20
+ #
21
+ # @param [RDF::Queryable] queryable
22
+ # the graph or repository to write
23
+ # @param [Hash{Symbol => Object}] options
24
+ # any additional options
25
+ # @option options [Array<RDF::Term>] starting terms
26
+ # @return [RDF::Query::Solutions] solutions with `:term` mapping
27
+ def execute(queryable, options = {})
28
+ debug(options) {"Reverse"}
29
+ op = operand(0)
30
+ terms = Array(options.fetch(:terms))
31
+
32
+ results = terms.map do |object|
33
+ queryable.query(object: object, predicate: op).map(&:subject)
34
+ end.flatten
35
+
36
+ RDF::Query::Solutions.new(results.map {|t| RDF::Query::Solution.new(path: t)})
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,77 @@
1
+ module LD::Patch::Algebra
2
+
3
+ ##
4
+ # The LD Patch `updateList` operator.
5
+ #
6
+ # The UpdateList operation is used to splice a new list into a subset of an existing list.
7
+ #
8
+ class UpdateList < SPARQL::Algebra::Operator
9
+ include SPARQL::Algebra::Update
10
+ include SPARQL::Algebra::Evaluatable
11
+
12
+ NAME = :updateList
13
+
14
+ ##
15
+ # Executes this upate on the given `writable` graph or repository.
16
+ #
17
+ # @param [RDF::Queryable] queryable
18
+ # the graph or repository to write
19
+ # @param [Hash{Symbol => Object}] options
20
+ # any additional options
21
+ # @return [RDF::Query::Solutions] A single solution including passed bindings with `var` bound to the solution.
22
+ # @raise [Error]
23
+ # If the subject and predicate provided to an UpdateList do not have a unique object, or if this object is not a well-formed collection.
24
+ # If an index in a slice expression is greater than the length of the rdf:List or otherwise out of bound.
25
+ # @see http://www.w3.org/TR/sparql11-update/
26
+ def execute(queryable, options = {})
27
+ debug(options) {"UpdateList"}
28
+ bindings = options.fetch(:bindings)
29
+ solution = bindings.first
30
+ var_or_iri, predicate, slice1, slice2, collection = operands
31
+
32
+ # Bind variables to path
33
+ if var_or_iri.variable?
34
+ raise LD::Patch::Error("Operand uses unbound variable #{var_or_iri.inspect}", code: 400) unless solution.bound?(var_or_iri)
35
+ var_or_iri = solution[variable]
36
+ end
37
+
38
+ list_heads = queryable.query(subject: var_or_iri, predicate: predicate).map {|s| s.object}
39
+
40
+ raise LD::Patch::Error, "UpdateList ambigious value for #{var_or_iri.to_ntriples} and #{predicate.to_ntriples}" if list_heads.length > 1
41
+ raise LD::Patch::Error, "UpdateList no value found for #{var_or_iri.to_ntriples} and #{predicate.to_ntriples}" if list_heads.empty?
42
+ lh = list_heads.first
43
+ list = RDF::List.new(lh, queryable)
44
+ raise LD::Patch::Error, "Invalid list" unless list.valid?
45
+
46
+ start = case
47
+ when slice1.nil? || slice1 == RDF.nil then list.length
48
+ when slice1 < 0 then list.length + slice1.to_i
49
+ else slice1.to_i
50
+ end
51
+
52
+ finish = case
53
+ when slice2.nil? || slice2 == RDF.nil then list.length
54
+ when slice2 < 0 then list.length + slice2.to_i
55
+ else slice2.to_i
56
+ end
57
+
58
+ raise LD::Patch::Error.new("UpdateList slice indexes out of order #{start}..#{finish}}", code: 400) if finish < start
59
+
60
+ length = finish - start
61
+ raise LD::Patch::Error, "UpdateList out of bounds #{start}..#{finish}}" if start + length > list.length
62
+ raise LD::Patch::Error, "UpdateList out of bounds #{start}..#{finish}}" if start < 0
63
+
64
+ # Uses #[]= logic in RDF::List
65
+ list[start, length] = collection
66
+ new_lh = list.subject
67
+
68
+ # If lh was rdf:nil, then we may have a new list head. Similarly, if the list was emptied, we now need to replace the head
69
+ if lh != new_lh
70
+ queryable.delete(RDF::Statement(var_or_iri, predicate, lh))
71
+ queryable.insert(RDF::Statement(var_or_iri, predicate, new_lh))
72
+ end
73
+
74
+ bindings
75
+ end
76
+ end
77
+ end