RDF graph query API

This commit is contained in:
Marcel Otto 2020-06-12 03:23:20 +02:00
parent 8d68e925ad
commit 7d3473abd0
13 changed files with 343 additions and 246 deletions

View file

@ -16,11 +16,10 @@ alias RDF.Query.BGP
test_graph = RDF.Turtle.read_file!("test/data/TURTLE-TESTS/manifest.ttl", base: "http://www.w3.org/2013/TurtleTests/")
all_query = %BGP{triple_patterns: [{:s, :p, :o}]}
Benchee.run(%{
"take 1 from BGP.Simple" => fn -> BGP.Simple.query_stream(test_graph, all_query) |> Enum.take(1) end,
"take 1 from BGP.Stream" => fn -> BGP.Stream.query_stream(test_graph, all_query) |> Enum.take(1) end,
"take 1 from BGP.Simple" => fn -> BGP.Simple.stream(all_query, test_graph) |> Enum.take(1) end,
"take 1 from BGP.Stream" => fn -> BGP.Stream.stream(all_query, test_graph) |> Enum.take(1) end,
})
@ -30,21 +29,19 @@ approved_query = %BGP{triple_patterns: [
{:test_case, MF.name, :name},
{:test_case, RDFS.comment, :comment},
]}
# rdft:approval rdft:Proposed - count: 4
proposed_query = %BGP{triple_patterns: [
{:test_case, RDFT.approval, RDF.iri(RDFT.Proposed)},
{:test_case, MF.name, :name},
{:test_case, RDFS.comment, :comment},
]}
Benchee.run(%{
"APPROVED from BGP.Simple" => fn -> BGP.Simple.query(test_graph, approved_query) end,
"PROPOSED from BGP.Simple" => fn -> BGP.Simple.query(test_graph, proposed_query) end,
"APPROVED from BGP.Simple" => fn -> BGP.Simple.execute(approved_query, test_graph) end,
"PROPOSED from BGP.Simple" => fn -> BGP.Simple.execute(proposed_query, test_graph) end,
"APPROVED from BGP.Stream (consumed)" => fn -> BGP.Stream.query(test_graph, approved_query) end,
"PROPOSED from BGP.Stream (consumed)" => fn -> BGP.Stream.query(test_graph, proposed_query) end,
"APPROVED from BGP.Stream (unconsumed)" => fn -> BGP.Stream.query_stream(test_graph, approved_query) end,
"PROPOSED from BGP.Stream (unconsumed)" => fn -> BGP.Stream.query_stream(test_graph, proposed_query) end,
"APPROVED from BGP.Stream (1 consumed)" => fn -> BGP.Stream.query_stream(test_graph, approved_query) |> Enum.take(1) end
"APPROVED from BGP.Stream (consumed)" => fn -> BGP.Stream.execute(approved_query, test_graph) end,
"PROPOSED from BGP.Stream (consumed)" => fn -> BGP.Stream.execute(proposed_query, test_graph) end,
"APPROVED from BGP.Stream (unconsumed)" => fn -> BGP.Stream.stream(approved_query, test_graph) end,
"PROPOSED from BGP.Stream (unconsumed)" => fn -> BGP.Stream.stream(proposed_query, test_graph) end,
"APPROVED from BGP.Stream (1 consumed)" => fn -> BGP.Stream.stream(approved_query, test_graph) |> Enum.take(1) end
})

View file

@ -479,6 +479,14 @@ defmodule RDF.Graph do
Access.fetch(descriptions, coerce_subject(subject))
end
def query(graph, query, opts \\ []) do
RDF.Query.execute!(query, graph, opts)
end
def query_stream(graph, query, opts \\ []) do
RDF.Query.stream!(query, graph, opts)
end
@doc """
Gets the description of the given subject.

55
lib/rdf/query.ex Normal file
View file

@ -0,0 +1,55 @@
defmodule RDF.Query do
@moduledoc """
The RDF Graph query API.
"""
alias RDF.Graph
alias RDF.Query.{BGP, Builder}
@default_matcher RDF.Query.BGP.Stream
def execute(query, graph, opts \\ [])
def execute(%BGP{} = query, %Graph{} = graph, opts) do
matcher = Keyword.get(opts, :matcher, @default_matcher)
matcher.execute(query, graph, opts)
end
def execute(query, graph, opts) when is_list(query) or is_tuple(query) do
with {:ok, bgp} <- Builder.bgp(query) do
execute(bgp, graph, opts)
end
end
def execute!(query, graph, opts) do
case execute(query, graph, opts) do
{:ok, results} -> results
{:error, error} -> raise error
end
end
def stream(query, graph, opts \\ [])
def stream(%BGP{} = query, %Graph{} = graph, opts) do
matcher = Keyword.get(opts, :matcher, @default_matcher)
matcher.stream(query, graph, opts)
end
def stream(query, graph, opts) when is_list(query) or is_tuple(query) do
with {:ok, bgp} <- Builder.bgp(query) do
stream(bgp, graph, opts)
end
end
def stream!(query, graph, opts) do
case execute(query, graph, opts) do
{:ok, results} -> results
{:error, error} -> raise error
end
end
defdelegate bgp(query), to: Builder, as: :bgp!
defdelegate path(query, opts \\ []), to: Builder, as: :path!
end

View file

@ -39,7 +39,7 @@ defmodule RDF.Query.BGP.BlankNodeHandler do
defp bnodes(triple_patterns) when is_list(triple_patterns) do
triple_patterns
|> Enum.reduce([], fn triple_pattern, vars -> bnodes(triple_pattern) ++ vars end)
|> Enum.flat_map(&bnodes/1)
|> Enum.uniq()
end

View file

@ -4,13 +4,13 @@ defmodule RDF.Query.BGP.Matcher do
"""
alias RDF.Query.BGP
alias RDF.Graph
@type solution :: map
@type solutions :: [solution]
@callback query(data :: RDF.Graph.t, bgp :: BGP.t, opts :: Keyword.t) :: solutions
@callback query_stream(data :: RDF.Graph.t, bgp :: BGP.t, opts :: Keyword.t) :: Enumerable.t()
@callback execute(BGP.t, Graph.t, opts :: Keyword.t) :: solutions
@callback stream(BGP.t, Graph.t, opts :: Keyword.t) :: Enumerable.t()
end

View file

@ -6,51 +6,51 @@ defmodule RDF.Query.BGP.Simple do
alias RDF.{Graph, Description}
@impl RDF.Query.BGP.Matcher
def query(data, pattern, opts \\ [])
def execute(bgp, graph, opts \\ [])
def query(_, %BGP{triple_patterns: []}, _), do: [%{}] # https://www.w3.org/TR/sparql11-query/#emptyGroupPattern
def execute(%BGP{triple_patterns: []}, _, _), do: [%{}] # https://www.w3.org/TR/sparql11-query/#emptyGroupPattern
def query(data, %BGP{triple_patterns: triple_patterns}, opts) do
def execute(%BGP{triple_patterns: triple_patterns}, %Graph{} = graph, opts) do
{bnode_state, preprocessed_triple_patterns} =
BlankNodeHandler.preprocess(triple_patterns)
preprocessed_triple_patterns
|> QueryPlanner.query_plan()
|> do_query(data)
|> do_execute(graph)
|> BlankNodeHandler.postprocess(triple_patterns, bnode_state, opts)
end
@impl RDF.Query.BGP.Matcher
def query_stream(data, bgp, opts \\ []) do
query(data, bgp, opts)
def stream(bgp, graph, opts \\ []) do
execute(bgp, graph, opts)
|> Stream.into([])
end
defp do_query([triple_pattern | remaining], data) do
do_query(remaining, data, match(data, triple_pattern))
defp do_execute([triple_pattern | remaining], graph) do
do_execute(remaining, graph, match(graph, triple_pattern))
end
defp do_query(triple_patterns, data, solutions)
defp do_execute(triple_patterns, graph, solutions)
defp do_query(_, _, []), do: []
defp do_execute(_, _, []), do: []
defp do_query([], _, solutions), do: solutions
defp do_execute([], _, solutions), do: solutions
defp do_query([triple_pattern | remaining], data, solutions) do
do_query(remaining, data, match_with_solutions(data, triple_pattern, solutions))
defp do_execute([triple_pattern | remaining], graph, solutions) do
do_execute(remaining, graph, match_with_solutions(graph, triple_pattern, solutions))
end
defp match_with_solutions(data, {s, p, o} = triple_pattern, existing_solutions)
defp match_with_solutions(graph, {s, p, o} = triple_pattern, existing_solutions)
when is_tuple(s) or is_tuple(p) or is_tuple(o) do
triple_pattern
|> apply_solutions(existing_solutions)
|> Enum.flat_map(&(merging_match(&1, data)))
|> Enum.flat_map(&(merging_match(&1, graph)))
end
defp match_with_solutions(data, triple_pattern, existing_solutions) do
data
defp match_with_solutions(graph, triple_pattern, existing_solutions) do
graph
|> match(triple_pattern)
|> Enum.flat_map(fn solution ->
Enum.map(existing_solutions, &(Map.merge(solution, &1)))
@ -76,8 +76,8 @@ defmodule RDF.Query.BGP.Simple do
end
end
defp merging_match({dependent_solution, triple_pattern}, data) do
case match(data, triple_pattern) do
defp merging_match({dependent_solution, triple_pattern}, graph) do
case match(graph, triple_pattern) do
nil -> []
solutions ->
Enum.map(solutions, fn solution ->
@ -143,7 +143,8 @@ defmodule RDF.Query.BGP.Simple do
{_, predicate, object_or_variable}) do
case predications[predicate] do
nil -> []
objects -> cond do
objects ->
cond do
# object_or_variable is a variable
is_atom(object_or_variable) ->
Enum.map(objects, fn {object, _} ->

View file

@ -7,54 +7,54 @@ defmodule RDF.Query.BGP.Stream do
@impl RDF.Query.BGP.Matcher
def query_stream(data, pattern, opts \\ [])
def stream(bgp, graph, opts \\ [])
def query_stream(_, %BGP{triple_patterns: []}, _), do: stream([%{}]) # https://www.w3.org/TR/sparql11-query/#emptyGroupPattern
def stream(%BGP{triple_patterns: []}, _, _), do: to_stream([%{}]) # https://www.w3.org/TR/sparql11-query/#emptyGroupPattern
def query_stream(data, %BGP{triple_patterns: triple_patterns}, opts) do
def stream(%BGP{triple_patterns: triple_patterns}, %Graph{} = graph, opts) do
{bnode_state, preprocessed_triple_patterns} =
BlankNodeHandler.preprocess(triple_patterns)
preprocessed_triple_patterns
|> QueryPlanner.query_plan()
|> do_query(data)
|> do_execute(graph)
|> BlankNodeHandler.postprocess(triple_patterns, bnode_state, opts)
end
@impl RDF.Query.BGP.Matcher
def query(data, bgp, opts \\ []) do
query_stream(data, bgp, opts)
def execute(bgp, graph, opts \\ []) do
stream(bgp, graph, opts)
|> Enum.to_list()
end
defp do_query([triple_pattern | remaining], data) do
do_query(remaining, data, match(data, triple_pattern))
defp do_execute([triple_pattern | remaining], graph) do
do_execute(remaining, graph, match(graph, triple_pattern))
end
# CAUTION: Careful with using Enum.empty?/1 on the solution stream!! The first match must be
# searched for every call in the query loop repeatedly then, which can have dramatic effects potentially.
# Only use it very close to the data (in the match/1 functions operating on data directly).
defp do_query(triple_patterns, data, solutions)
defp do_execute(triple_patterns, graph, solutions)
defp do_query(_, _, nil), do: stream([])
defp do_execute(_, _, nil), do: to_stream([])
defp do_query([], _, solutions), do: solutions
defp do_execute([], _, solutions), do: solutions
defp do_query([triple_pattern | remaining], data, solutions) do
do_query(remaining, data, match_with_solutions(data, triple_pattern, solutions))
defp do_execute([triple_pattern | remaining], graph, solutions) do
do_execute(remaining, graph, match_with_solutions(graph, triple_pattern, solutions))
end
defp match_with_solutions(data, {s, p, o} = triple_pattern, existing_solutions)
defp match_with_solutions(graph, {s, p, o} = triple_pattern, existing_solutions)
when is_tuple(s) or is_tuple(p) or is_tuple(o) do
triple_pattern
|> apply_solutions(existing_solutions)
|> Stream.flat_map(&(merging_match(&1, data)))
|> Stream.flat_map(&(merging_match(&1, graph)))
end
defp match_with_solutions(data, triple_pattern, existing_solutions) do
if solutions = match(data, triple_pattern) do
defp match_with_solutions(graph, triple_pattern, existing_solutions) do
if solutions = match(graph, triple_pattern) do
Stream.flat_map(solutions, fn solution ->
Stream.map(existing_solutions, &(Map.merge(solution, &1)))
end)
@ -80,8 +80,8 @@ defmodule RDF.Query.BGP.Stream do
end
end
defp merging_match({dependent_solution, triple_pattern}, data) do
case match(data, triple_pattern) do
defp merging_match({dependent_solution, triple_pattern}, graph) do
case match(graph, triple_pattern) do
nil -> []
solutions ->
Stream.map solutions, fn solution ->
@ -144,7 +144,8 @@ defmodule RDF.Query.BGP.Stream do
{_, predicate, object_or_variable}) do
case predications[predicate] do
nil -> nil
objects -> cond do
objects ->
cond do
# object_or_variable is a variable
is_atom(object_or_variable) ->
Stream.map(objects, fn {object, _} ->
@ -153,7 +154,7 @@ defmodule RDF.Query.BGP.Stream do
# object_or_variable is a object
Map.has_key?(objects, object_or_variable) ->
stream([%{}])
to_stream([%{}])
# else
true ->
@ -171,5 +172,5 @@ defmodule RDF.Query.BGP.Stream do
defp solve_variables(var, val, {s, p, var}), do: {s, p, val}
defp solve_variables(_, _, pattern), do: pattern
defp stream(enum), do: Stream.into(enum, [])
defp to_stream(enum), do: Stream.into(enum, [])
end

View file

@ -0,0 +1,25 @@
defmodule RDF.Query.Test.Case do
use ExUnit.CaseTemplate
using do
quote do
use RDF.Test.Case
alias RDF.Query.BGP
import unquote(__MODULE__)
end
end
alias RDF.Query.BGP
def bgp_struct(), do: %BGP{triple_patterns: []}
def bgp_struct(triple_patterns) when is_list(triple_patterns),
do: %BGP{triple_patterns: triple_patterns}
def bgp_struct({_, _, _} = triple_pattern),
do: %BGP{triple_patterns: [triple_pattern]}
def ok_bgp_struct(triple_patterns), do: {:ok, bgp_struct(triple_patterns)}
end

View file

@ -1,10 +1,9 @@
defmodule RDF.Query.BGP.QueryPlannerTest do
use RDF.Test.Case
use RDF.Query.Test.Case
alias RDF.Query.BGP.QueryPlanner
describe "query_plan/1" do
test "empty" do
assert QueryPlanner.query_plan([]) == []
end

View file

@ -1,8 +1,7 @@
defmodule RDF.Query.BGP.SimpleTest do
use RDF.Test.Case
use RDF.Query.Test.Case
alias RDF.Query.BGP
import RDF.Query.BGP.Simple, only: [query: 2]
import RDF.Query.BGP.Simple, only: [execute: 2]
@example_graph Graph.new([
{EX.s1, EX.p1, EX.o1},
@ -10,18 +9,12 @@ defmodule RDF.Query.BGP.SimpleTest do
{EX.s3, EX.p3, EX.o2}
])
defp bgp(), do: %BGP{triple_patterns: []}
defp bgp(triple_patterns) when is_list(triple_patterns),
do: %BGP{triple_patterns: triple_patterns}
defp bgp({_, _, _} = triple_pattern),
do: %BGP{triple_patterns: [triple_pattern]}
test "empty bgp" do
assert query(@example_graph, bgp()) == [%{}]
assert bgp_struct() |> execute(@example_graph) == [%{}]
end
test "single {s ?p ?o}" do
assert query(@example_graph, bgp({EX.s1, :p, :o})) ==
assert bgp_struct({EX.s1, :p, :o}) |> execute(@example_graph) ==
[
%{p: EX.p1, o: EX.o1},
%{p: EX.p2, o: EX.o2}
@ -29,7 +22,7 @@ defmodule RDF.Query.BGP.SimpleTest do
end
test "single {?s ?p o}" do
assert query(@example_graph, bgp({:s, :p, EX.o2})) ==
assert bgp_struct({:s, :p, EX.o2}) |> execute(@example_graph) ==
[
%{s: EX.s3, p: EX.p3},
%{s: EX.s1, p: EX.p2}
@ -37,12 +30,12 @@ defmodule RDF.Query.BGP.SimpleTest do
end
test "single {?s p ?o}" do
assert query(@example_graph, bgp({:s, EX.p3, :o})) ==
assert bgp_struct({:s, EX.p3, :o}) |> execute(@example_graph) ==
[%{s: EX.s3, o: EX.o2}]
end
test "with no solutions" do
assert query(Graph.new(), bgp({:a, :b, :c})) == []
assert bgp_struct({:a, :b, :c}) |> execute(Graph.new()) == []
end
test "with solutions on one triple pattern but none on another one" do
@ -51,10 +44,10 @@ defmodule RDF.Query.BGP.SimpleTest do
{EX.y, EX.y, EX.z},
])
assert query(example_graph, bgp [
assert bgp_struct([
{:a, EX.p1, ~L"unmatched" },
{:a, EX.y, EX.z}
]) == []
]) |> execute(example_graph) == []
end
test "repeated variable: {?a ?a ?b}" do
@ -64,7 +57,7 @@ defmodule RDF.Query.BGP.SimpleTest do
{EX.y, EX.x, EX.y}
])
assert query(example_graph, bgp({:a, :a, :b})) ==
assert bgp_struct({:a, :a, :b}) |> execute(example_graph) ==
[%{a: EX.y, b: EX.x}]
end
@ -75,7 +68,7 @@ defmodule RDF.Query.BGP.SimpleTest do
{EX.y, EX.x, EX.y}
])
assert query(example_graph, bgp({:a, :b, :a})) ==
assert bgp_struct({:a, :b, :a}) |> execute(example_graph) ==
[%{a: EX.y, b: EX.x}]
end
@ -86,7 +79,7 @@ defmodule RDF.Query.BGP.SimpleTest do
{EX.y, EX.x, EX.y}
])
assert query(example_graph, bgp({:b, :a, :a})) ==
assert bgp_struct({:b, :a, :a}) |> execute(example_graph) ==
[%{a: EX.y, b: EX.x}]
end
@ -98,23 +91,24 @@ defmodule RDF.Query.BGP.SimpleTest do
{EX.y, EX.y, EX.y},
])
assert query(example_graph, bgp({:a, :a, :a})) == [%{a: EX.y}]
assert bgp_struct({:a, :a, :a}) |> execute(example_graph) == [%{a: EX.y}]
end
test "two connected triple patterns with a match" do
assert query(@example_graph, bgp [
assert execute(bgp_struct([
{EX.s1, :p, :o},
{EX.s3, :p2, :o }
]) == [%{
]), @example_graph) == [%{
p: EX.p2,
p2: EX.p3,
o: EX.o2
}]
assert query(@example_graph, bgp [
assert bgp_struct([
{EX.s1, :p, :o1},
{EX.s1, :p, :o2}
]) ==
])
|> execute(@example_graph) ==
[
%{
p: EX.p1,
@ -128,31 +122,28 @@ defmodule RDF.Query.BGP.SimpleTest do
},
]
assert query(
Graph.new([
assert bgp_struct([
{EX.s1, EX.p1, :o},
{EX.s3, :p, :o}
])
|> execute(Graph.new([
{EX.s1, EX.p1, EX.o1},
{EX.s3, EX.p2, EX.o2},
{EX.s3, EX.p3, EX.o1}
]),
bgp [
{EX.s1, EX.p1, :o},
{EX.s3, :p, :o}
]) == [%{p: EX.p3, o: EX.o1}]
])) == [%{p: EX.p3, o: EX.o1}]
end
test "a triple pattern with dependent variables from separate triple patterns" do
assert query(
Graph.new([
{EX.s1, EX.p1, EX.o1},
{EX.s2, EX.p2, EX.o2},
{EX.s3, EX.p2, EX.o1}
]),
bgp [
assert bgp_struct([
{EX.s1, EX.p1, :o},
{EX.s2, :p, EX.o2},
{:s, :p, :o}
]
) == [
])
|> execute(Graph.new([
{EX.s1, EX.p1, EX.o1},
{EX.s2, EX.p2, EX.o2},
{EX.s3, EX.p2, EX.o1}
])) == [
%{
s: EX.s3,
p: EX.p2,
@ -162,21 +153,22 @@ defmodule RDF.Query.BGP.SimpleTest do
end
test "when no solutions" do
assert query(@example_graph, bgp({EX.s, EX.p, :o})) == []
assert bgp_struct({EX.s, EX.p, :o}) |> execute(@example_graph) == []
end
test "multiple triple patterns with a constant unmatched triple has no solutions" do
assert query(@example_graph, bgp [
assert bgp_struct([
{EX.s1, :p, :o},
{EX.s, EX.p, EX.o}
]) == []
]) |> execute(@example_graph) == []
end
test "independent triple patterns lead to cross-products" do
assert query(@example_graph, bgp [
assert bgp_struct([
{EX.s1, :p1, :o},
{:s, :p2, EX.o2}
]) == [
])
|> execute(@example_graph) == [
%{
p1: EX.p1,
o: EX.o1,
@ -205,17 +197,19 @@ defmodule RDF.Query.BGP.SimpleTest do
end
test "blank nodes behave like variables, but don't appear in the solution" do
assert query(@example_graph, bgp [
assert bgp_struct([
{EX.s1, :p, RDF.bnode("o")},
{EX.s3, :p2, RDF.bnode("o")}
]) == [%{p: EX.p2, p2: EX.p3}]
])
|> execute(@example_graph) == [%{p: EX.p2, p2: EX.p3}]
end
test "cross-product with blank nodes" do
assert query(@example_graph, bgp [
assert bgp_struct([
{EX.s1, :p1, :o},
{RDF.bnode("s"), :p2, EX.o2}
]) ==
])
|> execute(@example_graph) ==
[
%{
p1: EX.p1,

View file

@ -1,8 +1,7 @@
defmodule RDF.Query.BGP.StreamTest do
use RDF.Test.Case
use RDF.Query.Test.Case
alias RDF.Query.BGP
import RDF.Query.BGP.Stream, only: [query: 2]
import RDF.Query.BGP.Stream, only: [execute: 2]
@example_graph Graph.new([
{EX.s1, EX.p1, EX.o1},
@ -10,19 +9,13 @@ defmodule RDF.Query.BGP.StreamTest do
{EX.s3, EX.p3, EX.o2}
])
defp bgp(), do: %BGP{triple_patterns: []}
defp bgp(triple_patterns) when is_list(triple_patterns),
do: %BGP{triple_patterns: triple_patterns}
defp bgp({_, _, _} = triple_pattern),
do: %BGP{triple_patterns: [triple_pattern]}
test "empty bgp" do
assert query(@example_graph, bgp()) == [%{}]
assert bgp_struct() |> execute(@example_graph) == [%{}]
end
test "single {s ?p ?o}" do
assert query(@example_graph, bgp({EX.s1, :p, :o})) ==
assert bgp_struct({EX.s1, :p, :o}) |> execute(@example_graph) ==
[
%{p: EX.p1, o: EX.o1},
%{p: EX.p2, o: EX.o2}
@ -30,20 +23,20 @@ defmodule RDF.Query.BGP.StreamTest do
end
test "single {?s ?p o}" do
assert query(@example_graph, bgp({:s, :p, EX.o2})) ==
assert bgp_struct({:s, :p, EX.o2}) |> execute(@example_graph) ==
[
%{s: EX.s1, p: EX.p2},
%{s: EX.s3, p: EX.p3},
%{s: EX.s3, p: EX.p3}
]
end
test "single {?s p ?o}" do
assert query(@example_graph, bgp({:s, EX.p3, :o})) ==
assert bgp_struct({:s, EX.p3, :o}) |> execute(@example_graph) ==
[%{s: EX.s3, o: EX.o2}]
end
test "with no solutions" do
assert query(Graph.new(), bgp({:a, :b, :c})) == []
assert bgp_struct({:a, :b, :c}) |> execute(Graph.new()) == []
end
test "with solutions on one triple pattern but none on another one" do
@ -52,10 +45,10 @@ defmodule RDF.Query.BGP.StreamTest do
{EX.y, EX.y, EX.z},
])
assert query(example_graph, bgp [
assert bgp_struct([
{:a, EX.p1, ~L"unmatched" },
{:a, EX.y, EX.z}
]) == []
]) |> execute(example_graph) == []
end
test "repeated variable: {?a ?a ?b}" do
@ -65,7 +58,7 @@ defmodule RDF.Query.BGP.StreamTest do
{EX.y, EX.x, EX.y}
])
assert query(example_graph, bgp({:a, :a, :b})) ==
assert bgp_struct({:a, :a, :b}) |> execute(example_graph) ==
[%{a: EX.y, b: EX.x}]
end
@ -76,7 +69,7 @@ defmodule RDF.Query.BGP.StreamTest do
{EX.y, EX.x, EX.y}
])
assert query(example_graph, bgp({:a, :b, :a})) ==
assert bgp_struct({:a, :b, :a}) |> execute(example_graph) ==
[%{a: EX.y, b: EX.x}]
end
@ -87,7 +80,7 @@ defmodule RDF.Query.BGP.StreamTest do
{EX.y, EX.x, EX.y}
])
assert query(example_graph, bgp({:b, :a, :a})) ==
assert bgp_struct({:b, :a, :a}) |> execute(example_graph) ==
[%{a: EX.y, b: EX.x}]
end
@ -99,23 +92,24 @@ defmodule RDF.Query.BGP.StreamTest do
{EX.y, EX.y, EX.y},
])
assert query(example_graph, bgp({:a, :a, :a})) == [%{a: EX.y}]
assert bgp_struct({:a, :a, :a}) |> execute(example_graph) == [%{a: EX.y}]
end
test "two connected triple patterns with a match" do
assert query(@example_graph, bgp [
assert execute(bgp_struct([
{EX.s1, :p, :o},
{EX.s3, :p2, :o }
]) == [%{
]), @example_graph) == [%{
p: EX.p2,
p2: EX.p3,
o: EX.o2
}]
assert query(@example_graph, bgp [
assert bgp_struct([
{EX.s1, :p, :o1},
{EX.s1, :p, :o2}
]) ==
])
|> execute(@example_graph) ==
[
%{
p: EX.p1,
@ -129,31 +123,28 @@ defmodule RDF.Query.BGP.StreamTest do
},
]
assert query(
Graph.new([
assert bgp_struct([
{EX.s1, EX.p1, :o},
{EX.s3, :p, :o}
])
|> execute(Graph.new([
{EX.s1, EX.p1, EX.o1},
{EX.s3, EX.p2, EX.o2},
{EX.s3, EX.p3, EX.o1}
]),
bgp [
{EX.s1, EX.p1, :o},
{EX.s3, :p, :o}
]) == [%{p: EX.p3, o: EX.o1}]
])) == [%{p: EX.p3, o: EX.o1}]
end
test "a triple pattern with dependent variables from separate triple patterns" do
assert query(
Graph.new([
{EX.s1, EX.p1, EX.o1},
{EX.s2, EX.p2, EX.o2},
{EX.s3, EX.p2, EX.o1}
]),
bgp [
assert bgp_struct([
{EX.s1, EX.p1, :o},
{EX.s2, :p, EX.o2},
{:s, :p, :o}
]
) == [
])
|> execute(Graph.new([
{EX.s1, EX.p1, EX.o1},
{EX.s2, EX.p2, EX.o2},
{EX.s3, EX.p2, EX.o1}
])) == [
%{
s: EX.s3,
p: EX.p2,
@ -163,23 +154,23 @@ defmodule RDF.Query.BGP.StreamTest do
end
test "when no solutions" do
assert query(@example_graph, bgp({EX.s, EX.p, :o})) == []
assert bgp_struct({EX.s, EX.p, :o}) |> execute(@example_graph) == []
end
test "multiple triple patterns with a constant unmatched triple has no solutions" do
assert query(@example_graph, bgp [
assert bgp_struct([
{EX.s1, :p, :o},
{EX.s, EX.p, EX.o}
]) == []
]) |> execute(@example_graph) == []
end
test "independent triple patterns lead to cross-products" do
assert MapSet.new(
query(@example_graph, bgp [
assert bgp_struct([
{EX.s1, :p1, :o},
{:s, :p2, EX.o2}
])
) == MapSet.new([
|> execute(@example_graph)
|> MapSet.new() == MapSet.new([
%{
p1: EX.p1,
o: EX.o1,
@ -208,20 +199,21 @@ defmodule RDF.Query.BGP.StreamTest do
end
test "blank nodes behave like variables, but don't appear in the solution" do
assert query(@example_graph, bgp [
assert bgp_struct([
{EX.s1, :p, RDF.bnode("o")},
{EX.s3, :p2, RDF.bnode("o")}
]) == [%{p: EX.p2, p2: EX.p3}]
])
|> execute(@example_graph) == [%{p: EX.p2, p2: EX.p3}]
end
test "cross-product with blank nodes" do
assert MapSet.new(
query(@example_graph, bgp [
assert bgp_struct([
{EX.s1, :p1, :o},
{RDF.bnode("s"), :p2, EX.o2}
])
) ==
MapSet.new([
|> execute(@example_graph)
|> MapSet.new() == MapSet.new(
[
%{
p1: EX.p1,
o: EX.o1,
@ -242,6 +234,7 @@ defmodule RDF.Query.BGP.StreamTest do
o: EX.o2,
p2: EX.p2,
},
])
]
)
end
end

View file

@ -1,37 +1,34 @@
defmodule RDF.Query.BuilderTest do
use RDF.Test.Case
use RDF.Query.Test.Case
alias RDF.Query.{BGP, Builder}
defp bgp(triple_patterns) when is_list(triple_patterns),
do: {:ok, %BGP{triple_patterns: triple_patterns}}
alias RDF.Query.Builder
describe "new/1" do
test "empty triple pattern" do
assert Builder.bgp([]) == bgp([])
assert Builder.bgp([]) == ok_bgp_struct([])
end
test "one triple pattern doesn't require list brackets" do
assert Builder.bgp({EX.s, EX.p, EX.o}) ==
bgp [{EX.s, EX.p, EX.o}]
ok_bgp_struct [{EX.s, EX.p, EX.o}]
end
test "variables" do
assert Builder.bgp([{:s?, :p?, :o?}]) == bgp [{:s, :p, :o}]
assert Builder.bgp([{:s?, :p?, :o?}]) == ok_bgp_struct [{:s, :p, :o}]
end
test "blank nodes" do
assert Builder.bgp([{RDF.bnode("s"), RDF.bnode("p"), RDF.bnode("o")}]) ==
bgp [{RDF.bnode("s"), RDF.bnode("p"), RDF.bnode("o")}]
ok_bgp_struct [{RDF.bnode("s"), RDF.bnode("p"), RDF.bnode("o")}]
end
test "blank nodes as atoms" do
assert Builder.bgp([{:_s, :_p, :_o}]) ==
bgp [{RDF.bnode("s"), RDF.bnode("p"), RDF.bnode("o")}]
ok_bgp_struct [{RDF.bnode("s"), RDF.bnode("p"), RDF.bnode("o")}]
end
test "variable notation has precedence over blank node notation" do
assert Builder.bgp([{:_s?, :_p?, :_o?}]) == bgp [{:_s, :_p, :_o}]
assert Builder.bgp([{:_s?, :_p?, :_o?}]) == ok_bgp_struct [{:_s, :_p, :_o}]
end
test "IRIs" do
@ -39,26 +36,26 @@ defmodule RDF.Query.BuilderTest do
RDF.iri("http://example.com/s"),
RDF.iri("http://example.com/p"),
RDF.iri("http://example.com/o")}]
) == bgp [{EX.s, EX.p, EX.o}]
) == ok_bgp_struct [{EX.s, EX.p, EX.o}]
assert Builder.bgp([{
~I<http://example.com/s>,
~I<http://example.com/p>,
~I<http://example.com/o>}]
) == bgp [{EX.s, EX.p, EX.o}]
) == ok_bgp_struct [{EX.s, EX.p, EX.o}]
assert Builder.bgp([{EX.s, EX.p, EX.o}]) ==
bgp [{EX.s, EX.p, EX.o}]
ok_bgp_struct [{EX.s, EX.p, EX.o}]
end
test "vocabulary term atoms" do
assert Builder.bgp([{EX.S, EX.P, EX.O}]) ==
bgp [{RDF.iri(EX.S), RDF.iri(EX.P), RDF.iri(EX.O)}]
ok_bgp_struct [{RDF.iri(EX.S), RDF.iri(EX.P), RDF.iri(EX.O)}]
end
test "special :a atom for rdf:type" do
assert Builder.bgp([{EX.S, :a, EX.O}]) ==
bgp [{RDF.iri(EX.S), RDF.type, RDF.iri(EX.O)}]
ok_bgp_struct [{RDF.iri(EX.S), RDF.type, RDF.iri(EX.O)}]
end
test "URIs" do
@ -66,21 +63,21 @@ defmodule RDF.Query.BuilderTest do
URI.parse("http://example.com/s"),
URI.parse("http://example.com/p"),
URI.parse("http://example.com/o")}]
) == bgp [{EX.s, EX.p, EX.o}]
) == ok_bgp_struct [{EX.s, EX.p, EX.o}]
end
test "literals" do
assert Builder.bgp([{EX.s, EX.p, ~L"foo"}]) ==
bgp [{EX.s, EX.p, ~L"foo"}]
ok_bgp_struct [{EX.s, EX.p, ~L"foo"}]
end
test "values coercible to literals" do
assert Builder.bgp([{EX.s, EX.p, "foo"}]) ==
bgp [{EX.s, EX.p, ~L"foo"}]
ok_bgp_struct [{EX.s, EX.p, ~L"foo"}]
assert Builder.bgp([{EX.s, EX.p, 42}]) ==
bgp [{EX.s, EX.p, RDF.literal(42)}]
ok_bgp_struct [{EX.s, EX.p, RDF.literal(42)}]
assert Builder.bgp([{EX.s, EX.p, true}]) ==
bgp [{EX.s, EX.p, XSD.true}]
ok_bgp_struct [{EX.s, EX.p, XSD.true}]
end
test "literals on non-object positions" do
@ -93,7 +90,7 @@ defmodule RDF.Query.BuilderTest do
{EX.S, EX.p, :o?},
{:o?, EX.p2, 42}
]) ==
bgp [
ok_bgp_struct [
{RDF.iri(EX.S), EX.p, :o},
{:o, EX.p2, RDF.literal(42)}
]
@ -101,19 +98,19 @@ defmodule RDF.Query.BuilderTest do
test "multiple objects to the same subject-predicate" do
assert Builder.bgp([{EX.s, EX.p, EX.o1, EX.o2}]) ==
bgp [
ok_bgp_struct [
{EX.s, EX.p, EX.o1},
{EX.s, EX.p, EX.o2}
]
assert Builder.bgp({EX.s, EX.p, EX.o1, EX.o2}) ==
bgp [
ok_bgp_struct [
{EX.s, EX.p, EX.o1},
{EX.s, EX.p, EX.o2}
]
assert Builder.bgp({EX.s, EX.p, :o?, false, 42, "foo"}) ==
bgp [
ok_bgp_struct [
{EX.s, EX.p, :o},
{EX.s, EX.p, XSD.false},
{EX.s, EX.p, RDF.literal(42)},
@ -127,7 +124,7 @@ defmodule RDF.Query.BuilderTest do
[EX.p1, EX.o1],
[EX.p2, EX.o2],
}]) ==
bgp [
ok_bgp_struct [
{EX.s, EX.p1, EX.o1},
{EX.s, EX.p2, EX.o2}
]
@ -138,7 +135,7 @@ defmodule RDF.Query.BuilderTest do
[EX.p1, 42, 3.14],
[EX.p2, "foo", true],
}]) ==
bgp [
ok_bgp_struct [
{EX.s, RDF.type, :o},
{EX.s, EX.p1, RDF.literal(42)},
{EX.s, EX.p1, RDF.literal(3.14)},
@ -147,26 +144,26 @@ defmodule RDF.Query.BuilderTest do
]
assert Builder.bgp([{EX.s, [EX.p, EX.o]}]) ==
bgp [{EX.s, EX.p, EX.o}]
ok_bgp_struct [{EX.s, EX.p, EX.o}]
end
end
describe "path/2" do
test "element count == 3" do
assert Builder.path([EX.s, EX.p, EX.o]) == bgp [{EX.s, EX.p, EX.o}]
assert Builder.path([:s?, :p?, :o?]) == bgp [{:s, :p, :o}]
assert Builder.path([EX.s, EX.p, EX.o]) == ok_bgp_struct [{EX.s, EX.p, EX.o}]
assert Builder.path([:s?, :p?, :o?]) == ok_bgp_struct [{:s, :p, :o}]
end
test "element count > 3" do
assert Builder.path([EX.s, EX.p1, EX.p2, EX.o]) ==
bgp [
ok_bgp_struct [
{EX.s, EX.p1, RDF.bnode("0")},
{RDF.bnode("0"), EX.p2, EX.o},
]
assert Builder.path([:s?, :p1?, :p2?, :o?]) ==
bgp [
ok_bgp_struct [
{:s, :p1, RDF.bnode("0")},
{RDF.bnode("0"), :p2, :o},
]
@ -180,7 +177,7 @@ defmodule RDF.Query.BuilderTest do
test "with_elements: true" do
assert Builder.path([EX.s, EX.p1, EX.p2, :o?], with_elements: true) ==
bgp [
ok_bgp_struct [
{EX.s, EX.p1, :el0},
{:el0, EX.p2, :o},
]

View file

@ -0,0 +1,27 @@
defmodule RDF.QueryTest do
use RDF.Query.Test.Case
@example_graph Graph.new([
{EX.s1, EX.p1, EX.o1},
{EX.s1, EX.p2, EX.o2},
{EX.s3, EX.p3, EX.o2}
])
@example_query [{:s?, :p?, EX.o2}]
test "execute/2" do
assert RDF.Query.execute(RDF.Query.bgp(@example_query), @example_graph) ==
BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph)
assert RDF.Query.execute(@example_query, @example_graph) ==
BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph)
end
test "stream/2" do
assert RDF.Query.stream(RDF.Query.bgp(@example_query), @example_graph) ==
BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph)
assert RDF.Query.stream(@example_query, @example_graph) ==
BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph)
end
end