Add RDF-star support on the BGP query engine RDF.Query.BGP.Stream

This commit is contained in:
Marcel Otto 2021-12-11 22:34:17 +01:00
parent 711d1a4fd3
commit b67db534dd
4 changed files with 484 additions and 222 deletions

143
lib/rdf/query/bgp/helper.ex Normal file
View file

@ -0,0 +1,143 @@
defmodule RDF.Query.BGP.Helper do
@moduledoc !"""
Shared functions between the `RDF.Query.BGP.Simple` and `RDF.Query.BGP.Stream` engines.
"""
import RDF.Guards
def solvable?(term) when is_tuple(term) and tuple_size(term) == 1, do: true
def solvable?({s, p, o}), do: solvable?(p) or solvable?(s) or solvable?(o)
def solvable?(_), do: false
def apply_solutions(triple_pattern, solutions) do
if solver = solver(triple_pattern) do
Stream.map(solutions, solver)
else
solutions
end
end
defp solver(triple_pattern) do
if solver = solver_fun(triple_pattern) do
&{&1, solver.(&1)}
end
end
defp solver_fun({{s}, {p}, {o}}), do: &{&1[s], &1[p], &1[o]}
defp solver_fun({{s}, p, {o}}), do: &{&1[s], p, &1[o]}
defp solver_fun({{s}, {p}, o}) do
if o_solver = solver_fun(o) do
&{&1[s], &1[p], o_solver.(&1)}
else
&{&1[s], &1[p], o}
end
end
defp solver_fun({{s}, p, o}) do
if o_solver = solver_fun(o) do
&{&1[s], p, o_solver.(&1)}
else
&{&1[s], p, o}
end
end
defp solver_fun({s, {p}, {o}}) do
if s_solver = solver_fun(s) do
&{s_solver.(&1), &1[p], &1[o]}
else
&{s, &1[p], &1[o]}
end
end
defp solver_fun({s, p, {o}}) do
if s_solver = solver_fun(s) do
&{s_solver.(&1), p, &1[o]}
else
&{s, p, &1[o]}
end
end
defp solver_fun({s, {p}, o}) do
s_solver = solver_fun(s)
o_solver = solver_fun(o)
cond do
s_solver && o_solver -> &{s_solver.(&1), &1[p], o_solver.(&1)}
s_solver -> &{s_solver.(&1), &1[p], o}
o_solver -> &{s, &1[p], o_solver.(&1)}
true -> &{s, &1[p], o}
end
end
defp solver_fun({s, p, o}) do
s_solver = solver_fun(s)
o_solver = solver_fun(o)
cond do
s_solver && o_solver -> &{s_solver.(&1), p, o_solver.(&1)}
s_solver -> &{s_solver.(&1), p, o}
o_solver -> &{s, p, o_solver.(&1)}
true -> fn _ -> {s, p, o} end
end
end
defp solver_fun(_), do: nil
def solve_variables(var, val, {s, p, o}),
do: {solve_variables(var, val, s), solve_variables(var, val, p), solve_variables(var, val, o)}
def solve_variables(var, val, var), do: val
def solve_variables(_, _, term), do: term
def solve_variables(bindings, pattern) do
Enum.reduce(bindings, pattern, fn {var, val}, pattern ->
solve_variables(var, val, pattern)
end)
end
def quoted_triple_with_variables?({s, p, o}) do
is_atom(s) or is_atom(p) or is_atom(o) or
quoted_triple_with_variables?(s) or
quoted_triple_with_variables?(p) or
quoted_triple_with_variables?(o)
end
def quoted_triple_with_variables?(_), do: false
def match_triple(triple, triple), do: %{}
def match_triple({s, p, o}, {var, p, o}) when is_atom(var), do: %{var => s}
def match_triple({s, p, o}, {s, var, o}) when is_atom(var), do: %{var => p}
def match_triple({s, p, o}, {s, p, var}) when is_atom(var), do: %{var => o}
def match_triple({s, p1, o1}, {triple_pattern, p2, o2}) when is_triple(triple_pattern) do
if bindings = match_triple({"solved", p1, o1}, {"solved", p2, o2}) do
if nested_bindings = match_triple(s, triple_pattern) do
Map.merge(bindings, nested_bindings)
end
end
end
def match_triple({s1, p1, o}, {s2, p2, triple_pattern}) when is_triple(triple_pattern) do
if bindings = match_triple({s1, p1, "solved"}, {s2, p2, "solved"}) do
if nested_bindings = match_triple(o, triple_pattern) do
Map.merge(bindings, nested_bindings)
end
end
end
def match_triple({s, p, o}, {var1, var2, o}) when is_atom(var1) and is_atom(var2),
do: %{var1 => s, var2 => p}
def match_triple({s, p, o}, {var1, p, var2}) when is_atom(var1) and is_atom(var2),
do: %{var1 => s, var2 => o}
def match_triple({s, p, o}, {s, var1, var2}) when is_atom(var1) and is_atom(var2),
do: %{var1 => p, var2 => o}
def match_triple({s, p, o}, {var1, var2, var3})
when is_atom(var1) and is_atom(var2) and is_atom(var3),
do: %{var1 => s, var2 => p, var3 => o}
def match_triple(_, _), do: nil
end

View file

@ -8,6 +8,7 @@ defmodule RDF.Query.BGP.Simple do
alias RDF.{Graph, Description}
import RDF.Guards
import RDF.Query.BGP.Helper
@impl RDF.Query.BGP.Matcher
def execute(bgp, graph, opts \\ [])
@ -58,81 +59,6 @@ defmodule RDF.Query.BGP.Simple do
end
end
defp apply_solutions(triple_pattern, solutions) do
if solver = solver(triple_pattern) do
Stream.map(solutions, solver)
else
solutions
end
end
defp solver(triple_pattern) do
if solver = solver_fun(triple_pattern) do
&{&1, solver.(&1)}
end
end
defp solver_fun({{s}, {p}, {o}}), do: &{&1[s], &1[p], &1[o]}
defp solver_fun({{s}, p, {o}}), do: &{&1[s], p, &1[o]}
defp solver_fun({{s}, {p}, o}) do
if o_solver = solver_fun(o) do
&{&1[s], &1[p], o_solver.(&1)}
else
&{&1[s], &1[p], o}
end
end
defp solver_fun({{s}, p, o}) do
if o_solver = solver_fun(o) do
&{&1[s], p, o_solver.(&1)}
else
&{&1[s], p, o}
end
end
defp solver_fun({s, {p}, {o}}) do
if s_solver = solver_fun(s) do
&{s_solver.(&1), &1[p], &1[o]}
else
&{s, &1[p], &1[o]}
end
end
defp solver_fun({s, p, {o}}) do
if s_solver = solver_fun(s) do
&{s_solver.(&1), p, &1[o]}
else
&{s, p, &1[o]}
end
end
defp solver_fun({s, {p}, o}) do
s_solver = solver_fun(s)
o_solver = solver_fun(o)
cond do
s_solver && o_solver -> &{s_solver.(&1), &1[p], o_solver.(&1)}
s_solver -> &{s_solver.(&1), &1[p], o}
o_solver -> &{s, &1[p], o_solver.(&1)}
true -> &{s, &1[p], o}
end
end
defp solver_fun({s, p, o}) do
s_solver = solver_fun(s)
o_solver = solver_fun(o)
cond do
s_solver && o_solver -> &{s_solver.(&1), p, o_solver.(&1)}
s_solver -> &{s_solver.(&1), p, o}
o_solver -> &{s, p, o_solver.(&1)}
true -> fn _ -> {s, p, o} end
end
end
defp solver_fun(_), do: nil
defp merging_match({dependent_solution, triple_pattern}, graph) do
case match(graph, triple_pattern) do
nil ->
@ -262,65 +188,4 @@ defmodule RDF.Query.BGP.Simple do
acc
end)
end
defp match_triple(triple, triple), do: %{}
defp match_triple({s, p, o}, {var, p, o}) when is_atom(var), do: %{var => s}
defp match_triple({s, p, o}, {s, var, o}) when is_atom(var), do: %{var => p}
defp match_triple({s, p, o}, {s, p, var}) when is_atom(var), do: %{var => o}
defp match_triple({s, p1, o1}, {triple_pattern, p2, o2}) when is_triple(triple_pattern) do
if bindings = match_triple({"solved", p1, o1}, {"solved", p2, o2}) do
if nested_bindings = match_triple(s, triple_pattern) do
Map.merge(bindings, nested_bindings)
end
end
end
defp match_triple({s1, p1, o}, {s2, p2, triple_pattern}) when is_triple(triple_pattern) do
if bindings = match_triple({s1, p1, "solved"}, {s2, p2, "solved"}) do
if nested_bindings = match_triple(o, triple_pattern) do
Map.merge(bindings, nested_bindings)
end
end
end
defp match_triple({s, p, o}, {var1, var2, o}) when is_atom(var1) and is_atom(var2),
do: %{var1 => s, var2 => p}
defp match_triple({s, p, o}, {var1, p, var2}) when is_atom(var1) and is_atom(var2),
do: %{var1 => s, var2 => o}
defp match_triple({s, p, o}, {s, var1, var2}) when is_atom(var1) and is_atom(var2),
do: %{var1 => p, var2 => o}
defp match_triple({s, p, o}, {var1, var2, var3})
when is_atom(var1) and is_atom(var2) and is_atom(var3),
do: %{var1 => s, var2 => p, var3 => o}
defp match_triple(_, _), do: nil
defp solvable?(term) when is_tuple(term) and tuple_size(term) == 1, do: true
defp solvable?({s, p, o}), do: solvable?(p) or solvable?(s) or solvable?(o)
defp solvable?(_), do: false
defp quoted_triple_with_variables?({s, p, o}) do
is_atom(s) or is_atom(p) or is_atom(o) or
quoted_triple_with_variables?(s) or
quoted_triple_with_variables?(p) or
quoted_triple_with_variables?(o)
end
defp quoted_triple_with_variables?(_), do: false
defp solve_variables(var, val, {s, p, o}),
do: {solve_variables(var, val, s), solve_variables(var, val, p), solve_variables(var, val, o)}
defp solve_variables(var, val, var), do: val
defp solve_variables(_, _, term), do: term
defp solve_variables(bindings, pattern) do
Enum.reduce(bindings, pattern, fn {var, val}, pattern ->
solve_variables(var, val, pattern)
end)
end
end

View file

@ -7,6 +7,9 @@ defmodule RDF.Query.BGP.Stream do
alias RDF.Query.BGP.{QueryPlanner, BlankNodeHandler}
alias RDF.{Graph, Description}
import RDF.Query.BGP.Helper
import RDF.Guards
@impl RDF.Query.BGP.Matcher
def stream(bgp, graph, opts \\ [])
@ -32,10 +35,6 @@ defmodule RDF.Query.BGP.Stream 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_execute(triple_patterns, graph, solutions)
defp do_execute(_, _, nil), do: to_stream([])
@ -46,50 +45,24 @@ defmodule RDF.Query.BGP.Stream do
do_execute(remaining, graph, match_with_solutions(graph, triple_pattern, solutions))
end
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, graph))
end
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)
end
end
defp apply_solutions(triple_pattern, solutions) do
apply_solution =
case triple_pattern do
{{s}, {p}, {o}} -> fn solution -> {solution, {solution[s], solution[p], solution[o]}} end
{{s}, {p}, o} -> fn solution -> {solution, {solution[s], solution[p], o}} end
{{s}, p, {o}} -> fn solution -> {solution, {solution[s], p, solution[o]}} end
{{s}, p, o} -> fn solution -> {solution, {solution[s], p, o}} end
{s, {p}, {o}} -> fn solution -> {solution, {s, solution[p], solution[o]}} end
{s, {p}, o} -> fn solution -> {solution, {s, solution[p], o}} end
{s, p, {o}} -> fn solution -> {solution, {s, p, solution[o]}} end
_ -> nil
end
if apply_solution do
Stream.map(solutions, apply_solution)
defp match_with_solutions(graph, {s, p, o} = triple_pattern, existing_solutions) do
if solvable?(p) or solvable?(s) or solvable?(o) do
triple_pattern
|> apply_solutions(existing_solutions)
|> Stream.flat_map(&merging_match(&1, graph))
else
solutions
if solutions = match(graph, triple_pattern) do
Stream.flat_map(solutions, fn solution ->
Stream.map(existing_solutions, &Map.merge(solution, &1))
end)
end
end
end
defp merging_match({dependent_solution, triple_pattern}, graph) do
case match(graph, triple_pattern) do
nil ->
[]
solutions ->
Stream.map(solutions, fn solution ->
Map.merge(dependent_solution, solution)
end)
nil -> []
solutions -> Stream.map(solutions, &Map.merge(dependent_solution, &1))
end
end
@ -97,32 +70,39 @@ defmodule RDF.Query.BGP.Stream do
when is_atom(subject_variable) do
Stream.flat_map(descriptions, fn {subject, description} ->
case match(description, solve_variables(subject_variable, subject, triple_pattern)) do
nil ->
[]
solutions ->
Stream.map(solutions, fn solution ->
Map.put(solution, subject_variable, subject)
end)
nil -> []
solutions -> Stream.map(solutions, &Map.put(&1, subject_variable, subject))
end
end)
end
defp match(%Graph{} = graph, {subject, _, _} = triple_pattern) do
case graph[subject] do
nil -> nil
description -> match(description, triple_pattern)
if quoted_triple_with_variables?(subject) do
graph
|> matching_subject_triples(subject)
|> Stream.flat_map(fn {description, subject_solutions} ->
case match(description, solve_variables(subject_solutions, triple_pattern)) do
nil -> []
solutions -> Stream.map(solutions, &Map.merge(&1, subject_solutions))
end
end)
else
case graph[subject] do
nil -> nil
description -> match(description, triple_pattern)
end
end
end
defp match(%Description{predications: predications}, {_, variable, variable})
when is_atom(variable) do
matches =
Stream.filter(predications, fn {predicate, objects} -> Map.has_key?(objects, predicate) end)
unless Enum.empty?(matches) do
Stream.map(matches, fn {predicate, _} -> %{variable => predicate} end)
end
Stream.flat_map(predications, fn {predicate, objects} ->
if Map.has_key?(objects, predicate) do
[%{variable => predicate}]
else
[]
end
end)
end
defp match(%Description{predications: predications}, {_, predicate_variable, object_variable})
@ -134,33 +114,35 @@ defmodule RDF.Query.BGP.Stream do
end)
end
defp match(
%Description{predications: predications},
{_, predicate_variable, object}
)
defp match(%Description{predications: predications}, {_, predicate_variable, object})
when is_atom(predicate_variable) do
matches = Stream.filter(predications, fn {_, objects} -> Map.has_key?(objects, object) end)
unless Enum.empty?(matches) do
Stream.map(matches, fn {predicate, _} -> %{predicate_variable => predicate} end)
if quoted_triple_with_variables?(object) do
Stream.flat_map(predications, fn {predicate, objects} ->
objects
|> matching_object_triples(object)
|> Stream.map(&Map.put(&1, predicate_variable, predicate))
end)
else
Stream.flat_map(predications, fn {predicate, objects} ->
if Map.has_key?(objects, object) do
[%{predicate_variable => predicate}]
else
[]
end
end)
end
end
defp match(
%Description{predications: predications},
{_, predicate, object_or_variable}
) do
case predications[predicate] do
nil ->
nil
objects ->
defp match(%Description{predications: predications}, {_, predicate, object_or_variable}) do
if objects = predications[predicate] do
if quoted_triple_with_variables?(object_or_variable) do
matching_object_triples(objects, object_or_variable)
matching_object_triples(objects, object_or_variable)
else
cond do
# object_or_variable is a variable
is_atom(object_or_variable) ->
Stream.map(objects, fn {object, _} ->
%{object_or_variable => object}
end)
Stream.map(objects, fn {object, _} -> %{object_or_variable => object} end)
# object_or_variable is a object
Map.has_key?(objects, object_or_variable) ->
@ -170,17 +152,29 @@ defmodule RDF.Query.BGP.Stream do
true ->
nil
end
end
end
end
defp solve_variables(var, val, {var, var, var}), do: {val, val, val}
defp solve_variables(var, val, {s, var, var}), do: {s, val, val}
defp solve_variables(var, val, {var, p, var}), do: {val, p, val}
defp solve_variables(var, val, {var, var, o}), do: {val, val, o}
defp solve_variables(var, val, {var, p, o}), do: {val, p, o}
defp solve_variables(var, val, {s, var, o}), do: {s, val, o}
defp solve_variables(var, val, {s, p, var}), do: {s, p, val}
defp solve_variables(_, _, pattern), do: pattern
defp matching_subject_triples(graph, triple_pattern) do
Stream.flat_map(graph.descriptions, fn
{subject, description} when is_triple(subject) ->
case match_triple(subject, triple_pattern) do
nil -> []
solutions -> [{description, solutions}]
end
_ ->
[]
end)
end
defp matching_object_triples(objects, triple_pattern) do
Stream.flat_map(objects, fn
{object, _} when is_triple(object) -> match_triple(object, triple_pattern) |> List.wrap()
_ -> []
end)
end
defp to_stream(enum), do: Stream.into(enum, [])
end

View file

@ -0,0 +1,260 @@
defmodule RDF.Query.BGP.StreamStarTest do
use RDF.Query.Test.Case
import RDF.Query.BGP.Stream, only: [execute: 2]
@example_graph Graph.new([
{{EX.qs1(), EX.qp(), EX.qo1()}, EX.p1(), EX.o1()},
{{EX.qs1(), EX.qp(), EX.qo1()}, EX.p2(), {EX.qs2(), EX.qp(), EX.qo2()}},
{EX.s3(), EX.p3(), {EX.qs2(), EX.qp(), EX.qo2()}}
])
test "quoted triples in results" do
assert bgp_struct({{EX.qs1(), EX.qp(), EX.qo1()}, :p, :o}) |> execute(@example_graph) ==
[
%{p: EX.p1(), o: EX.o1()},
%{p: EX.p2(), o: {EX.qs2(), EX.qp(), EX.qo2()}}
]
assert bgp_struct({:s, :p, {EX.qs2(), EX.qp(), EX.qo2()}}) |> execute(@example_graph) ==
[
%{s: {EX.qs1(), EX.qp(), EX.qo1()}, p: EX.p2()},
%{s: EX.s3(), p: EX.p3()}
]
assert bgp_struct({:s, EX.p2(), :o}) |> execute(@example_graph) ==
[%{s: {EX.qs1(), EX.qp(), EX.qo1()}, o: {EX.qs2(), EX.qp(), EX.qo2()}}]
end
test "connected triple patterns with quoted triples" do
assert bgp_struct([
{EX.s(), EX.p(), :o},
{{EX.qs(), EX.qp(), EX.qo()}, :p, :o}
])
|> execute(
Graph.new([
{EX.s(), EX.p(), EX.o()},
{{EX.qs(), EX.qp(), EX.qo()}, EX.p2(), EX.o()}
])
) == [
%{
p: EX.p2(),
o: EX.o()
}
]
end
test "triple patterns connected via a shared quoted triple" do
assert bgp_struct([
{:s, EX.p1(), EX.o1()},
{:s, EX.p2(), {EX.qs2(), EX.qp(), EX.qo2()}}
])
|> execute(@example_graph) ==
[%{s: {EX.qs1(), EX.qp(), EX.qo1()}}]
end
test "variables in quoted triples on subject position" do
assert bgp_struct({{:s, EX.qp(), EX.qo1()}, EX.p1(), EX.o1()}) |> execute(@example_graph) ==
[%{s: EX.qs1()}]
assert bgp_struct({{EX.qs1(), :p, EX.qo1()}, EX.p1(), EX.o1()}) |> execute(@example_graph) ==
[%{p: EX.qp()}]
assert bgp_struct({{EX.qs1(), EX.qp(), :o}, EX.p1(), EX.o1()}) |> execute(@example_graph) ==
[%{o: EX.qo1()}]
assert bgp_struct({{:s, :p, EX.qo1()}, EX.p1(), EX.o1()}) |> execute(@example_graph) ==
[%{s: EX.qs1(), p: EX.qp()}]
assert bgp_struct({{:s, EX.qp(), :o}, EX.p1(), EX.o1()}) |> execute(@example_graph) ==
[%{s: EX.qs1(), o: EX.qo1()}]
assert bgp_struct({{EX.qs1(), :p, :o}, EX.p1(), EX.o1()}) |> execute(@example_graph) ==
[%{p: EX.qp(), o: EX.qo1()}]
assert bgp_struct({{:s, :p, :o}, EX.p1(), EX.o1()}) |> execute(@example_graph) ==
[%{s: EX.qs1(), p: EX.qp(), o: EX.qo1()}]
end
test "variables in quoted triples on object position" do
assert bgp_struct({EX.s3(), EX.p3(), {:s, EX.qp(), EX.qo2()}}) |> execute(@example_graph) ==
[%{s: EX.qs2()}]
assert bgp_struct({EX.s3(), EX.p3(), {EX.qs2(), :p, EX.qo2()}}) |> execute(@example_graph) ==
[%{p: EX.qp()}]
assert bgp_struct({EX.s3(), EX.p3(), {EX.qs2(), EX.qp(), :o}}) |> execute(@example_graph) ==
[%{o: EX.qo2()}]
assert bgp_struct({EX.s3(), EX.p3(), {:s, :p, EX.qo2()}}) |> execute(@example_graph) == [
%{s: EX.qs2(), p: EX.qp()}
]
assert bgp_struct({EX.s3(), EX.p3(), {:s, EX.qp(), :o}}) |> execute(@example_graph) == [
%{s: EX.qs2(), o: EX.qo2()}
]
assert bgp_struct({EX.s3(), EX.p3(), {EX.qs2(), :p, :o}}) |> execute(@example_graph) == [
%{p: EX.qp(), o: EX.qo2()}
]
assert bgp_struct({EX.s3(), EX.p3(), {:s, :p, :o}}) |> execute(@example_graph) == [
%{s: EX.qs2(), p: EX.qp(), o: EX.qo2()}
]
# when the outer predicate is a variable
assert bgp_struct({EX.s3(), :p, {:qs, EX.qp(), EX.qo2()}}) |> execute(@example_graph) ==
[%{qs: EX.qs2(), p: EX.p3()}]
assert bgp_struct({EX.s3(), :p, {EX.qs2(), :qp, :qo}}) |> execute(@example_graph) == [
%{qp: EX.qp(), qo: EX.qo2(), p: EX.p3()}
]
end
test "variables in quoted triples on subject and object position" do
assert bgp_struct({{:s1, EX.qp(), EX.qo1()}, EX.p2(), {:s2, EX.qp(), EX.qo2()}})
|> execute(@example_graph) ==
[%{s1: EX.qs1(), s2: EX.qs2()}]
assert bgp_struct({{:s1, :p, EX.qo1()}, EX.p2(), {:s2, :p, EX.qo2()}})
|> execute(@example_graph) ==
[%{s1: EX.qs1(), s2: EX.qs2(), p: EX.qp()}]
assert bgp_struct({{:s1, :p, :o1}, EX.p2(), {:s2, :p, :o2}})
|> execute(@example_graph) ==
[%{s1: EX.qs1(), o1: EX.qo1(), s2: EX.qs2(), o2: EX.qo2(), p: EX.qp()}]
assert bgp_struct({{:s, EX.qp(), EX.qo1()}, EX.p2(), {:s, EX.qp(), EX.qo2()}})
|> execute(@example_graph) ==
[]
assert bgp_struct({{:s1, :p, EX.qo1()}, :p, {:s2, EX.qp(), EX.qo2()}})
|> execute(@example_graph) ==
[]
end
test "triple patterns with interdependent variables" do
assert bgp_struct([
{{:qs1, :qp, EX.qo1()}, EX.p1(), EX.o1()},
{:s, :p, {:qs2, :qp, EX.qo2()}}
])
|> execute(@example_graph) ==
[
%{
qs1: EX.qs1(),
qs2: EX.qs2(),
qp: EX.qp(),
s: {EX.qs1(), EX.qp(), EX.qo1()},
p: EX.p2()
},
%{qs1: EX.qs1(), qs2: EX.qs2(), qp: EX.qp(), s: EX.s3(), p: EX.p3()}
]
assert bgp_struct([
{{:qs1, :qp, EX.qo1()}, :p, :o},
{EX.s3(), EX.p3(), {:qs2, :qp, EX.qo2()}}
])
|> execute(@example_graph) ==
[
%{qs1: EX.qs1(), qs2: EX.qs2(), qp: EX.qp(), o: EX.o1(), p: EX.p1()},
%{
qs1: EX.qs1(),
qs2: EX.qs2(),
qp: EX.qp(),
o: {EX.qs2(), EX.qp(), EX.qo2()},
p: EX.p2()
}
]
assert bgp_struct([
{{EX.qs1(), EX.qp(), :qo1}, EX.p1(), :o},
{{EX.qs1(), EX.qp(), :qo1}, EX.p2(), {:qs2, EX.qp(), EX.qo2()}},
{:s, EX.p3(), {:qs2, EX.qp(), EX.qo2()}}
])
|> execute(@example_graph) ==
[
%{
s: EX.s3(),
o: EX.o1(),
qs2: EX.qs2(),
qo1: EX.qo1()
}
]
assert bgp_struct([
{:qt, EX.p1(), :o},
{:qt, EX.p2(), {:qs2, EX.qp(), EX.qo2()}},
{:s, EX.p3(), {:qs2, EX.qp(), EX.qo2()}}
])
|> execute(@example_graph) ==
[
%{
s: EX.s3(),
o: EX.o1(),
qs2: EX.qs2(),
qt: {EX.qs1(), EX.qp(), EX.qo1()}
}
]
assert bgp_struct([
{:qt, EX.p1(), :o},
{:qt, EX.p2(), {:qs2, EX.qp(), EX.qo2()}},
{:s, EX.p3(), {:qs2, EX.qp(), EX.qo2()}},
{
{{:qs3, EX.b(), ~B"c"}, EX.d(), :qo3},
EX.f(),
{{{EX.g(), EX.h(), {:s, EX.j(), ~L"k"}}, EX.m(), EX.n()}, EX.o(), EX.p()}
}
])
|> execute(
Graph.add(@example_graph, {
{{EX.a(), EX.b(), ~B"c"}, EX.d(), EX.e()},
EX.f(),
{{{EX.g(), EX.h(), {EX.s3(), EX.j(), ~L"k"}}, EX.m(), EX.n()}, EX.o(), EX.p()}
})
) ==
[
%{
s: EX.s3(),
o: EX.o1(),
qs2: EX.qs2(),
qs3: EX.a(),
qo3: EX.e(),
qt: {EX.qs1(), EX.qp(), EX.qo1()}
}
]
{
{{:a, :b, ~B"c"}, :d, :e},
:f,
{{{:g, :h, {EX.s3(), :j, ~L"k"}}, :m, :n}, :o, :p}
}
end
test "blank nodes in quoted triple patterns" do
assert bgp_struct({{:s, EX.qp(), ~B"o"}, EX.p1(), EX.o1()})
|> execute(@example_graph) ==
[%{s: EX.qs1()}]
assert bgp_struct({:s, EX.p3(), {~B"s", ~B"p", ~B"o"}})
|> execute(@example_graph) ==
[%{s: EX.s3()}]
assert bgp_struct([
{~B"s", EX.p3(), ~B"quoted triple"},
{
{{EX.a(), EX.b(), ~B"c"}, EX.d(), EX.e()},
EX.f(),
{{{EX.g(), EX.h(), {~B"s", EX.j(), ~L"k"}}, EX.m(), :n}, EX.o(), EX.p()}
}
])
|> execute(
Graph.add(@example_graph, {
{{EX.a(), EX.b(), ~B"c"}, EX.d(), EX.e()},
EX.f(),
{{{EX.g(), EX.h(), {EX.s3(), EX.j(), ~L"k"}}, EX.m(), EX.n()}, EX.o(), EX.p()}
})
) ==
[%{n: EX.n()}]
end
end