ld-patch 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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