diff --git a/lib/rdf.ex b/lib/rdf.ex
index 7bed1a3..915d61c 100644
--- a/lib/rdf.ex
+++ b/lib/rdf.ex
@@ -9,6 +9,7 @@ defmodule RDF do
- `RDF.IRI`
- `RDF.BlankNode`
- `RDF.Literal`
+ - the `RDF.Literal.Datatype` system
- a facility for the mapping of URIs of a vocabulary to Elixir modules and
functions: `RDF.Vocabulary.Namespace`
- modules for the construction of statements
@@ -22,6 +23,7 @@ defmodule RDF do
- `RDF.Data`
- `RDF.List`
- `RDF.Diff`
+ - functions to construct and execute basic graph pattern queries: `RDF.Query`
- functions for working with RDF serializations: `RDF.Serialization`
- behaviours for the definition of RDF serialization formats
- `RDF.Serialization.Format`
diff --git a/lib/rdf/graph.ex b/lib/rdf/graph.ex
index d7e1eda..42b3b85 100644
--- a/lib/rdf/graph.ex
+++ b/lib/rdf/graph.ex
@@ -479,10 +479,26 @@ defmodule RDF.Graph do
Access.fetch(descriptions, coerce_subject(subject))
end
+ @doc """
+ Execute the given `query` against the given `graph`.
+
+ This is just a convenience delegator function to `RDF.Query.execute!/3` with
+ the first two arguments swapped so it can be used in a pipeline on a `RDF.Graph`.
+
+ See `RDF.Query.execute/3` and `RDF.Query.execute!/3` for more information and examples.
+ """
def query(graph, query, opts \\ []) do
RDF.Query.execute!(query, graph, opts)
end
+ @doc """
+ Returns a `Stream` for the execution of the given `query` against the given `graph`.
+
+ This is just a convenience delegator function to `RDF.Query.stream!/3` with
+ the first two arguments swapped so it can be used in a pipeline on a `RDF.Graph`.
+
+ See `RDF.Query.stream/3` and `RDF.Query.stream!/3` for more information and examples.
+ """
def query_stream(graph, query, opts \\ []) do
RDF.Query.stream!(query, graph, opts)
end
@@ -854,7 +870,7 @@ defmodule RDF.Graph do
def take(%RDF.Graph{} = graph, subjects, properties) do
graph = take(graph, subjects, nil)
- %RDF.Graph{graph |
+ %RDF.Graph{graph |
descriptions: Map.new(graph.descriptions, fn {subject, description} ->
{subject, Description.take(description, properties)}
end)
diff --git a/lib/rdf/query.ex b/lib/rdf/query.ex
index ac884a6..270c79a 100644
--- a/lib/rdf/query.ex
+++ b/lib/rdf/query.ex
@@ -8,33 +8,136 @@ defmodule RDF.Query do
@default_matcher RDF.Query.BGP.Stream
+ @doc """
+ Execute the given `query` against the given `graph`.
+ The `query` can be given directly as `RDF.Query.BGP` struct created with one
+ of the builder functions in this module or as basic graph pattern expression
+ accepted by `bgp/1`.
+
+ The result is a list of maps with the solutions for the variables in the graph
+ pattern query and will be returned in a `:ok` tuple. In case of an error a
+ `:error` tuple is returned.
+
+ ## Example
+
+ Let's assume we have an `example_graph` with these triples:
+
+ ```turtle
+ @prefix foaf: .
+ @prefix ex: .
+
+ ex:Outlaw
+ foaf:name "Johnny Lee Outlaw" ;
+ foaf:mbox .
+
+ ex:Goodguy
+ foaf:name "Peter Goodguy" ;
+ foaf:mbox ;
+ foaf:friend ex:Outlaw .
+ ```
+
+ iex> {:_, FOAF.name, :name?} |> RDF.Query.execute(example_graph())
+ {:ok, [%{name: ~L"Peter Goodguy"}, %{name: ~L"Johnny Lee Outlaw"}]}
+
+ iex> [
+ ...> {:_, FOAF.name, :name?},
+ ...> {:_, FOAF.mbox, :mbox?},
+ ...> ] |> RDF.Query.execute(example_graph())
+ {:ok, [
+ %{name: ~L"Peter Goodguy", mbox: ~I},
+ %{name: ~L"Johnny Lee Outlaw", mbox: ~I}
+ ]}
+
+ iex> query = [
+ ...> {:_, FOAF.name, :name?},
+ ...> {:_, FOAF.mbox, :mbox?},
+ ...> ] |> RDF.Query.bgp()
+ ...> RDF.Query.execute(query, example_graph())
+ {:ok, [
+ %{name: ~L"Peter Goodguy", mbox: ~I},
+ %{name: ~L"Johnny Lee Outlaw", mbox: ~I}
+ ]}
+
+ iex> [
+ ...> EX.Goodguy, FOAF.friend, FOAF.name, :name?
+ ...> ] |> RDF.Query.path() |> RDF.Query.execute(example_graph())
+ {:ok, [%{name: ~L"Johnny Lee Outlaw"}]}
+
+ """
def execute(query, graph, opts \\ [])
def execute(%BGP{} = query, %Graph{} = graph, opts) do
matcher = Keyword.get(opts, :matcher, @default_matcher)
- matcher.execute(query, graph, opts)
+ {:ok, matcher.execute(query, graph, opts)}
end
def execute(query, graph, opts) when is_list(query) or is_tuple(query) do
with {:ok, bgp} <- Builder.bgp(query) do
- execute(bgp, graph, opts)
+ execute(bgp, graph, opts)
end
end
- def execute!(query, graph, opts) do
+ @doc """
+ Execute the given `query` against the given `graph`.
+
+ As opposed to `execute/3` this returns the results directly or fails with an
+ exception.
+ """
+ def execute!(query, graph, opts \\ []) do
case execute(query, graph, opts) do
{:ok, results} -> results
{:error, error} -> raise error
end
end
+ @doc """
+ Returns a `Stream` for the execution of the given `query` against the given `graph`.
+ Just like on `execute/3` the `query` can be given directly as `RDF.Query.BGP` struct
+ created with one of the builder functions in this module or as basic graph pattern
+ expression accepted by `bgp/1`.
+
+ The stream of solutions for variable bindings will be returned in a `:ok` tuple.
+ In case of an error a `:error` tuple is returned.
+
+ ## Example
+
+ Let's assume we have an `example_graph` with these triples:
+
+ ```turtle
+ @prefix foaf: .
+ @prefix ex: .
+
+ ex:Outlaw
+ foaf:name "Johnny Lee Outlaw" ;
+ foaf:mbox .
+
+ ex:Goodguy
+ foaf:name "Peter Goodguy" ;
+ foaf:mbox ;
+ foaf:friend ex:Outlaw .
+ ```
+
+ iex> {:ok, stream} = {:_, FOAF.name, :name?} |> RDF.Query.stream(example_graph())
+ ...> Enum.to_list(stream)
+ [%{name: ~L"Peter Goodguy"}, %{name: ~L"Johnny Lee Outlaw"}]
+
+ iex> {:ok, stream} = [
+ ...> {:_, FOAF.name, :name?},
+ ...> {:_, FOAF.mbox, :mbox?},
+ ...> ] |> RDF.Query.stream(example_graph())
+ ...> Enum.take(stream, 1)
+ [
+ %{name: ~L"Peter Goodguy", mbox: ~I},
+ ]
+
+ """
def stream(query, graph, opts \\ [])
def stream(%BGP{} = query, %Graph{} = graph, opts) do
matcher = Keyword.get(opts, :matcher, @default_matcher)
- matcher.stream(query, graph, opts)
+ {:ok, matcher.stream(query, graph, opts)}
end
def stream(query, graph, opts) when is_list(query) or is_tuple(query) do
@@ -43,13 +146,87 @@ defmodule RDF.Query do
end
end
- def stream!(query, graph, opts) do
- case execute(query, graph, opts) do
+ @doc """
+ Returns a `Stream` for the execution of the given `query` against the given `graph`.
+
+ As opposed to `stream/3` this returns the stream directly or fails with an
+ exception.
+ """
+ def stream!(query, graph, opts \\ []) do
+ case stream(query, graph, opts) do
{:ok, results} -> results
{:error, error} -> raise error
end
end
+ @doc """
+ Creates a `RDF.Query.BGP` struct.
+
+ A basic graph pattern consist of single or list of triple patterns.
+ A triple pattern is a tuple which consists of RDF terms or variables for
+ the subject, predicate and object of a RDF triple.
+
+ As RDF terms `RDF.IRI`s, `RDF.BlankNode`s, `RDF.Literal`s or all Elixir
+ values which can be coerced to any of those are allowed, i.e.
+ `RDF.Vocabulary.Namespace` atoms or Elixir values which can be coerced to RDF
+ literals with `RDF.Literal.coerce/1` (only on object position). On predicate
+ position the `:a` atom can be used for the `rdf:type` property.
+
+ Variables are written as atoms ending with a question mark. Blank nodes which
+ in a graph query patterns act like a variable which doesn't show up in the
+ results can be written as atoms starting with an underscore.
+
+ Here's a basic graph pattern example:
+
+ ```elixir
+ [
+ {:s?, :a, EX.Foo},
+ {:s?, :a, EX.Bar},
+ {:s?, RDFS.label, "foo"},
+ {:s?, :p?, :o?}
+ ]
+ ```
+
+ Multiple triple patterns sharing the same subject and/or predicate can be grouped:
+
+ - Multiple objects to the same subject-predicate pair can be written by just
+ writing them one by one in the same triple pattern.
+ - Multiple predicate-objects pair on the same subject can be written by
+ grouping them with square brackets.
+
+ With these, the previous example can be shortened to:
+
+ ```elixir
+ {
+ :s?,
+ [:a, EX.Foo, EX.Bar],
+ [RDFS.label, "foo"],
+ [:p?, :o?]
+ }
+ ```
+
+ """
defdelegate bgp(query), to: Builder, as: :bgp!
+
+ @doc """
+ Creates a `RDF.Query.BGP` struct for a path through a graph.
+
+ The elements of the path can consist of the same RDF terms and variable
+ expressions allowed in `bgp/1` expressions.
+
+ ## Example
+
+ The `RDF.Query.BGP` struct build with this:
+
+ RDF.Query.path [EX.S, EX.p, RDFS.label, :name?]
+
+ is the same as the one build by this `bgp/1` call:
+
+ RDF.Query.bgp [
+ {EX.S, EX.p, :_o},
+ {:_o, RDFS.label, :name?},
+ ]
+
+ """
defdelegate path(query, opts \\ []), to: Builder, as: :path!
end
diff --git a/lib/rdf/query/bgp.ex b/lib/rdf/query/bgp.ex
index c28cbc6..1104d13 100644
--- a/lib/rdf/query/bgp.ex
+++ b/lib/rdf/query/bgp.ex
@@ -1,4 +1,11 @@
defmodule RDF.Query.BGP do
+ @moduledoc """
+ A struct for Basic Graph Pattern queries.
+
+ See `RDF.Query` and its functions on how to construct this query struct and
+ apply it on `RDF.Graph`s.
+ """
+
@enforce_keys [:triple_patterns]
defstruct [:triple_patterns]
@@ -13,10 +20,12 @@ defmodule RDF.Query.BGP do
@type t :: %__MODULE__{triple_patterns: triple_patterns}
-
@doc """
Return a list of all variables in a BGP.
"""
+ @spec variables(any) :: [atom]
+ def variables(bgp)
+
def variables(%__MODULE__{triple_patterns: triple_patterns}), do: variables(triple_patterns)
def variables(triple_patterns) when is_list(triple_patterns) do
diff --git a/lib/rdf/query/bgp/matcher.ex b/lib/rdf/query/bgp/matcher.ex
index 82e69e9..63040d3 100644
--- a/lib/rdf/query/bgp/matcher.ex
+++ b/lib/rdf/query/bgp/matcher.ex
@@ -1,5 +1,5 @@
defmodule RDF.Query.BGP.Matcher do
- @moduledoc """
+ @moduledoc !"""
An interface for various BGP matching algorithm implementations.
"""
diff --git a/lib/rdf/query/bgp/simple.ex b/lib/rdf/query/bgp/simple.ex
index f73db86..0cf9fad 100644
--- a/lib/rdf/query/bgp/simple.ex
+++ b/lib/rdf/query/bgp/simple.ex
@@ -1,4 +1,6 @@
defmodule RDF.Query.BGP.Simple do
+ @moduledoc false
+
@behaviour RDF.Query.BGP.Matcher
alias RDF.Query.BGP
diff --git a/lib/rdf/query/bgp/stream.ex b/lib/rdf/query/bgp/stream.ex
index 41959cf..6b2c807 100644
--- a/lib/rdf/query/bgp/stream.ex
+++ b/lib/rdf/query/bgp/stream.ex
@@ -1,4 +1,6 @@
defmodule RDF.Query.BGP.Stream do
+ @moduledoc false
+
@behaviour RDF.Query.BGP.Matcher
alias RDF.Query.BGP
diff --git a/lib/rdf/query/builder.ex b/lib/rdf/query/builder.ex
index 73a2ad5..ee6c584 100644
--- a/lib/rdf/query/builder.ex
+++ b/lib/rdf/query/builder.ex
@@ -1,4 +1,6 @@
defmodule RDF.Query.Builder do
+ @moduledoc false
+
alias RDF.Query.BGP
alias RDF.{IRI, BlankNode, Literal, Namespace}
import RDF.Utils.Guards
diff --git a/test/support/rdf_case.ex b/test/support/rdf_case.ex
index 8c79dac..3612ceb 100644
--- a/test/support/rdf_case.ex
+++ b/test/support/rdf_case.ex
@@ -6,13 +6,17 @@ defmodule RDF.Test.Case do
base_iri: "http://example.com/",
terms: [], strict: false
+ defvocab FOAF,
+ base_iri: "http://xmlns.com/foaf/0.1/",
+ terms: [], strict: false
+
alias RDF.{Dataset, Graph, Description, IRI}
import RDF, only: [iri: 1]
using do
quote do
alias RDF.{Dataset, Graph, Description, IRI, XSD}
- alias unquote(__MODULE__).EX
+ alias unquote(__MODULE__).{EX, FOAF}
import RDF, only: [iri: 1, literal: 1, bnode: 1]
import unquote(__MODULE__)
@@ -20,6 +24,7 @@ defmodule RDF.Test.Case do
import RDF.Sigils
@compile {:no_warn_undefined, RDF.Test.Case.EX}
+ @compile {:no_warn_undefined, RDF.Test.Case.FOAF}
end
end
diff --git a/test/unit/query/query_test.exs b/test/unit/query/query_test.exs
index eab8e17..66505b1 100644
--- a/test/unit/query/query_test.exs
+++ b/test/unit/query/query_test.exs
@@ -1,27 +1,55 @@
defmodule RDF.QueryTest do
use RDF.Query.Test.Case
- @example_graph Graph.new([
- {EX.s1, EX.p1, EX.o1},
- {EX.s1, EX.p2, EX.o2},
- {EX.s3, EX.p3, EX.o2}
- ])
+ doctest RDF.Query
- @example_query [{:s?, :p?, EX.o2}]
+ @example_graph """
+ @prefix foaf: .
+ @prefix ex: .
+
+ ex:Outlaw
+ foaf:name "Johnny Lee Outlaw" ;
+ foaf:mbox .
+
+ ex:Goodguy
+ foaf:name "Peter Goodguy" ;
+ foaf:mbox ;
+ foaf:friend ex:Outlaw .
+ """ |> RDF.Turtle.read_string!()
+
+ def example_graph, do: @example_graph
+
+ @example_query [{:s?, FOAF.name, ~L"Peter Goodguy"}]
test "execute/2" do
assert RDF.Query.execute(RDF.Query.bgp(@example_query), @example_graph) ==
- BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph)
+ {:ok, BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph)}
assert RDF.Query.execute(@example_query, @example_graph) ==
+ {:ok, BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph)}
+ end
+
+ test "execute!/2" do
+ assert RDF.Query.execute!(RDF.Query.bgp(@example_query), @example_graph) ==
+ BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph)
+
+ assert RDF.Query.execute!(@example_query, @example_graph) ==
BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph)
end
test "stream/2" do
assert RDF.Query.stream(RDF.Query.bgp(@example_query), @example_graph) ==
- BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph)
+ {:ok, BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph)}
assert RDF.Query.stream(@example_query, @example_graph) ==
+ {:ok, BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph)}
+ end
+
+ test "stream!/2" do
+ assert RDF.Query.stream!(RDF.Query.bgp(@example_query), @example_graph) ==
+ BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph)
+
+ assert RDF.Query.stream!(@example_query, @example_graph) ==
BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph)
end
end