# RDF data structures

RDF.ex provides various data structures for collections of statements:

  • RDF.Description: a collection of triples about the same subject
  • RDF.Graph: a named collection of statements
  • RDF.Dataset: a named collection of graphs, i.e. a collection of statements from different graphs; it may have multiple named graphs and at most one unnamed ("default") graph

All of these structures have similar sets of functions and implement Elixirs Enumerable and Collectable protocol, Elixirs Access behaviour and the RDF.Data protocol of RDF.ex.

# Construction

The new function of these data structures create new instances of the struct. RDF.Description.new requires at least an IRI or blank node for the subject, while RDF.Graph.new and RDF.Dataset.new take an optional IRI for the name of the graph or dataset via the name option.

empty_description = RDF.Description.new(EX.Subject)

empty_unnamed_graph = RDF.Graph.new
empty_named_graph   = RDF.Graph.new(name: EX.Graph)

empty_unnamed_dataset = RDF.Dataset.new
empty_named_dataset   = RDF.Dataset.new(name: EX.Dataset)

As you can see, qualified terms from a vocabulary namespace can be given instead of an IRI and will be resolved automatically. This applies to all of the functions discussed below.

The new functions can be called more shortly with the respective delegator functions RDF.description, RDF.graph and RDF.dataset.

The new functions also take optional initial data, which can be provided in various forms. Basically it takes the given data and hands it to the add function with the newly created struct. One way support by the new constructor of all three data structures is with the :init option.

description = RDF.Description.new(EX.Subject, init: {EX.predicate, EX.Object})

unnamed_graph = RDF.Graph.new(init: {EX.Subject, EX.predicate, EX.Object})
named_graph   = RDF.Graph.new(name: EX.Graph, init: {EX.Subject, EX.predicate, EX.Object})

unnamed_dataset = RDF.Dataset.new(init: {EX.Subject, EX.predicate, EX.Object, EX.Graph})
named_dataset   = RDF.Dataset.new(name: EX.Dataset, init: {EX.Subject, EX.predicate, EX.Object})

The value of the :init option can also be a function (without args) which can return the data to be initialized in any form discussed in the next subsection.

RDF.Graph.new(name: EX.Graph, init: &initializer_function/0)

On the new constructors of RDF.Graph and RDF.Dataset the data can also be passed directly in order to support its use with the pipeline operator.

{EX.Subject, EX.predicate, EX.Object}
|> RDF.Graph.new(name: EX.Graph)

{EX.Subject, EX.predicate, EX.Object, EX.Graph}
|> RDF.Dataset.new()

This feature cannot be supported on RDF.Description/new/2 since the subject is mandatory. But this shouldn't be such a big limitation, since often times RDF.Descriptions are created with the Description DSL introduced here.

DANGER

This form of passing the input data directly has one caveat: the input form of grouping multiple predicate-object pairs for a subject given as a vocabulary namespace term is not supported as it is indistinguishable from Keyword opts, eg. in this example the input won't be recognized correctly:

[{EX.Subject, [{EX.p1(), EX.O1}, {EX.p2(), EX.O2}]}]
|> RDF.Graph.new()

For this reason the usage of the :init option variant is the recommended way to populate the data structures on construction. Use the direct passing variant only when you want to call the constructors in a pipeline and are sure that input in this form won't occur.

A workaround if you really want to use this variant and can't exclude this form is to explicitly pass options:

[{EX.Subject, [{EX.p1(), EX.O1}, {EX.p2(), EX.O2}]}] 
|> RDF.Graph.new(name: EX.Graph)

[{EX.Subject, [{EX.p1(), EX.O1}, {EX.p2(), EX.O2}]}] 
|> RDF.Graph.new([])

# Input forms

In the last section we already encountered a couple of ways of how RDF statements can be provided as input to the new/2 functions. There are more ways and all of them are commonly supported on all functions taking input data. Let's look at them one by one.

Most basically, single triple and quad tuples can be provided. As with all supported forms, the elements must not be RDF terms directly, as long they are coercible as discussed in the previous section about Statements.

{EX.S, EX.p, EX.O}
{EX.S, EX.p, "string"}
{EX.S, EX.p, 42}

{EX.S, EX.p, EX.O, EX.Graph}

On the object position a list of objects can provided for multiple statements to the same subject and predicate.

{EX.S, EX.p, [EX.O, "string", 42]}

Multiple predicate-object pairs to the same subject can be given via a two-element tuple of a subject and a list of predicate-object pairs. The object can also be a list in this form.

{EX.S, [
    {EX.p2, EX.O1},
    {EX.p2, ["string", 42]},
  ]
}

The input data can also be given as a map.

%{EX.S => %{
    EX.p2 => EX.O1,
    EX.p2 => ["string", 42]
  }
}

This nested map form for RDF statements however is only supported on RDF.Graph and RDF.Dataset functions. The RDF.Description operating only on RDF statements about the same subject supports the map form only with the inner map with the predicate-object pairs.

%{
  EX.p2 => EX.O1,
  EX.p2 => ["string", 42]
}

The RDF.Description functions also supports the initially mentioned tuple forms, but the subject must match the subject of the description. Additionally however they support also two-element tuples with just the predicate and object(s).

Naturally it's also possible to provide the statements in the RDF data structures themselves. However, for any of RDF data structure only the respective RDF data structure itself and the smaller ones are supported:

  • RDF.Dataset functions with input data support all three RDF data structures.
  • RDF.Graph functions can only handle RDF.Graphs and RDF.Descriptions.
  • RDF.Description only works with RDF.Descriptions themself. Unlike the other forms for input data however, the subject of an input description does not have to match the subject of the description on which the function is applied.

WARNING

One could expect that RDF.Dataset would support an additional nesting with graph names at the outer level, but this is not the case. Supporting this would have been relatively costly, since it would require always checking the depth of the given input. But it's also actually not needed for most cases, since the RDF.Dataset functions allow addressing the graph separately via the graph option.

The input can be further shortened with the use of RDF.PropertyMaps, which are bidirectional mappings from atoms to IRIs of properties. They can be created from keyword lists or maps of terms to IRIs via the new/1 function or its alias function RDF.property_map/1.

iex> RDF.PropertyMap.new(%{type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"})
RDF.PropertyMap.new(%{:type => ~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>})

iex> RDF.property_map(foo: EX.foo, bar: EX.Bar)
RDF.PropertyMap.new(%{
  :bar => ~I<http://example.com/Bar>,
  :foo => ~I<http://example.com/foo>
})

PropertyMaps can also be created from strict vocabulary namespaces, where term mappings are added for lowercased terms.

iex> RDF.property_map(RDFS)
RDF.PropertyMap.new(%{
  :comment => ~I<http://www.w3.org/2000/01/rdf-schema#comment>,
  :domain => ~I<http://www.w3.org/2000/01/rdf-schema#domain>,
  :isDefinedBy => ~I<http://www.w3.org/2000/01/rdf-schema#isDefinedBy>,
  :label => ~I<http://www.w3.org/2000/01/rdf-schema#label>,
  :member => ~I<http://www.w3.org/2000/01/rdf-schema#member>,
  :range => ~I<http://www.w3.org/2000/01/rdf-schema#range>,
  :seeAlso => ~I<http://www.w3.org/2000/01/rdf-schema#seeAlso>,
  :subClassOf => ~I<http://www.w3.org/2000/01/rdf-schema#subClassOf>,
  :subPropertyOf => ~I<http://www.w3.org/2000/01/rdf-schema#subPropertyOf>
})

All functions accepting input data support a :context option for which you can either pass a RDF.PropertyMap directly or one of the values from which a RDF.PropertyMap can be created implicitly. If the :context is defined you can use the atoms for the properties in any of the input forms.

property_map = RDF.property_map(foo: EX.foo)
RDF.Description.add(description, [foo: "bar"], context: property_map)

RDF.Graph.add(graph, %{EX.S => %{subClassOf: EX.Class}}, context: RDFS)

Finally, lists of all the mentioned forms are accepted as input on the RDF data structures.

[
  {EX.S1, EX.p1, O2},
  {EX.S2, [{EX.p2, ["string", 42]}]},
  %{EX.S3 => %{p3: EX.O3}},
  EX.p4(EX.S4, EX.O4)
]

# Adding statements

RDF statements can be added to the data structures with various functions, all which support all of the input forms introduced in the last section. Let's first define some example data structures on which we can exemplify the differences of the different functions.

iex> description = EX.S |> EX.p1(EX.O1) |> EX.p2(EX.O2) 
#RDF.Description<
  <http://example.com/S>
      <http://example.com/p1> <http://example.com/O1> ;
      <http://example.com/p2> <http://example.com/O2> .
>

iex> graph = RDF.graph(description, prefixes: [ex: EX])
#RDF.Graph<name: nil
  @prefix ex: <http://example.com/> .

  ex:S
      ex:p1 ex:O1 ;
      ex:p2 ex:O2 .
>

The add/3 functions of the RDF data structures merge the given statements with the existing ones.

iex> RDF.Description.add(description, {EX.S, EX.p1, EX.New})
#RDF.Description<
  <http://example.com/S>
      <http://example.com/p1> <http://example.com/New>, <http://example.com/O1> ;
      <http://example.com/p2> <http://example.com/O2> .
>
iex> RDF.Graph.add(graph, %{EX.S => %{p1: EX.O}}, context: %{p1: EX.p1})
#RDF.Graph<name: nil
  @prefix ex: <http://example.com/> .

  ex:S
      ex:p1 ex:O, ex:O1 ;
      ex:p2 ex:O2 .
>

The put/3 functions on the other hand overwrite existing statements, but behave differently in their overwriting behavior depending on the respective RDF data structure:

  • RDF.Description.put/3 overwrites only statements with same subject and predicate.
  • RDF.Graph.put/3 and RDF.Dataset.put/3 both overwrite all statements with same subject.
iex> RDF.Description.put(description, {EX.S, EX.p1, EX.New})
#RDF.Description<
  <http://example.com/S>
      <http://example.com/p1> <http://example.com/New> ;
      <http://example.com/p2> <http://example.com/O2> .
>
iex> RDF.Graph.put(graph, %{EX.S => %{p1: EX.New}}, context: %{p1: EX.p1})
#RDF.Graph<name: nil
  @prefix ex: <http://example.com/> .

  ex:S
      ex:p1 ex:New .
>

If you want to add statements to an RDF.Graph or RDF.Dataset with the same overwrite behavior as RDF.Description.put/3, i.e. only overwrite the statements with the same subject and predicate, you can use the RDF.Graph.put_properties/3 and RDF.Dataset.put_properties/3 functions.

iex> RDF.Graph.put_properties(graph, %{EX.S => %{p1: EX.New}}, context: %{p1: EX.p1})
#RDF.Graph<name: nil
  @prefix ex: <http://example.com/> .

  ex:S
      ex:p1 ex:New ;
      ex:p2 ex:O2 .
>

For adding a graph to a RDF.Dataset overwriting a previous graph, the RDF.Dataset.put_graph/3 function can be used.

As mentioned in the last section, when the subject of a statement doesn't match the subject of a description, RDF.Description.add/3 ignores it and is a no-op. However, when given a RDF.Description to add, it ignores its subject and just adds its property-value pairs, because this is a common use case when merging the descriptions of differently named resources.

iex> description = RDF.description(EX.S, init: {EX.p, EX.O1})
#RDF.Description<
  <http://example.com/S>
      <http://example.com/p> <http://example.com/O1> .
>
iex> RDF.Description.add(description, {EX.Other, EX.p, EX.O2})
#RDF.Description<
  <http://example.com/S>
      <http://example.com/p> <http://example.com/O1> .
>
iex> RDF.Description.add(description, RDF.description(EX.Other, init: {EX.p, EX.O2}))
#RDF.Description<
  <http://example.com/S>
      <http://example.com/p> <http://example.com/O1>, <http://example.com/O2> .
>

Since put/3 is a destructive operation, RDF.Description.put/3 does not replicate the behavior of RDF.Description.add/3 to ignore the subject of descriptions. If you really want to overwrite the statements of a description with the ones from another description with put/3 you'll have to explicitly change the subject of the input description with RDF.Description.change_subject/2.

iex> other_description = RDF.description(EX.Other, init: {EX.p, EX.O2})
#RDF.Description<
  <http://example.com/Other>
      <http://example.com/p> <http://example.com/O2> .
>
iex> RDF.Description.put(description, other_description)
#RDF.Description<
  <http://example.com/S>
      <http://example.com/p> <http://example.com/O1> .
>
iex> RDF.Description.put(description, 
...>   RDF.Description.change_subject(other_description, description.subject))
#RDF.Description<
  <http://example.com/S>
      <http://example.com/p> <http://example.com/O2> .
>

As most of the functions of RDF.Dataset the functions for adding statements have essentially two modes in which they operate:

  1. When called with a graph name via the :graph option, the function call is essentially delegated to the respective graph and the implementation of this function on RDF.Graph, which might even mean that input data from different graphs (eg. quads or RDF.Graphs with different graph names) becomes aggregated and get redirected to the specified graph.
  2. Without a :graph option the all quads or RDF.Graphs in the input are directed to respective graphs.
iex> dataset = RDF.dataset([
...>   (EX.S1 |> EX.p1(EX.O1)),
...>   {EX.S2, EX.p2, EX.O2, EX.Graph}
...> ])
%RDF.Dataset{name: nil, graph_names: [nil, ~I<http://example.com/Graph>]}

iex> RDF.Dataset.default_graph(dataset)
#RDF.Graph<name: nil
  <http://example.com/S1>
      <http://example.com/p1> <http://example.com/O1> .
>
iex> RDF.Dataset.graph(dataset, EX.Graph)
#RDF.Graph<name: ~I<http://example.com/Graph>
  <http://example.com/S2>
      <http://example.com/p2> <http://example.com/O2> .
>
iex> RDF.Dataset.add(dataset, [
...>   {EX.S1, EX.p1, "new"},
...>   {EX.S2, EX.p2, "new", EX.Graph}
...> ]) |> RDF.Dataset.graphs()
[#RDF.Graph<name: nil
  <http://example.com/S1>
      <http://example.com/p1> "new", <http://example.com/O1> .
>,
 #RDF.Graph<name: ~I<http://example.com/Graph>
  <http://example.com/S2>
      <http://example.com/p2> "new", <http://example.com/O2> .
>]
iex> RDF.Dataset.add(dataset, [
...>   {EX.S1, EX.p1, "new"},
...>   {EX.S2, EX.p2, "new", EX.Graph}
...> ], graph: nil) |> RDF.Dataset.graphs()
[#RDF.Graph<name: nil
  <http://example.com/S1>
      <http://example.com/p1> "new", <http://example.com/O1> .

  <http://example.com/S2>
      <http://example.com/p2> "new" .
>,
 #RDF.Graph<name: ~I<http://example.com/Graph>
  <http://example.com/S2>
      <http://example.com/p2> <http://example.com/O2> .
>]

Unlike the add function, which always returns the same data structure as the data structure to which the addition happens, which possible means ignoring some input statements (eg. when the subject of a statement doesn't match the description subject) or reinterpreting some parts of the input statement (eg. ignoring the subject of another description), the merge function of the RDF.Data protocol implemented by all three data structures will always add all of the input statements and possibly creates another type of data structure. For example, merging two RDF.Descriptions with different subjects results in a RDF.Graph or adding a quad to a RDF.Graph with a different name than the quad’s graph context results in a RDF.Dataset.

RDF.Description.new(EX.S1, init: {EX.p, EX.O}) 
|> RDF.Data.merge(RDF.Description.new(EX.S2, init: {EX.p, EX.O})) # returns an unnamed RDF.Graph
|> RDF.Data.merge(RDF.Graph.new({EX.S2, EX.p, EX.O2}, name: EX.Graph)) # returns a RDF.Dataset

Finally, the update/4 functions allows updating of specified elements in the RDF data structures with a custom update function based on the previous values.

  • RDF.Description.update/4 updates the objects of the given predicate with the results of the update function which receives the previous objects and can either return a single or multiple new objects to be set or nil if all statements with this predicate should be deleted.
  • RDF.Graph.update/4 updates the description of the given subject with the results of the update function which receives the previous RDF.Description and can either return all supported input formats for RDF.Descriptions or nil if the description should be deleted.
  • RDF.Dataset.update/4 updates the graph of the given name with the results of the update function which receives the previous RDF.Graph and can either return all supported input formats for RDF.Graphs or nil if the graph should be deleted.
iex> RDF.description(EX.S, init: {EX.p, 42})
...> |> RDF.Description.update(EX.p, fn [object] -> 
...>      XSD.Integer.value(object) + 1 
...> end)
#RDF.Description<
  <http://example.com/S>
      <http://example.com/p> 43 .
>
iex> RDF.graph({EX.S, EX.p, EX.O})
...> |> RDF.Graph.update(EX.S,
...>      fn description -> Description.add(description, {EX.p, EX.O2})
...>    end)
#RDF.Graph<name: nil
  <http://example.com/S>
      <http://example.com/p> <http://example.com/O>, <http://example.com/O2> .
>

The optional third argument allows to specify a default value which should be set in case no value to be updated exist for the given element.

iex> RDF.description(EX.S) 
...> |> RDF.Description.update(EX.p, EX.O, fn _ -> EX.O2 end)
#RDF.Description<
  <http://example.com/S>
      <http://example.com/p> <http://example.com/O> .
>

# Accessing the content

All three RDF data structures implement the Enumerable protocol over the set of contained statements. In the case of RDF.Description and RDF.Graph as a set of triples and in case of RDF.Dataset as a set of quads. This means you can use all Enum functions over the contained statements as tuples.

RDF.Description.new(EX.S1, {EX.p, [EX.O1, EX.O2]})
|> Enum.each(&IO.inspect/1)

The RDF.Data protocol offers various functions to access the contents of RDF data structures:

  • RDF.Data.subjects/1 returns the set of all subject resources
  • RDF.Data.predicates/1 returns the set of all used properties
  • RDF.Data.objects/1 returns the set of all resources on the object position of statements - literals not included
  • RDF.Data.resources/1 returns the set of all used resources at any position in the contained RDF statements
  • RDF.Data.description/2 returns all statements from a data structure about the given resource as a RDF.Description. It will be empty if no such statements exist. On a RDF.Dataset it will aggregate the statements about the resource from all graphs.
  • RDF.Data.descriptions/1 returns all RDF.Descriptions within a data structure (possible aggregated in the case of a RDF.Dataset)
  • RDF.Data.statements/1 returns a list of all contained RDF statements

The get/3 functions return individual elements of a RDF data structure:

  • RDF.Description.get/3 returns the list of all object values for a given property
  • RDF.Graph.get/3 returns the RDF.Description for a given subject resource
  • RDF.Dataset.get/3 returns the RDF.Graph with the given graph name

All of these get/3 functions return nil or the optionally given default value, when the given element cannot be found.

iex> RDF.Description.new(EX.S1, init: {EX.p, [EX.O1, EX.O2]})
...> |> RDF.Description.get(EX.p)
[~I<http://example.com/O1>, ~I<http://example.com/O2>]

iex> RDF.Graph.new({EX.S1, EX.p, [EX.O1, EX.O2]})
...> |> RDF.Graph.get(EX.p2, :not_found)
:not_found

You can get a single object value for a given predicate in a RDF.Description with the RDF.Description.first/2 function:

iex> RDF.Description.new(EX.S1, init: {EX.p, EX.O1})
...> |> RDF.Description.first(EX.p)
~I<http://example.com/O1>

Since all three RDF data structures implement the Access behaviour, you can also use data[key] syntax, which basically just calls the respective get function.

iex> description[EX.p]
[~I<http://example.com/O1>, ~I<http://example.com/O2>]

iex> graph[EX.p2] 
nil

Also, the familiar fetch/2 function of the Access behaviour, as a variant of get/3 which returns ok tuples, is available on all RDF data structures.

iex> RDF.Description.new(EX.S1, init: {EX.p, [EX.O1, EX.O2]})
...> |> RDF.Description.fetch(EX.p)
{:ok, [~I<http://example.com/O1>, ~I<http://example.com/O2>]}

iex> RDF.Graph.new({EX.S1, EX.p, [EX.O1, EX.O2]})
...> |> RDF.Graph.fetch(EX.p2)
:error

Finally, the function for a property on a RDF.Vocabulary.Namespace can be used to access the respective objects from a RDF.Description. By passing it a RDF.Description it can be used as a shortcut for the RDF.Description.get/2 function.

iex> RDF.Description.new(EX.S1, init: {EX.p, [EX.O1, EX.O2]})
...> |> EX.p()
[~I<http://example.com/O1>, ~I<http://example.com/O2>]

RDF.Dataset also provides the following functions to access individual graphs:

  • RDF.Dataset.graphs/1 returns the list of all the graphs of the dataset
  • RDF.Dataset.default_graph/1 returns the default graph of the dataset
  • RDF.Dataset.graph/2 returns the graph of the dataset with the given name

# Querying graphs

The SPARQL.ex package allows you to execute SPARQL queries against RDF.ex graphs. It's still very limited at the moment. See the SPARQL.ex guide for more information. But you can also do basic graph queries within RDF.ex directly with the RDF.Graph.query/3 or RDF.Graph.query_stream/3 functions.

These functions take a graph and a basic graph pattern (BGP) consisting of some RDF triples with variables, which are written as atoms ending with a question mark. The RDF triples with the variables can again provided in any of the forms for input data introduced above. This query for example returns all triples about resources which have a rdfs:label "foo":

RDF.Graph.query(graph, [
    {:s?, RDFS.label, "foo"},
    {:s?, :p?, :o?}
  ])

The results are returned in an :ok tuple (or directly with RDF.Graph.query!/3) as a list of solutions for the variables. The solutions are maps where the keys are the variables without the ending question mark.

{:ok, [
  %{
    s: ~I<http://example.com/subject>,
    p: ~I<http://www.w3.org/2000/01/rdf-schema#label>,
    o: ~L"foo"
  },
  # ...
]}

Here's another example of a query pattern demonstrating one of the other forms and that also RDF.PropertyMaps can be used with the :context opt:

RDF.Graph.query(graph, %{
  s?: %{
    p1: :o?,
    p2: [42, 3.14, true]
  },
  o?: %{p3: ["foo", "bar"]}
}, context: %{
  p1: EX.p1,
  p2: EX.p2,
  p3: EX.p3  
})

The rdf:type property can be written shortly with the atom :a and blank nodes can be written more shortly in the query pattern with atoms starting with an underscore.

TIP

Blank nodes in query patterns have the interesting property to behave like variables which don't show up in the results. So they can be quite convenient for intermediary variables.

iex> RDF.Graph.query(graph, %{
...>   _s: %{
...>     :a => EX.Class,
...>     RDFS.label => :name?
...> }})
{:ok, [
  %{name: ~L"foo"},
  # ...
]}

If you want store a basic graph pattern query in a variable for reuse or want to build your own query builder function you can use the RDF.Query.bgp/2 function. This function is used implicitly by RDF.Graph.query/3 to build RDF.Query.BGP structs from lists (or tuples for single triple patterns).

query = 
  RDF.Query.bgp %{
   s?: %{
     :a => EX.Class,
     RDFS.label => :name?
  }}

RDF.Graph.query(graph, query)

The RDF.Query module also offers another handy builder function: RDF.Query.path/2 creates a basic graph pattern for a list representing a path through the graph.

path = RDF.Query.path([EX.S, EX.p, RDFS.label, :name?])
RDF.Graph.query(graph, path)

This is similar to the following query:

RDF.Graph.query(graph, [
    {EX.S, EX.p, :_o},
    {:_o, RDFS.label, :name?},
  ])

If you want the path builder function to generate variables (instead of blank nodes) for the path element objects in order to get them in the results, you can say so with the with_elements: true option.

Instead of executing the query to get the results directly, you can also request the results as a stream with the RDF.Graph.query_stream/3 and RDF.Graph.query_stream!/3 functions.

# Deleting statements

Statements can be deleted in two slightly different ways. One way is to use the delete/3 function of the respective data structure. It accepts all forms for specifying statements as input introduced above and removes the found triples.

iex> RDF.Description.new(EX.S1, init: {EX.p, [EX.O1, EX.O2]})
...> |> RDF.Description.delete({EX.S1, EX.p, EX.O1})
#RDF.Description<
  <http://example.com/S1>
      <http://example.com/p> <http://example.com/O2> .
>

Another way to delete statements is the delete/3 function of the RDF.Data protocol. The only difference to delete functions on the data structures directly is how it handles the deletion of a RDF.Description from another RDF.Description or RDF.Graph from another RDF.Graph. While the dedicated RDF data structure function ignores the description subject or graph name and removes the statements even when they don't match, RDF.Data.delete/3 only deletes when the description’s subject respective graph name matches.

iex> RDF.Description.new(EX.S1, init: {EX.p, [EX.O1, EX.O2]})
...> |> RDF.Description.delete(RDF.Description.new(EX.S2, init: {EX.p, EX.O1}))
#RDF.Description<
  <http://example.com/S1>
      <http://example.com/p> <http://example.com/O2> .
>

iex> RDF.Description.new(EX.S1, init: {EX.p, [EX.O1, EX.O2]})
...> |> RDF.Data.delete(RDF.Description.new(EX.S2, init: {EX.p, EX.O1}))
#RDF.Description<
  <http://example.com/S1>
      <http://example.com/p> <http://example.com/O1>, <http://example.com/O2> .
>

Beyond that, there are:

  • RDF.Description.delete_predicates/2 which deletes all statements with the given property from a RDF.Description,
  • RDF.Graph.delete_predications/2 which deletes all statements with the given subject and predicate from a RDF.Graph,
  • RDF.Graph.delete_subjects/2 which deletes all statements with the given subject resource from a RDF.Graph,
  • RDF.Dataset.delete_graph/2 which deletes all graphs with the given graph name from a RDF.Dataset and
  • RDF.Dataset.delete_default_graph/1 which deletes the default graph of a RDF.Dataset.

The intersection/2 function of RDF.Description, RDF.Graph and RDF.Dataset can be used create a graph intersection. The first argument must match the base data structure, while the RDF data on second argument can be given in any form a RDF.Graph resp. RDF.Dataset can constructed from.

iex> RDF.Graph.new({EX.S1, EX.p(), [EX.O1, EX.O2]})
...> |> RDF.Graph.intersection({EX.S1, EX.p(), [EX.O2, EX.O3]})
#RDF.Graph<name: nil
  @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
  @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

  <http://example.com/S1>
      <http://example.com/p> <http://example.com/O2> .
>

# Equality

RDF data structures can be compared for equality with the equal?/2 function of the respective data structure. You should these instead of comparisons with ==, because the data structures might contain fields which are not relevant for equality. For example the defined prefixes (see here for more on that) are ignored for this comparison.

iex> d = RDF.description(EX.S, init: {EX.p, EX.O})
iex> RDF.Description.equal?(d, d)
true
iex> RDF.Graph.equal?(
...>   RDF.graph(d, prefixes: %{ex: EX}), 
...>   RDF.graph(d, prefixes: %{ex: EX, xsd: XSD}))
true

...> RDF.graph(d, prefixes: %{ex: EX}) ==
...>   RDF.graph(d, prefixes: %{ex: EX, xsd: XSD}))
false

You can also compare different types of RDF data structures with the RDF.Data.equal?/2 function, which takes just the raw data into account.

iex> RDF.Data.equal?(d, RDF.graph(d))
true

As opposed to RDF.Graph.equal?/2 the RDF.Data.equal?/2 function also doesn't consider the graph name when comparing RDF.Graphs.

iex> RDF.Graph.equal?(RDF.graph(d), RDF.graph(d, name: EX.Graph))
false

iex> RDF.Data.equal?(RDF.graph(d), RDF.graph(d, name: EX.Graph))
true

If you want to check whether two graphs or datasets are the same, regardless of the concrete names of the blank nodes they contain (because they do not matter for the semantics of a graph), you can do this with the functions RDF.Graph.isomorphic?/2 resp. RDF.Dataset.isomorphic?/2.

iex> RDF.Graph.new([{~B<foo>, EX.p(), ~B<bar>}, {~B<bar>, EX.p(), 42}])
...> |> RDF.Graph.isomorphic?(
...>      RDF.Graph.new([{~B<b1>, EX.p(), ~B<b2>}, {~B<b2>, EX.p(), 42}]))
true

iex> RDF.Graph.new([{~B<foo>, EX.p(), ~B<bar>}, {~B<bar>, EX.p(), 42}])
...> |> RDF.Graph.isomorphic?(
...>      RDF.Graph.new([{~B<b1>, EX.p(), ~B<b2>}, {~B<b3>, EX.p(), 42}]))
false

# Canonicalization

An RDF.Graph or RDF.Dataset can be canonicalized with the standardized RDF Dataset Canonicalization algorithm (opens new window) with the functions RDF.Graph.canonicalize/1 resp. RDF.Dataset.canonicalize/1.

iex> RDF.Graph.new([{~B<foo>, EX.p(), ~B<bar>}, {~B<bar>, EX.p(), ~B<foo>}])
...> |> RDF.Graph.canonicalize()
#RDF.Graph<name: nil
  @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
  @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

  _:c14n0
      <http://example.com/p> _:c14n1 .

  _:c14n1
      <http://example.com/p> _:c14n0 .
>

The functions RDF.Graph.canonical_hash/2 and RDF.Dataset.canonical_hash/2 can be used get a hash of the N-Quads serialization of a RDF.Graph resp. RDF.Dataset in this RDF dataset canonicalized form.

iex> RDF.Graph.new([{~B<foo>, EX.p(), ~B<bar>}, {~B<bar>, EX.p(), ~B<foo>}])
...> |> RDF.Graph.canonical_hash()
"053688e09a20a49acc3e1a5e6403c827b817eef9e4c90bfd71f2360e2a6446aa"

iex> RDF.Dataset.new([{~B<foo2>, EX.p(), ~B<bar2>}, {~B<bar2>, EX.p(), ~B<foo2>}])
...> |> RDF.Dataset.canonical_hash()
"053688e09a20a49acc3e1a5e6403c827b817eef9e4c90bfd71f2360e2a6446aa"

By default SHA-256 is used for hashing, which can be changed, however, with the :hash_algorithm keyword option.

Last Updated: 3/18/2024, 12:31:40 PM