Add RDF-star support on the BGP query engine RDF.Query.BGP.Simple
This commit is contained in:
parent
110c305eeb
commit
20934ef0ec
12 changed files with 1052 additions and 191 deletions
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
129
test/support/query/query_planner_helper.ex
Normal file
129
test/support/query/query_planner_helper.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
260
test/unit/query/bgp/simple_star_test.exs
Normal file
260
test/unit/query/bgp/simple_star_test.exs
Normal 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
|
|
@ -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
|
||||
|
|
140
test/unit/query/builder_star_test.exs
Normal file
140
test/unit/query/builder_star_test.exs
Normal 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
|
Loading…
Reference in a new issue