From b67db534dd6cad97fc012b70c7a3e9fb1ae11708 Mon Sep 17 00:00:00 2001 From: Marcel Otto Date: Sat, 11 Dec 2021 22:34:17 +0100 Subject: [PATCH] Add RDF-star support on the BGP query engine RDF.Query.BGP.Stream --- lib/rdf/query/bgp/helper.ex | 143 +++++++++++++ lib/rdf/query/bgp/simple.ex | 137 +----------- lib/rdf/query/bgp/stream.ex | 166 +++++++-------- test/unit/query/bgp/stream_star_test.exs | 260 +++++++++++++++++++++++ 4 files changed, 484 insertions(+), 222 deletions(-) create mode 100644 lib/rdf/query/bgp/helper.ex create mode 100644 test/unit/query/bgp/stream_star_test.exs diff --git a/lib/rdf/query/bgp/helper.ex b/lib/rdf/query/bgp/helper.ex new file mode 100644 index 0000000..6dc3a20 --- /dev/null +++ b/lib/rdf/query/bgp/helper.ex @@ -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 diff --git a/lib/rdf/query/bgp/simple.ex b/lib/rdf/query/bgp/simple.ex index f2db41b..2f2a1c0 100644 --- a/lib/rdf/query/bgp/simple.ex +++ b/lib/rdf/query/bgp/simple.ex @@ -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 diff --git a/lib/rdf/query/bgp/stream.ex b/lib/rdf/query/bgp/stream.ex index 6ede1e2..96cc18e 100644 --- a/lib/rdf/query/bgp/stream.ex +++ b/lib/rdf/query/bgp/stream.ex @@ -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 diff --git a/test/unit/query/bgp/stream_star_test.exs b/test/unit/query/bgp/stream_star_test.exs new file mode 100644 index 0000000..cd6182e --- /dev/null +++ b/test/unit/query/bgp/stream_star_test.exs @@ -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