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

This commit is contained in:
Marcel Otto 2021-12-10 02:01:55 +01:00
parent 110c305eeb
commit 20934ef0ec
12 changed files with 1052 additions and 191 deletions

View file

@ -9,11 +9,13 @@ defmodule RDF.Query.BGP do
@enforce_keys [:triple_patterns]
defstruct [:triple_patterns]
alias RDF.Star.Statement
@type variable :: String.t()
@type triple_pattern :: {
subject :: variable | RDF.Term.t(),
predicate :: variable | RDF.Term.t(),
object :: variable | RDF.Term.t()
subject :: variable | Statement.subject(),
predicate :: variable | Statement.predicate(),
object :: variable | Statement.object()
}
@type triple_patterns :: list(triple_pattern)
@ -33,6 +35,12 @@ defmodule RDF.Query.BGP do
|> Enum.uniq()
end
def variables({quoted, p, o}) when is_tuple(quoted),
do: variables(quoted) ++ variables({"", p, o})
def variables({s, p, quoted}) when is_tuple(quoted),
do: variables(quoted) ++ variables({s, p, ""})
def variables({s, p, o}) when is_atom(s) and is_atom(p) and is_atom(o), do: [s, p, o]
def variables({s, p, _}) when is_atom(s) and is_atom(p), do: [s, p]
def variables({s, _, o}) when is_atom(s) and is_atom(o), do: [s, o]

View file

@ -1,7 +1,6 @@
defmodule RDF.Query.BGP.BlankNodeHandler do
@moduledoc false
alias RDF.Query.BGP
alias RDF.BlankNode
@default_remove_bnode_query_variables Application.get_env(
@ -11,57 +10,44 @@ defmodule RDF.Query.BGP.BlankNodeHandler do
)
def preprocess(triple_patterns) do
Enum.reduce(triple_patterns, {false, []}, fn
original_triple_pattern, {had_blank_nodes, triple_patterns} ->
{is_converted, triple_pattern} = convert_blank_nodes(original_triple_pattern)
{had_blank_nodes || is_converted, [triple_pattern | triple_patterns]}
end)
convert_triple_patterns(triple_patterns)
end
defp convert_blank_nodes({%BlankNode{} = s, %BlankNode{} = p, %BlankNode{} = o}),
do: {true, {bnode_var(s), bnode_var(p), bnode_var(o)}}
defp convert_triple_patterns(triple_patterns, acc \\ {[], []})
defp convert_triple_patterns([], acc), do: acc
defp convert_blank_nodes({s, %BlankNode{} = p, %BlankNode{} = o}),
do: {true, {s, bnode_var(p), bnode_var(o)}}
defp convert_triple_patterns([triple_pattern | rest], {converted, bnode_vars}) do
{converted_triple_pattern, bnode_vars} = convert_triple_pattern(triple_pattern, bnode_vars)
convert_triple_patterns(rest, {[converted_triple_pattern | converted], bnode_vars})
end
defp convert_blank_nodes({%BlankNode{} = s, p, %BlankNode{} = o}),
do: {true, {bnode_var(s), p, bnode_var(o)}}
defp convert_triple_pattern({s, p, o}, bnode_vars) do
{converted_s, bnode_vars} = convert_term(s, bnode_vars)
{converted_p, bnode_vars} = convert_term(p, bnode_vars)
{converted_o, bnode_vars} = convert_term(o, bnode_vars)
{{converted_s, converted_p, converted_o}, bnode_vars}
end
defp convert_blank_nodes({%BlankNode{} = s, %BlankNode{} = p, o}),
do: {true, {bnode_var(s), bnode_var(p), o}}
defp convert_term(%BlankNode{} = bnode, bnode_vars) do
bnode_var = bnode_var(bnode)
{bnode_var, [bnode_var | bnode_vars]}
end
defp convert_blank_nodes({%BlankNode{} = s, p, o}), do: {true, {bnode_var(s), p, o}}
defp convert_blank_nodes({s, %BlankNode{} = p, o}), do: {true, {s, bnode_var(p), o}}
defp convert_blank_nodes({s, p, %BlankNode{} = o}), do: {true, {s, p, bnode_var(o)}}
defp convert_blank_nodes(triple_pattern), do: {false, triple_pattern}
defp convert_term({_, _, _} = quoted_triple, bnode_vars) do
convert_triple_pattern(quoted_triple, bnode_vars)
end
defp convert_term(term, bnode_vars), do: {term, bnode_vars}
defp bnode_var(bnode), do: bnode |> to_string() |> String.to_atom()
def postprocess(solutions, bgp, has_blank_nodes, opts) do
if has_blank_nodes and
Keyword.get(opts, :remove_bnode_query_variables, @default_remove_bnode_query_variables) do
bnode_vars = bgp |> bnodes() |> Enum.map(&bnode_var/1)
def postprocess(solutions, [], _), do: solutions
def postprocess(solutions, bnode_vars, opts) do
if Keyword.get(opts, :remove_bnode_query_variables, @default_remove_bnode_query_variables) do
Enum.map(solutions, &Map.drop(&1, bnode_vars))
else
solutions
end
end
defp bnodes(%BGP{triple_patterns: triple_patterns}), do: bnodes(triple_patterns)
defp bnodes(triple_patterns) when is_list(triple_patterns) do
triple_patterns
|> Enum.flat_map(&bnodes/1)
|> Enum.uniq()
end
defp bnodes({%BlankNode{} = s, %BlankNode{} = p, %BlankNode{} = o}), do: [s, p, o]
defp bnodes({%BlankNode{} = s, %BlankNode{} = p, _}), do: [s, p]
defp bnodes({%BlankNode{} = s, _, %BlankNode{} = o}), do: [s, o]
defp bnodes({_, %BlankNode{} = p, %BlankNode{} = o}), do: [p, o]
defp bnodes({%BlankNode{} = s, _, _}), do: [s]
defp bnodes({_, %BlankNode{} = p, _}), do: [p]
defp bnodes({_, _, %BlankNode{} = o}), do: [o]
defp bnodes(_), do: []
end

View file

@ -3,12 +3,16 @@ defmodule RDF.Query.BGP.QueryPlanner do
alias RDF.Query.BGP
import RDF.Guards
@dedup_var ""
def query_plan(triple_patterns, solved \\ [], plan \\ [])
def query_plan([], _, plan), do: Enum.reverse(plan)
def query_plan(triple_patterns, solved, plan) do
[next_best | rest] = Enum.sort_by(triple_patterns, &triple_priority/1)
[next_best | rest] = Enum.sort(triple_patterns, &best_triple_pattern/2)
new_solved = Enum.uniq(BGP.variables(next_best) ++ solved)
query_plan(
@ -18,26 +22,111 @@ defmodule RDF.Query.BGP.QueryPlanner do
)
end
defp triple_priority({v, v, v}), do: triple_priority({v, "p", "o"})
defp triple_priority({v, v, o}), do: triple_priority({v, "p", o})
defp triple_priority({v, p, v}), do: triple_priority({v, p, "o"})
defp triple_priority({s, v, v}), do: triple_priority({s, v, "o"})
defp best_triple_pattern(triple_pattern1, triple_pattern2) do
triple_pattern1 = deduplicate(triple_pattern1)
triple_pattern2 = deduplicate(triple_pattern2)
{var_count1, var_positions1} = var_info(triple_pattern1)
{var_count2, var_positions2} = var_info(triple_pattern2)
defp triple_priority({s, p, o}) do
{sp, pp, op} = {value_priority(s), value_priority(p), value_priority(o)}
<<sp + pp + op::size(2), sp::size(1), pp::size(1), op::size(1)>>
if var_count1 != var_count2 do
var_count1 < var_count2
else
better_positioning(var_positions1, var_positions2)
end
end
defp value_priority(value) when is_atom(value), do: 1
defp value_priority(_), do: 0
defp deduplicate({v, v, v}), do: {v, @dedup_var, @dedup_var}
defp deduplicate({v, v, o}), do: {v, @dedup_var, o}
defp deduplicate({v, p, v}), do: {v, p, @dedup_var}
defp deduplicate({s, v, v}), do: {s, v, @dedup_var}
defp deduplicate({s, _, o} = star_triple) when is_triple(s) or is_triple(o) do
{deduplicated_triple, _} = deduplicate_star(star_triple, [])
deduplicated_triple
end
defp deduplicate(triple_pattern), do: triple_pattern
defp deduplicate_star({s, p, o}, vars) do
{s, vars} = deduplicate_star(s, vars)
{p, vars} = deduplicate_star(p, vars)
{o, vars} = deduplicate_star(o, vars)
{{s, p, o}, vars}
end
defp deduplicate_star(var, vars) when is_atom(var) do
if var in vars do
{@dedup_var, vars}
else
{var, [var | vars]}
end
end
defp deduplicate_star(var, vars), do: {var, vars}
defp var_info({s, p, o}) when is_triple(s) or is_triple(o) do
{s_var_count, s_quoted?} = var_info_star(s)
{p_var_count, _} = var_info_star(p)
{o_var_count, o_quoted?} = var_info_star(o)
{
star_term_value(s_var_count, s_quoted?) +
p_var_count +
star_term_value(o_var_count, o_quoted?),
{
if(s_quoted? and not (s_var_count == 0), do: {s_var_count}, else: s_var_count),
p_var_count,
if(o_quoted? and not (o_var_count == 0), do: {o_var_count}, else: o_var_count)
}
}
end
defp var_info({s, p, o}) when is_atom(s) and is_atom(p) and is_atom(o), do: {3, {1, 1, 1}}
defp var_info({s, p, _}) when is_atom(s) and is_atom(p), do: {2, {1, 1, 0}}
defp var_info({s, _, o}) when is_atom(s) and is_atom(o), do: {2, {1, 0, 1}}
defp var_info({_, p, o}) when is_atom(p) and is_atom(o), do: {2, {0, 1, 1}}
defp var_info({s, _, _}) when is_atom(s), do: {1, {1, 0, 0}}
defp var_info({_, p, _}) when is_atom(p), do: {1, {0, 1, 0}}
defp var_info({_, _, o}) when is_atom(o), do: {1, {0, 0, 1}}
defp var_info(_), do: {0, {0, 0, 0}}
defp var_info_star({s, p, o}) do
{s_var_count, _} = var_info_star(s)
{p_var_count, _} = var_info_star(p)
{o_var_count, _} = var_info_star(o)
{s_var_count + p_var_count + o_var_count, true}
end
defp var_info_star(var) when is_atom(var), do: {1, false}
defp var_info_star(_), do: {0, false}
defp star_term_value(0, _), do: 0
defp star_term_value(1, false), do: 1
defp star_term_value(count, _) when count > 2, do: 1
defp star_term_value(_, _), do: 0
defp better_positioning(var_position, var_position), do: true
defp better_positioning({0, _, _}, {_, _, _}), do: true
defp better_positioning({{_}, _, _}, {1, _, _}), do: true
defp better_positioning({{c1}, _, _}, {{c2}, _, _}) when c1 != c2, do: c1 < c2
defp better_positioning({s, 0, _}, {s, _, _}), do: true
defp better_positioning({s, p, 0}, {s, p, _}), do: true
defp better_positioning({s, p, {_}}, {s, p, 1}), do: true
defp better_positioning({s, p, {c1}}, {s, p, {c2}}), do: c1 < c2
defp better_positioning(_, _), do: false
defp mark_solved_variables(triple_patterns, solved) do
Enum.map(triple_patterns, fn {s, p, o} ->
{
if(is_atom(s) and s in solved, do: {s}, else: s),
if(is_atom(p) and p in solved, do: {p}, else: p),
if(is_atom(o) and o in solved, do: {o}, else: o)
}
end)
Enum.map(triple_patterns, &mark_solved(&1, solved))
end
defp mark_solved(var, solved) when is_atom(var) do
if var in solved, do: {var}, else: var
end
defp mark_solved({s, p, o}, solved) do
{mark_solved(s, solved), mark_solved(p, solved), mark_solved(o, solved)}
end
defp mark_solved(var, _), do: var
end

View file

@ -7,6 +7,8 @@ defmodule RDF.Query.BGP.Simple do
alias RDF.Query.BGP.{QueryPlanner, BlankNodeHandler}
alias RDF.{Graph, Description}
import RDF.Guards
@impl RDF.Query.BGP.Matcher
def execute(bgp, graph, opts \\ [])
@ -14,12 +16,12 @@ defmodule RDF.Query.BGP.Simple do
def execute(%BGP{triple_patterns: []}, _, _), do: [%{}]
def execute(%BGP{triple_patterns: triple_patterns}, %Graph{} = graph, opts) do
{bnode_state, preprocessed_triple_patterns} = BlankNodeHandler.preprocess(triple_patterns)
{preprocessed_triple_patterns, bnode_state} = BlankNodeHandler.preprocess(triple_patterns)
preprocessed_triple_patterns
|> QueryPlanner.query_plan()
|> do_execute(graph)
|> BlankNodeHandler.postprocess(triple_patterns, bnode_state, opts)
|> BlankNodeHandler.postprocess(bnode_state, opts)
end
@impl RDF.Query.BGP.Matcher
@ -42,41 +44,95 @@ defmodule RDF.Query.BGP.Simple 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)
|> Enum.flat_map(&merging_match(&1, graph))
end
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))
end)
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)
|> Enum.flat_map(&merging_match(&1, graph))
else
graph
|> match(triple_pattern)
|> Enum.flat_map(fn solution ->
Enum.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)
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 ->
@ -93,21 +149,27 @@ defmodule RDF.Query.BGP.Simple do
when is_atom(subject_variable) do
Enum.reduce(descriptions, [], fn {subject, description}, acc ->
case match(description, solve_variables(subject_variable, subject, triple_pattern)) do
nil ->
acc
solutions ->
Enum.map(solutions, fn solution ->
Map.put(solution, subject_variable, subject)
end) ++ acc
nil -> acc
solutions -> Enum.map(solutions, &Map.put(&1, subject_variable, subject)) ++ acc
end
end)
end
defp match(%Graph{} = graph, {subject, _, _} = triple_pattern) do
case graph[subject] do
nil -> []
description -> match(description, triple_pattern)
if quoted_triple_with_variables?(subject) do
graph
|> matching_subject_triples(subject)
|> Enum.flat_map(fn {description, subject_solutions} ->
case match(description, solve_variables(subject_solutions, triple_pattern)) do
nil -> []
solutions -> Enum.map(solutions, &Map.merge(&1, subject_solutions))
end
end)
else
case graph[subject] do
nil -> []
description -> match(description, triple_pattern)
end
end
end
@ -132,24 +194,26 @@ defmodule RDF.Query.BGP.Simple 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
Enum.reduce(predications, [], fn {predicate, objects}, solutions ->
if Map.has_key?(objects, object) do
[%{predicate_variable => predicate} | solutions]
else
solutions
cond do
Map.has_key?(objects, object) ->
[%{predicate_variable => predicate} | solutions]
quoted_triple_with_variables?(object) ->
(objects
|> matching_object_triples(object)
|> Enum.map(&Map.put(&1, predicate_variable, predicate))) ++
solutions
true ->
solutions
end
end)
end
defp match(
%Description{predications: predications},
{_, predicate, object_or_variable}
) do
defp match(%Description{predications: predications}, {_, predicate, object_or_variable}) do
case predications[predicate] do
nil ->
[]
@ -158,14 +222,15 @@ defmodule RDF.Query.BGP.Simple do
cond do
# object_or_variable is a variable
is_atom(object_or_variable) ->
Enum.map(objects, fn {object, _} ->
%{object_or_variable => object}
end)
Enum.map(objects, fn {object, _} -> %{object_or_variable => object} end)
# object_or_variable is a object
Map.has_key?(objects, object_or_variable) ->
[%{}]
quoted_triple_with_variables?(object_or_variable) ->
matching_object_triples(objects, object_or_variable)
# else
true ->
[]
@ -173,12 +238,90 @@ defmodule RDF.Query.BGP.Simple do
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
Enum.reduce(graph.descriptions, [], fn
{subject, description}, acc when is_triple(subject) ->
case match_triple(subject, triple_pattern) do
nil -> acc
solutions -> [{description, solutions} | acc]
end
_, acc ->
acc
end)
end
defp matching_object_triples(objects, triple_pattern) do
Enum.reduce(objects, [], fn
{object, _}, acc when is_triple(object) ->
case match_triple(object, triple_pattern) do
nil -> acc
solutions -> [solutions | acc]
end
_, acc ->
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

@ -14,12 +14,12 @@ defmodule RDF.Query.BGP.Stream do
def stream(%BGP{triple_patterns: []}, _, _), do: to_stream([%{}])
def stream(%BGP{triple_patterns: triple_patterns}, %Graph{} = graph, opts) do
{bnode_state, preprocessed_triple_patterns} = BlankNodeHandler.preprocess(triple_patterns)
{preprocessed_triple_patterns, bnode_state} = BlankNodeHandler.preprocess(triple_patterns)
preprocessed_triple_patterns
|> QueryPlanner.query_plan()
|> do_execute(graph)
|> BlankNodeHandler.postprocess(triple_patterns, bnode_state, opts)
|> BlankNodeHandler.postprocess(bnode_state, opts)
end
@impl RDF.Query.BGP.Matcher

View file

@ -1,5 +1,10 @@
defmodule RDF.Query.Builder do
@moduledoc false
@moduledoc !"""
Functions for building `RDF.Query`s.
This functions are not intended to be used directly,
but through the `RDF.Query` API instead.
"""
alias RDF.Query.BGP
alias RDF.{IRI, BlankNode, Literal, Namespace, PropertyMap}
@ -30,7 +35,7 @@ defmodule RDF.Query.Builder do
end
defp triple_patterns({subject, predicate, objects}, property_map) do
with {:ok, subject_pattern} <- subject_pattern(subject) do
with {:ok, subject_pattern} <- subject_pattern(subject, property_map) do
do_triple_patterns(subject_pattern, {predicate, objects}, property_map)
end
end
@ -40,7 +45,7 @@ defmodule RDF.Query.Builder do
end
defp triple_patterns({subject, predications}, property_map) do
with {:ok, subject_pattern} <- subject_pattern(subject) do
with {:ok, subject_pattern} <- subject_pattern(subject, property_map) do
predications
|> List.wrap()
|> flat_map_while_ok(&do_triple_patterns(subject_pattern, &1, property_map))
@ -52,15 +57,15 @@ defmodule RDF.Query.Builder do
objects
|> List.wrap()
|> map_while_ok(fn object ->
with {:ok, object_pattern} <- object_pattern(object) do
with {:ok, object_pattern} <- object_pattern(object, property_map) do
{:ok, {subject_pattern, predicate_pattern, object_pattern}}
end
end)
end
end
defp subject_pattern(subject) do
value = variable(subject) || resource(subject)
defp subject_pattern(subject, property_map) do
value = variable(subject) || resource(subject) || quoted_triple(subject, property_map)
if value do
{:ok, value}
@ -85,8 +90,10 @@ defmodule RDF.Query.Builder do
end
end
defp object_pattern(object) do
value = variable(object) || resource(object) || literal(object)
defp object_pattern(object, property_map) do
value =
variable(object) || resource(object) || literal(object) ||
quoted_triple(object, property_map)
if value do
{:ok, value}
@ -140,6 +147,18 @@ defmodule RDF.Query.Builder do
defp literal(%Literal{} = literal), do: literal
defp literal(value), do: Literal.coerce(value)
defp quoted_triple({s, p, o}, property_map) do
with {:ok, subject} <- subject_pattern(s, property_map),
{:ok, predicate} <- predicate_pattern(p, property_map),
{:ok, object} <- object_pattern(o, property_map) do
{subject, predicate, object}
else
_ -> nil
end
end
defp quoted_triple(_, _), do: nil
def path(query, opts \\ [])
def path(query, _) when is_list(query) and length(query) < 3 do

View file

@ -0,0 +1,129 @@
defmodule RDF.QueryPlannerHelper do
import RDF.Guards
def better?(left, right) do
left = simplify_triple(left)
right = simplify_triple(right)
less_variables?(left, right) or
(same_count_of_variables?(left, right) and better_positioning?(left, right)) or
false
end
defp simplify_triple({s, p, o}), do: {simplify_term(s), simplify_term(p), simplify_term(o)}
defp simplify_term(triple) when is_triple(triple) do
case variable_count(triple) do
0 -> 0
variable_count -> {variable_count}
end
end
defp simplify_term(variable) when is_atom(variable), do: 1
defp simplify_term(_), do: 0
defp variable_count({count}), do: count
defp variable_count(variable) when is_atom(variable), do: 1
defp variable_count(triple) when is_tuple(triple) do
triple
|> Tuple.to_list()
|> Enum.count(&variable?/1)
end
defp variable_count(_), do: 0
defp variable?(variable) when is_atom(variable), do: true
defp variable?(%type{}) when type in [RDF.IRI, RDF.Literal, RDF.BlankNode], do: false
defp variable?(count) when is_integer(count), do: count > 0
defp less_variables?(left, right) do
valued_variable_count(left) < valued_variable_count(right)
end
defp same_count_of_variables?(left, right) do
valued_variable_count(left) == valued_variable_count(right)
end
defp valued_variable_count(triple) when is_triple(triple) do
triple
|> Tuple.to_list()
|> Enum.map(&valued_variable_count/1)
|> Enum.sum()
end
defp valued_variable_count({3}), do: 1
defp valued_variable_count({_}), do: 0
defp valued_variable_count(count), do: count
defp better_positioning?(left, right) do
Enum.reduce_while(0..2, nil, fn i, _ ->
case better_term?(elem(left, i), elem(right, i)) do
nil -> {:cont, true}
result -> {:halt, result}
end
end)
end
defp better_term?(term, term), do: nil
defp better_term?(1, 0), do: false
defp better_term?(0, 1), do: true
defp better_term?({_}, 0), do: false
defp better_term?({_}, 1), do: true
defp better_term?(0, {_}), do: true
defp better_term?(1, {_}), do: false
defp better_term?({left_count}, {right_count}), do: left_count < right_count
#############################################################################
# functions for generating all possible combinations of triple patterns
# with quoted triples (but NOT nested quoted triples)
def all_combinations do
all_tps = all_tps()
for left <- all_tps, right <- all_tps, do: {left, right}
end
defp all_tps do
elements = [:var, :term, :quoted_triple]
for s <- elements, p <- elements, o <- elements do
{s, p, o}
end
|> Enum.reject(&match?({_, :quoted_triple, _}, &1))
|> Enum.map(fn {s, p, o} ->
{tp_term(s, :subject), tp_term(p, :predicate), tp_term(o, :object)}
end)
|> Enum.flat_map(&expand_quoted_triples/1)
end
defp expand_quoted_triples({{}, p, o}) do
quoted_triples("s")
|> Enum.map(&{&1, p, o})
|> Enum.flat_map(&expand_quoted_triples(&1))
end
defp expand_quoted_triples({s, p, {}}) do
quoted_triples("o")
|> Enum.map(&{s, p, &1})
|> Enum.flat_map(&expand_quoted_triples(&1))
end
defp expand_quoted_triples(triple), do: [triple]
defp quoted_triples(pos) do
[
{RDF.iri("urn:QS#{pos}"), RDF.iri("urn:QP#{pos}"), RDF.iri("urn:QO#{pos}")},
{:"qs_#{pos}?", RDF.iri("urn:QP#{pos}"), RDF.iri("urn:QO#{pos}")},
{:"qs_#{pos}?", :"qp_#{pos}?", RDF.iri("urn:QO#{pos}")},
{:"qs_#{pos}?", :"qp_#{pos}?", :"qo_#{pos}?"}
]
end
defp tp_term(:var, :subject), do: :s?
defp tp_term(:var, :predicate), do: :p?
defp tp_term(:var, :object), do: :o?
defp tp_term(:term, :subject), do: RDF.iri("urn:S")
defp tp_term(:term, :predicate), do: RDF.iri("urn:P")
defp tp_term(:term, :object), do: RDF.iri("urn:O")
defp tp_term(:quoted_triple, _), do: {}
end

View file

@ -22,4 +22,6 @@ defmodule RDF.Query.Test.Case do
do: %BGP{triple_patterns: [triple_pattern]}
def ok_bgp_struct(triple_patterns), do: {:ok, bgp_struct(triple_patterns)}
def comparable(elements), do: MapSet.new(elements)
end

View file

@ -1,37 +1,119 @@
defmodule RDF.Query.BGP.QueryPlannerTest do
use RDF.Query.Test.Case
import RDF.QueryPlannerHelper
alias RDF.Query.BGP.QueryPlanner
describe "query_plan/1" do
test "empty" do
assert QueryPlanner.query_plan([]) == []
end
test "single" do
assert QueryPlanner.query_plan([{:a, :b, :c}]) == [{:a, :b, :c}]
end
test "multiple connected" do
assert QueryPlanner.query_plan([
{:a, :b, :c},
{:a, :d, ~L"foo"}
]) == [
{:a, :d, ~L"foo"},
{{:a}, :b, :c}
]
assert QueryPlanner.query_plan([
{:s, :p, :o},
{:s2, :p2, :o2},
{:s, :p, :o2},
{:s4, :p4, ~L"foo"}
]) == [
{:s4, :p4, ~L"foo"},
{:s, :p, :o},
{{:s}, {:p}, :o2},
{:s2, :p2, {:o2}}
]
end
test "empty" do
assert QueryPlanner.query_plan([]) == []
end
test "single" do
assert QueryPlanner.query_plan([tp(1, {1, 1, 1})]) == [tp(1, {1, 1, 1})]
end
test "multiple connected" do
assert QueryPlanner.query_plan([tp(1, {:o, 0, 0}), tp(2, {0, 0, :o})]) == [
tp(2, {0, 0, :o}),
tp(1, {{:o}, 0, 0})
]
assert QueryPlanner.query_plan([tp(1, {:a, 1, 1}), tp(2, {:a, 1, 0})]) == [
tp(2, {:a, 1, 0}),
tp(1, {{:a}, 1, 1})
]
assert QueryPlanner.query_plan([
tp(1, {:s, :p, 1}),
tp(2, {1, 1, :o2}),
tp(3, {:s, :p, :o2}),
tp(4, {1, 1, 0})
]) == [
tp(4, {1, 1, 0}),
tp(1, {:s, :p, 1}),
tp(3, {{:s}, {:p}, :o2}),
tp(2, {1, 1, {:o2}})
]
end
test "deeply nested quoted triples" do
assert QueryPlanner.query_plan([
{:c, :d, ~L"foo"},
{{EX.S, EX.p(), EX.O}, :b, :c}
]) == [
{{EX.S, EX.p(), EX.O}, :b, :c},
{{:c}, :d, ~L"foo"}
]
assert QueryPlanner.query_plan([
{
{{:a, :b, ~B"c"}, :d, :e},
:f,
{{{:g, :h, {RDF.iri(EX.I), :j, ~L"k"}}, :m, :n}, :o, :p}
},
# This similar pattern contains a duplicate and should be prioritized
{
{{:a1, :b1, ~B"c"}, :d1, :a1},
:f1,
{{{:g1, :h1, {RDF.iri(EX.I), :j1, ~L"k"}}, :m1, :n1}, :o1, :p}
}
]) == [
{
{{:a1, :b1, ~B"c"}, :d1, :a1},
:f1,
{{{:g1, :h1, {RDF.iri(EX.I), :j1, ~L"k"}}, :m1, :n1}, :o1, :p}
},
{
{{:a, :b, ~B"c"}, :d, :e},
:f,
{{{:g, :h, {RDF.iri(EX.I), :j, ~L"k"}}, :m, :n}, :o, {:p}}
}
]
assert QueryPlanner.query_plan([
{
{{:a, :b, ~B"c"}, :d, :e},
:f,
{{{:g, :h, {RDF.iri(EX.I), :j, ~L"k"}}, :m, :n}, :o, :p}
},
{
{{:a, :b, ~B"c"}, :d, :e},
:f,
{{{:g, :h, {RDF.iri(EX.I), :j, ~L"k"}}, :m, :n}, :o, :a}
}
]) == [
{
{{:a, :b, ~B"c"}, :d, :e},
:f,
{{{:g, :h, {RDF.iri(EX.I), :j, ~L"k"}}, :m, :n}, :o, :a}
},
{
{{{:a}, {:b}, ~B"c"}, {:d}, {:e}},
{:f},
{{{{:g}, {:h}, {RDF.iri(EX.I), {:j}, ~L"k"}}, {:m}, {:n}}, {:o}, :p}
}
]
end
test "all possible combinations" do
Enum.each(all_combinations(), fn {left, right} ->
if left |> better?(right) do
assert match?([^left, _], QueryPlanner.query_plan([left, right])),
"#{inspect(left)} should have been prioritized over #{inspect(right)}, but wasn't"
end
end)
end
def tp(_, {0, 0, 0}), do: {EX.s(), EX.p(), ~L"o"}
def tp(i, {0, 0, o}), do: {EX.s(), EX.p(), tp_var(:o, o, i)}
def tp(i, {0, p, 0}), do: {EX.s(), tp_var(:p, p, i), ~L"o"}
def tp(i, {s, 0, 0}), do: {tp_var(:s, s, i), EX.p(), ~L"o"}
def tp(i, {0, p, o}), do: {EX.s(), tp_var(:p, p, i), tp_var(:o, o, i)}
def tp(i, {s, 0, o}), do: {tp_var(:s, s, i), EX.p(), tp_var(:o, o, i)}
def tp(i, {s, p, 0}), do: {tp_var(:s, s, i), tp_var(:p, p, i), ~L"o"}
def tp(i, {s, p, o}), do: {tp_var(:s, s, i), tp_var(:p, p, i), tp_var(:o, o, i)}
defp tp_var(pos, 1, i), do: String.to_atom("#{to_string(pos)}_#{i}")
defp tp_var(_, var, _), do: var
end

View file

@ -0,0 +1,260 @@
defmodule RDF.Query.BGP.SimpleStarTest do
use RDF.Query.Test.Case
import RDF.Query.BGP.Simple, 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.s3(), p: EX.p3()},
%{s: {EX.qs1(), EX.qp(), EX.qo1()}, p: EX.p2()}
]
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.s3(), p: EX.p3()},
%{
qs1: EX.qs1(),
qs2: EX.qs2(),
qp: EX.qp(),
s: {EX.qs1(), EX.qp(), EX.qo1()},
p: EX.p2()
}
]
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

View file

@ -184,32 +184,34 @@ defmodule RDF.Query.BGP.SimpleTest do
{EX.s1(), :p1, :o},
{:s, :p2, EX.o2()}
])
|> execute(@example_graph) == [
%{
p1: EX.p1(),
o: EX.o1(),
s: EX.s3(),
p2: EX.p3()
},
%{
p1: EX.p2(),
o: EX.o2(),
s: EX.s3(),
p2: EX.p3()
},
%{
p1: EX.p1(),
o: EX.o1(),
s: EX.s1(),
p2: EX.p2()
},
%{
p1: EX.p2(),
o: EX.o2(),
s: EX.s1(),
p2: EX.p2()
}
]
|> execute(@example_graph)
|> comparable() ==
comparable([
%{
p1: EX.p1(),
o: EX.o1(),
s: EX.s3(),
p2: EX.p3()
},
%{
p1: EX.p2(),
o: EX.o2(),
s: EX.s3(),
p2: EX.p3()
},
%{
p1: EX.p1(),
o: EX.o1(),
s: EX.s1(),
p2: EX.p2()
},
%{
p1: EX.p2(),
o: EX.o2(),
s: EX.s1(),
p2: EX.p2()
}
])
end
test "blank nodes behave like variables, but don't appear in the solution" do
@ -225,8 +227,9 @@ defmodule RDF.Query.BGP.SimpleTest do
{EX.s1(), :p1, :o},
{RDF.bnode("s"), :p2, EX.o2()}
])
|> execute(@example_graph) ==
[
|> execute(@example_graph)
|> comparable() ==
comparable([
%{
p1: EX.p1(),
o: EX.o1(),
@ -247,6 +250,6 @@ defmodule RDF.Query.BGP.SimpleTest do
o: EX.o2(),
p2: EX.p2()
}
]
])
end
end

View file

@ -0,0 +1,140 @@
defmodule RDF.Query.BuilderStarTest do
use RDF.Query.Test.Case
alias RDF.Query.Builder
describe "bgp/1" do
test "variables" do
assert Builder.bgp([{{:as1?, :ap1?, :ao1?}, :p?, {:as2?, :ap2?, :ao2?}}]) ==
ok_bgp_struct([{{:as1, :ap1, :ao1}, :p, {:as2, :ap2, :ao2}}])
end
test "blank nodes" do
assert Builder.bgp([
{{RDF.bnode("as1"), RDF.bnode("ap1"), RDF.bnode("ao1")}, RDF.bnode("p"),
{RDF.bnode("as2"), RDF.bnode("ap2"), RDF.bnode("ao2")}}
]) ==
ok_bgp_struct([
{{RDF.bnode("as1"), RDF.bnode("ap1"), RDF.bnode("ao1")}, RDF.bnode("p"),
{RDF.bnode("as2"), RDF.bnode("ap2"), RDF.bnode("ao2")}}
])
end
test "blank nodes as atoms" do
assert Builder.bgp([{{:_as1, :_ap1, :_ao1}, :_p, {:_as2, :_ap2, :_ao2}}]) ==
ok_bgp_struct([
{{RDF.bnode("as1"), RDF.bnode("ap1"), RDF.bnode("ao1")}, RDF.bnode("p"),
{RDF.bnode("as2"), RDF.bnode("ap2"), RDF.bnode("ao2")}}
])
end
test "variable notation has precedence over blank node notation" do
assert Builder.bgp([{{:_as1?, :_ap1?, :_ao1?}, :_p?, {:_as2?, :_ap2?, :_ao2?}}]) ==
ok_bgp_struct([{{:_as1, :_ap1, :_ao1}, :_p, {:_as2, :_ap2, :_ao2}}])
end
test "various RDF terms" do
assert Builder.bgp([
{{EX.AS, :a, ~I<http://example.com/ao>}, EX.p(),
{URI.parse("http://example.com/as"), EX.ap(), 42}}
]) ==
ok_bgp_struct([
{{RDF.iri(EX.AS), RDF.type(), EX.ao()}, EX.p(),
{EX.as(), EX.ap(), XSD.integer(42)}}
])
end
test "literals on non-object positions" do
assert {:error, %RDF.Query.InvalidError{}} =
Builder.bgp([{{~L"foo", EX.p(), ~L"bar"}, EX.p(), EX.o()}])
end
test "multiple objects to the same subject-predicate" do
assert Builder.bgp([
{{EX.as(), EX.ap(), EX.ao()}, EX.p(),
[{EX.s(), EX.p(), EX.o1()}, {EX.s(), EX.p(), EX.o2()}]}
]) ==
ok_bgp_struct([
{{EX.as(), EX.ap(), EX.ao()}, EX.p(), {EX.s(), EX.p(), EX.o1()}},
{{EX.as(), EX.ap(), EX.ao()}, EX.p(), {EX.s(), EX.p(), EX.o2()}}
])
end
test "multiple predicate-object pairs to the same subject" do
assert Builder.bgp([
{{EX.as(), EX.ap(), EX.ao()},
[
{EX.p1(), {EX.s(), EX.p(), EX.o1()}},
{EX.p2(), {EX.s(), EX.p(), EX.o2()}}
]}
]) ==
ok_bgp_struct([
{{EX.as(), EX.ap(), EX.ao()}, EX.p1(), {EX.s(), EX.p(), EX.o1()}},
{{EX.as(), EX.ap(), EX.ao()}, EX.p2(), {EX.s(), EX.p(), EX.o2()}}
])
assert Builder.bgp([{{EX.as(), EX.ap(), EX.ao()}, {EX.p1(), {EX.s(), EX.p(), EX.o1()}}}]) ==
ok_bgp_struct([{{EX.as(), EX.ap(), EX.ao()}, EX.p1(), {EX.s(), EX.p(), EX.o1()}}])
end
test "triple patterns with maps" do
assert Builder.bgp(%{
{EX.as(), EX.ap(), EX.ao1()} => %{EX.p1() => {EX.s(), EX.p(), EX.o1()}},
{EX.as(), EX.ap(), EX.ao2()} => {EX.p2(), {EX.s(), EX.p(), EX.o2()}}
}) ==
ok_bgp_struct([
{{EX.as(), EX.ap(), EX.ao1()}, EX.p1(), {EX.s(), EX.p(), EX.o1()}},
{{EX.as(), EX.ap(), EX.ao2()}, EX.p2(), {EX.s(), EX.p(), EX.o2()}}
])
end
test "with contexts" do
assert Builder.bgp(
%{
{EX.as(), :ap, EX.ao1()} => %{p1: {EX.s(), :p, EX.o1()}},
{EX.as(), :ap, EX.ao2()} => {:p2, {EX.s(), :p, EX.o2()}}
},
context: %{
p: EX.p(),
ap: EX.ap(),
p1: EX.p1(),
p2: EX.p2()
}
) ==
ok_bgp_struct([
{{EX.as(), EX.ap(), EX.ao1()}, EX.p1(), {EX.s(), EX.p(), EX.o1()}},
{{EX.as(), EX.ap(), EX.ao2()}, EX.p2(), {EX.s(), EX.p(), EX.o2()}}
])
end
test "with deeply nested quoted triples" do
assert Builder.bgp([
{
{{:a?, :b?, :_c}, :d?, :e?},
:f?,
{{{:g?, :h?, {EX.I, :j?, "k"}}, :m?, :n?}, :o?, :p?}
}
]) ==
ok_bgp_struct([
{
{{:a, :b, ~B"c"}, :d, :e},
:f,
{{{:g, :h, {RDF.iri(EX.I), :j, ~L"k"}}, :m, :n}, :o, :p}
}
])
end
end
test "path/2" do
assert Builder.path([
{EX.as(), EX.ap(), EX.ao1()},
EX.p1(),
EX.p2(),
{EX.as(), EX.ap(), EX.ao2()}
]) ==
ok_bgp_struct([
{{EX.as(), EX.ap(), EX.ao1()}, EX.p1(), RDF.bnode("b0")},
{RDF.bnode("b0"), EX.p2(), {EX.as(), EX.ap(), EX.ao2()}}
])
end
end