Project

grel

0.0
No commit activity in last 3 years
No release in over 3 years
idem
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

 Project Readme

#GRel

A small ruby library that makes it easy to store and query ruby objects stored in the RDF database Stardog.

RDoc documentation for the project can be found here.

Installation

The library is available as a Ruby Gem:

gem install grel

Initialization

    require 'grel'
 
    include GRel

    g = graph.with_db(DB)

Linking Data

GRel works with graphs of linked Ruby hashes. Evey hash in the graph will be identified by a special @id property. The value for the @id can be assigned when describing the object or it will be auto-generated by GRel if none is assigned. To link two hashes by a property, the linked object with its corresponding @id value can be nested in the parent object or the value for the @id using the string literal "@id(value)" can be assigned directly.

This two queries are equivalent:

    g.store(:@id     => 'abs',
            :name    => 'Abhinay',
            :citizen => {:@id  => 'in',
                         :name => 'India'})
    # abs -> citizen -> in
    g.store(:@id     => 'abs',
            :name    => 'Abhinay',
            :citizen => '@id(in)')
    # abs -> citizen -> in

If a string literal is used but no '@id(value)' syntax is used not object will be linked but the literal value of the string will be assigned:

    g.store(:@id     => 'abs',
            :name    => 'Abhinay',
            :citizen => 'in')
    # abs has a citizen property with the literal value 'in'

In the description of an object, the value for the *@id' property can be specified using the @id(value) syntax or directly with the value.

This two queries are equivalent:

    g.store(:@id     => 'abs',
            :name    => 'Abhinay')
    g.store(:@id     => '@id(abs)',
            :name    => 'Abhinay')

Data loading:

Data is loaded as arrays of nested hashes. Two special properties :@id and :@type are used to identify the identity of the node and its types. Identity must be unique, type can be multiple. If no :@id property is provided for an object, an identity will be generated.

    g.store(:name    => 'Abhinay',
            :surname => 'Mehta',
            :@type   => :Developer,
            :@id     => 'abs').

      store(:name    => 'Tom',
            :surname => 'Hall',
            :@type   => :Developer,
            :@id     => 'thattommyhall').

      store(:name       => 'India',
            :@type      => :Country,
            :population => 1200,
            :capital    => 'New Delhi',
            :@id        => 'in').

      store(:name       => 'United Kingdom',
            :@type      => :Country,
            :population => 62,
            :capital    => 'London',
            :@id        => 'uk').

      # Storing relationships
      store(:@id     => 'abs',
            :citizen => '@id(in)').

      store(:@id     => 'thattommyhall',
            :citizen => '@id(uk)').

      # Storing nested objects
      store(:@id     => 'antoniogarrote',
            :name    => 'Antonio',
            :@type   => :Developer,
            :citizen => {:name       => 'Spain',
                         :@type      => :Country,
                         :population => 43,
                         :capital    => 'Madrid',
                         :@id        => 'es'})             

Querying:

Queries can be performed using the where and passing a hash with a pattern for the nodes to be retrieved, and chaining it with the all methods.

    g.where(:@type => :Developer).all 
    # [ {:@id => '@id(abs)', :name => 'Abhinay', :citizen => '@(in)'},
    #   {:@id => '@id(thattommyhall)', :name => 'Tom', :citizen => '@(uk)'},
    #   {:@id => '@id(antoniogarrote)', :name => 'Antonio', :citizen => '@(es)'} ]

Nested objects can be retrieved specifying an empty hash for the property. By default, the method all will return an array with all the objects in the recovered, including objects nested in other objects properties. If our query returns a graph with no cycles and we want to return only the top level objects that or not linked from other objects properties, we can pass the option :unlinked => true to the message.

    g.where(:@type => :Developer, :citizen => {}).all(:unlinked => true)
    # [ {:@id => '@id(abs)', :name => 'Abhinay', ...
    #    :citizen => {:@id => '@(in)', :name => 'India', ... }},
    #   {:@id => '@id(thattommyhall)', :name => 'Tom', ...
    #    :citizen => {:@id => '@(uk)', :name => 'United Kingdom' ... }},
    #   {:@id => '@id(antoniogarrote)', :name => 'Antonio', ... 
    #    :citizen => {:@id => '@(es)', :name => 'Spain', ... }} ]

Relationships between objects can be specified in inverse order using a key starting with $inv.

    g.where(:@type => :Country, 
            :$inv_citizen => {:name => "Abhinay"}).all(:unlinked => true)
    # [ {:@id => '@id(in)', :name => 'India', ...'} ]

Filters can be applied to properties to select valid objects:

    g.where(:@type => :Country, 
            :population => {:$gt => 100}).all
    # [ {:@id => '@id(in)', :name => 'India', :population => 1200, ...'} ]
    g.where(:@type => :Country, 
            :population => {:$or => [{:$lt => 50},{:$gt => 1000}]}).all
    # [ {:@id => '@id(in)', :name => 'India', :population => 1200, ...'},
    #   {:@id => '@id(es)', :name => 'Spain', :population => 43, ...} ]
    g.where(:@type => :Country, :name => {:$like => /.+a.+/}).all
    # [ {:@id => '@id(es)', :name => 'Spain', :population => 43, ...} ]

Valid filters are: $and, $or, $lt, $lteq, $gt, $gteq, $eq, $in and $like.

Different optional patterns can be joined in a single query using the method union.

    g.where(:@type => :Country).union(:@type => :Developer).all
    # returns all objects

If more than one object matches a property, the final set of matching objects will be returned in an array.

    g.store(:@id     => 'abs',
            :citizen => '@id(uk)').

      where(:name => 'Abhinay', :citizen => {}).all(:unlinked => true)
    # [ {:@id => '@id(abs)', :name => 'Abhinay', ...
    #    :citizen => [{:@id => '@(in)', :name => 'India', ... },
    #                 {:@id => '@(uk)', :name => 'United Kingdom', ..}]} ],
   

Removing data

The message remove can be sent after running a query to remove the retrieved data from the graph:

    g.store(:@id  => 'abs',
            :name => 'Abhinay')

    g.where(:@id => 'abs').remove

If particular properties from the graph need to be removed from the graph without running a query, the remove message can be send directly to the graph object. This method is the opposite to an store operation.

    g.store(:@id        => 'es',
            :name       => 'Spain',
            :population => 43)

    g.where(:@id => 'es').first
    # {:@id => '@id(es), :name => 'Spain', :population => 43}

    g.remove(:@id => 'es', :population => 43)
    g.where(:@id => 'es').first
    # {:@id => '@id(es), :name => 'Spain'}

Unlinking nodes

Sometimes removing a node from the graph leaves pending incoming connection from other nodes that haven't been recovered in the query leading to the node removal. In order to get rid of any incoming or outgoing connection between one node and the rest of nodes in the graph, the function unlink can be used. Unlink accepts a single node ID or an array of IDs and will remove connections to those nodes in the graph without removing the remaining properties of the nodes.

    g.store(:@id        => 'it',
            :name       => 'Italy',
            :continent  => '@id(eu)')

    g.where(:continent => {}).all(:unlinked => true)
    # [{:@id => '@id(it)', :name => 'Italy', {:continent => {:@id => '@id(eu)'}}}]

    g.unlink('it').where(:continent => {}).all(:unlinked => true)
    # []
    
    g.where('@id' => 'it')
    # [{:@id => '@id(it)', :name => 'Italy'}]

Tuple Queries

Sometimes we just want to retrieve particular facts from the data graph instead of full nodes. Tuple queries makes it possible to retrieve elements of a graph pattern matching the data graph. Results will be returned as hashes where each property is one of the variables in the graph pattern.

Tuple variables are defined in the query as symbols starting by an underscore :_variable_name:

    g.where(:@id => :_id, 
            :name => :_first_name, 
            :citizen => { :name => 'Spain', 
                          :capital => :_capital }).tuples
    # [ {:id => '@id(antoniogarrote)', 
    #    :first_name => 'Antonio', 
    #    :capital => 'Madrid} ]

    # variables can also be added into properties not only values
    g.where(:_property => "Antonio").tuples
    # [ {:property => :name} ]

Inference

Schema information can be added using the define method and assertions like @subclass, @subproperty, @domain, @range.

    # All developers are People
    g.define(:Developer, :@subclass, :Person)   

If inference is enabled for a connection using the with_reasoning method, queries will return additional results.

    # No reasoning
    g.where(:@type => :Person).all
    # []

    # With reasoning
    g.with_reasoning.where(:@type => :Person).all
    # [{:@id => 'id(abs)', 
    #   :@type => :Developer, ...},
    #  {:@id => 'id(thattommyhall)', 
    #   :@type => :Developer, ...},
    #  {:@id => 'id(antoniogarrote)', 
    #   :@type => :Developer, ...}]

An example using the @subproperty declaration.

    g.define(:citizen, :@subproperty, :bornin)   

    g.with_reasoning.where(:bornin => {:@type => :Country, 
                                       :capital => 'Madrid'}).all
    # [{:@id => 'id(antoniogarrote)', 
    #   :citizen => {:@id => 'id(es)', 
    #                :capital => 'Madrid', ... }, ...}]

An example using the @domain and @range declarations.

    g.define(:citizen, :@domain, :Citizen)   
    g.define(:citizen, :@range, :State)   

    g.with_reasoning.where(:@type => :Citizen, 
                           :citizen => {:@type => :State}).all
    # [{:@id => 'id(antoniogarrote)', 
    #   :citizen => {:@id => 'id(es)', 
    #                :capital => 'Madrid', ... }, ...},
    #  {:@id => 'id(thattommyhall)',  
    #   :citizen => {:@id => 'id(uk)', 
    #                :capital => 'Madrid', ... }, ...}
    #  ... ]

Schema definitions can be removed using the method retract_definition.

An example using the @domain and @range declarations.

    g.define(:citizen, :@domain, :Citizen)   
    g.define(:citizen, :@range, :State)   

    g.with_reasoning.where(:@type => :Citizen, 
                           :citizen => {:@type => :State}).all
    # [{:@id => 'id(antoniogarrote)', 
    #   :citizen => {:@id => 'id(es)', 
    #                :capital => 'Madrid', ... }, ...},
    #  {:@id => 'id(thattommyhall)',  
    #   :citizen => {:@id => 'id(uk)', 
    #                :capital => 'Madrid', ... }, ...}
    #  ... ]

    g.retract_definition(:citizen, :@range, :State).where(:@type => :Citizen, :citizen => {:@type => :State}).all
    # [ ]

Inference can be disabled sending the without_reasoning message.

Validations

Reasoning support can also be used to validate the objects you insert in the graph. In this case, your schema definitions are interpreted not to infere new knowledge but to check that the structure of the objects inserted match the schema.

Validations can be introduced in the graph using the validate message that receives an assertion with the @subclass, @subproperty, @domain or @range properties.

Validations are turned on/off using the with_validations and without_validations messages.

If a validation is violated, an exception will be raised. If validations are turned on and there's already invalid data in the graph, no further insertions will succeed.

Validations can also be removed using the retract_validation message.

    g = graph.with_db(DB) # new graph

    g.with_validations.validate(:citizen, :@domain, :State)   

    g.store(:@id => 'id(malditogeek)', 
            :citizen => {:@id => 'id(ar)', 
                         :capital => 'Buenos Aires'}, ...)

    # An exception is raised due to validation violation

    g.store(:@id => 'ar', 
            :capital => 'Buenos Aires', 
            :@type => :State, 
            :name => 'Argentina').
      store(:@id => 'id(malditogeek)', 
            :citizen => '@id(ar)')
    # After adding the @type for Argentina, the insertion does not raise any exception.

Validations and inference can be used together to infere additional infromation that will make data valid according to the defined validations:

    g = graph.with_db(DB) # new graph

    g.with_validations.validate(:citizen, :@domain, :State)   

    g.store(:@id => 'id(malditogeek)', 
            :citizen => {:@id => 'id(ar)', 
                         :capital => 'Buenos Aires'}, ...)

    # An exception is raised due to validation violation

    g.with_reasoning.define(:citizen, :@domain, :State).
      store(:@id => 'id(malditogeek)', 
            :citizen => {:@id => 'id(ar)', 
                         :capital => 'Buenos Aires'}, ...)
    # Data is valid using reasoning since the @type :State for Argentina can be inferred.

    g.where(:@type => :Citizen, 
            :citizen => {:@type => :State}).all
    # [{:@id => 'id(malditogeek)', 
    #   :citizen => {:@id => 'id(ar)', 
    #                :capital => 'Buenos Aires', ... }, ...}]

    g.without_reasoning # graph is invalid now, no further operations can be committed.

    g.without_validations # graph is again valid since no validations will be checked.

Some examples of validations are:

  • Data types in range, using the corresponding class Date, Float, Fixnum, TrueClass / FalseClass:
    g = graph.with_db(DB) # new graph

    g.with_validations.validate(:born, :@domain, Date) # born must have a Date value

    g.store(:@id => 'antoniogarrote', :born => "1982-05-01", ...)

    # An exception is raised due to validation violation, :born has a string value not a date vaue

    g.store(:@id => 'antoniogarrote', :born => Date.parse("1982-05-01"), ...)

    # No validation error is raised
  • Subclass / Superclass relationships
    g = graph.with_db(DB) # new graph

    g.with_validations.validate(:Developer, :@subclass, :Person) # all Developers must be human!

    g.store(:@id => 'abhinay', :@type => :Developer, ...)

    # An exception is raised due to validation violation, :Person @type is missing

    g.store(:@id => 'abhinay', :@type => [:Developer, :Person], ...)

    # No validation error is raised
  • Participation constraints
    g = graph.with_db(DB)

    g.with_validations.validate(:Supervisor, :@some, [:supervises, :Employee])

    g.store(:@type  => :Supervisor)

    # An exception is raised, Supervisors must supervise employees
    
    g.store(:@type  => :Supervisor, :supervises => {:@type => :Employee})

    # No validation error is raised
    g = graph.with_db(DB)

    g.with_validations.validate(:Supervisor, :@all, [:supervises, :Employee])

    g.store(:@type  => :Supervisor, 
            :supervises => [{:@id => 'a', :@type => :Employee},
                            {:@id => 'b', :@type => :Assistant}])

    # An exception is raised, all objectes supervised by a Supervisor must belong to
    # the Employee class
    
    g.store(:@type  => :Supervisor, 
            :supervises => [{:@id => 'a', :@type => :Employee},
                            {:@id => 'b', :@type => [:Assistant, :Employee]}])


    # No validation error is raised
  • Cardinality constraints
    g = graph.with_db(DB).with_validations

    g.validate(:Person, :@cardinality, {:property => :lives, 
                                        :max => 1, 
                                        :min => 1})

    g.store(:@type => :Person,
            :name => 'Antonio',
            :lives => [{:@id => 'es'},{:@id => 'uk'}])


    # An exception is raised, People can only live in one place

    g.store(:@type => :Person,
            :name => 'Antonio',
            :lives => '@id(uk)')

    # No validation error is raised

The details about how to use validations can be found in the Stardog documentation related to ICV (Integrity Constraints Validations) for the data base (http://stardog.com/docs/sdp/#validation).

Rules

Reasoning with Datalog style rules is also possible using GRel thanks to Stardog support for the SWRL standard. Rules are declared using the rules method. This method accepts a hash where each key-value pair defines a different rule. The key is the body (antecedent) of the rule, the value is the (consequent) of the rule implication. Heads and bodies consist of a single triple or an array of triples of the form (property, var_or_value, var_or_value).

For example, the following rule:

(hasParent ?x1 ?x2) AND (hasBrother ?x2 ?x3) -> (hasUncle ?x1 ?x3)

Can be defined using the following code:

    g.rules([[:hasParent, "?x1", "?x2"], [:hasBrother, "?x2", "?x3"]]  => [:hasUncle, "?x1","?x3"])

Now if we store the following graph:

    g.store(:name => 'Antonio', :hasParent => { 
              :name => 'Juliana', :hasParent => {
                :name => 'Leonor', :hasBrother => {
                  :name => 'Santiago'
                } 
              } 
            })

The following query will return results if reasoning support is turned on:

    g.with_reasoning.where(:name => :_nephew, :hasUncle => {:name => :_uncle}).tuples
    # [{:nephew => "Juliana", :uncle => "Santiago"}]

More expressive rules can be defined using functions like greater than, less than etc. These functions can be used in rules using the following symbols:

  • :$gt greater than
  • :$gte greater than or equal
  • :$lt less than
  • :$lte less than or equal
  • :$eq equal
  • :$neq not equal

For example, the following rule defines all UK citizens older than 17 years to be major of age.

    g.rules([[:citizen, "?x1", "@id(uk)"], [:age, "?x1", "?age"], [:$gte, "?age", 18]]  => [:majorAge, "?x1", true],
            [[:citizen, "?x1", "@id(uk)"], [:age, "?x1", "?age"], [:$lt, "?age", 18]]   => [:majorAge, "?x1", false])

Unfortunately as of Stardog version 1.1.3 seems to be some problems using functions resulting in a 500 internal server error. This problems should be fixed in the next release of Stardog.

As with any other reasoning mechanism, rule definitions can be removed using the retract_rules method.

License

Licensed under the Apache2 license.

Author and contact:

Antonio Garrote (antoniogarrote@gmail.com)