Skip to content
This repository has been archived by the owner on Dec 21, 2017. It is now read-only.

Commit

Permalink
ActiveRecordStore uses ActiveRecord::Base.connection
Browse files Browse the repository at this point in the history
  • Loading branch information
gkellogg committed Dec 18, 2010
1 parent 19a5526 commit d49a193
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 1 deletion.
4 changes: 3 additions & 1 deletion History.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* SQLite3Store
* Basic support for using with ActiveRecord: #initialize _configuration_ can take a _connection_ option, using ActiveRecord::Base.connection.raw_connection
* If result set is returned as a hash, change back to an array (needed, as rails sets results_as_hash attribute on DB)
* Added ActiveRecordStore
* Uses ActiveRecord connection to store graph information, not an ActiveRecord::Base model

=== 0.5.9.1
* Fix minor graph property caching bug relating to manipulation of sequences.
Expand Down Expand Up @@ -156,7 +158,7 @@
* Add Graph#contents as alias to store
* Add ConjunctiveGraph#triples to return all triples from store, not just default context
* Add Graph#nsbinding to retreive Store#nsbinding
* Fix numerious SQLite3Store and AbstractSQLStore bugs. Add round-trip tests to Graph spec.
* Fix numerous SQLite3Store and AbstractSQLStore bugs. Add round-trip tests to Graph spec.

=== 0.4.4
* Namespace usage cleanup
Expand Down
1 change: 1 addition & 0 deletions lib/rdf_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ module RdfContext

# Stores
autoload :AbstractStore, "rdf_context/store/abstract_store"
autoload :AbstractSQLStore, "rdf_context/store/abstract_sql_store"
autoload :ActiveRecordStore, "rdf_context/store/active_record_store"
autoload :ListStore, "rdf_context/store/list_store"
autoload :MemoryStore, "rdf_context/store/memory_store"
Expand Down
272 changes: 272 additions & 0 deletions lib/rdf_context/store/active_record_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
gem 'activerecord'
require 'active_record'

module RdfContext
autoload :AbstractSQLStore, File.join(File.dirname(__FILE__), 'abstract_sql_store')

# ActiveRecord store context-ware and formula-aware implementation.
#
# Uses an ActiveRecord::Base.connection to run statements
#
# It stores it's triples in the following partitions:
# - Asserted non rdf:type statements
# - Asserted rdf:type statements (in a table which models Class membership). The motivation for this partition is primarily query speed and scalability as most graphs will always have more rdf:type statements than others
# - All Quoted statements
#
# In addition it persists namespace mappings in a seperate table
class ActiveRecordStore < AbstractSQLStore
# Create a new ActiveRecordStore Store
# @param [URIRef] identifier
# @param[Hash] configuration Specific to type of storage
# @return [ActiveRecordStore]
def initialize(identifier = nil, configuration = {})
super(identifier, configuration)
@autocommit_default = false
end

def open(options)
end

def close(commit_pending_transactions = false)
ActiveRecord::Base.connection.commit_db_transaction if commit_pending_transactions
end

# Create necessary tables and indecies for this database
def setup
executeSQL(CREATE_ASSERTED_STATEMENTS_TABLE % @internedId)
executeSQL(CREATE_ASSERTED_TYPE_STATEMENTS_TABLE % @internedId)
executeSQL(CREATE_QUOTED_STATEMENTS_TABLE % @internedId)
executeSQL(CREATE_NS_BINDS_TABLE % @internedId)
executeSQL(CREATE_LITERAL_STATEMENTS_TABLE % @internedId)

# Create indicies
{
asserted_table => {
"#{@internedId}_A_termComb_index" => %w(termComb),
"#{@internedId}_A_s_index" => %w(subject),
"#{@internedId}_A_p_index" => %w(predicate),
"#{@internedId}_A_o_index" => %w(object),
"#{@internedId}_A_c_index" => %w(context),
},
asserted_type_table => {
"#{@internedId}_T_termComb_index" => %w(termComb),
"#{@internedId}_T_member_index" => %w(member),
"#{@internedId}_T_klass_index" => %w(klass),
"#{@internedId}_T_c_index" => %w(context),
},
literal_table => {
"#{@internedId}_L_termComb_index" => %w(termComb),
"#{@internedId}_L_s_index" => %w(subject),
"#{@internedId}_L_p_index" => %w(predicate),
"#{@internedId}_L_c_index" => %w(context),
},
quoted_table => {
"#{@internedId}_Q_termComb_index" => %w(termComb),
"#{@internedId}_Q_s_index" => %w(subject),
"#{@internedId}_Q_p_index" => %w(predicate),
"#{@internedId}_Q_o_index" => %w(object),
"#{@internedId}_Q_c_index" => %w(context),
},
namespace_binds => {
"#{@internedId}_uri_index" => %w(uri),
}
}.each_pair do |tablename, indicies|
indicies.each_pair do |index, columns|
executeSQL("CREATE INDEX #{index} on #{tablename} ('#{columns.join(', ')}')")
end
end
end

# Teardown DB files
def teardown
# Drop indicies
{
asserted_table => {
"#{@internedId}_A_termComb_index" => %w(termComb),
"#{@internedId}_A_s_index" => %w(subject),
"#{@internedId}_A_p_index" => %w(predicate),
"#{@internedId}_A_o_index" => %w(object),
"#{@internedId}_A_c_index" => %w(context),
},
asserted_type_table => {
"#{@internedId}_T_termComb_index" => %w(termComb),
"#{@internedId}_T_member_index" => %w(member),
"#{@internedId}_T_klass_index" => %w(klass),
"#{@internedId}_T_c_index" => %w(context),
},
literal_table => {
"#{@internedId}_L_termComb_index" => %w(termComb),
"#{@internedId}_L_s_index" => %w(subject),
"#{@internedId}_L_p_index" => %w(predicate),
"#{@internedId}_L_c_index" => %w(context),
},
quoted_table => {
"#{@internedId}_Q_termComb_index" => %w(termComb),
"#{@internedId}_Q_s_index" => %w(subject),
"#{@internedId}_Q_p_index" => %w(predicate),
"#{@internedId}_Q_o_index" => %w(object),
"#{@internedId}_Q_c_index" => %w(context),
},
namespace_binds => {
"#{@internedId}_uri_index" => %w(uri),
}
}.each_pair do |tablename, indicies|
tn = "#{@internedId}_#{tablename}"
indicies.each_pair do |index, columns|
executeSQL("DROP INDEX #{index} ON #{tn}")
end
end

# Drop tables
executeSQL("DROP TABLE #{namespace_binds}")
executeSQL("DROP TABLE #{quoted_table}")
executeSQL("DROP TABLE #{literal_table}")
executeSQL("DROP TABLE #{asserted_type_table}")
executeSQL("DROP TABLE #{asserted_table}")
end

# Destroy database
#
# @option options[String] :path Path to database file defaults to a file in the current directory based on a hash of the store identifier
def destroy(options = {})
end

protected

# Where clase utility functions
def buildSubjClause(subject, tableName)
case subject
# when REGEXTerm
# when Array
when Graph
["#{tableName}.subject=?", self.normalizeTerm(subject.identifier)]
else
["#{tableName}.subject=?", subject] if subject
end
end

def buildPredClause(predicate, tableName)
# case predicate
# when REGEXTerm
# when Array
# else
["#{tableName}.predicate=?", predicate] if predicate
# end
end

# Where clase utility functions
def buildObjClause(object, tableName)
case object
# when REGEXTerm
# when Array
when Graph
["#{tableName}.object=?", self.normalizeTerm(object.identifier)]
else
["#{tableName}.object=?", object] if object
end
end

# Where clase utility functions
def buildContextClause(context, tableName)
context = normalizeTerm(context) if context

# case context
# when REGEXTerm
# when Array
# else
["#{tableName}.context=?", context] if context
# end
end

# Where clase utility functions
def buildTypeMemberClause(subject, tableName)
# case context
# when REGEXTerm
# when Array
# else
["#{tableName}.member=?", subject] if subject
# end
end

# Where clase utility functions
def buildTypeClassClause(object, tableName)
# case context
# when REGEXTerm
# else
["#{tableName}.klass=?", object] if object
# end
end

# This takes the query string and parameters and (depending on the SQL implementation) either fill in
# the parameter in-place or pass it on to the DB impl (if it supports this).
# The default (here) is to fill the parameters in-place surrounding each param with quote characters
#
# Yields each row
def executeSQL(qStr, *params, &block)
conn = ActiveRecord::Base.connection

puts "executeSQL: params=#{params.flatten.inspect}" if ::RdfContext::debug?
puts "executeSQL: '#{params.dup.unshift(qStr).join("', '")}'" if ::RdfContext::debug?
qStr = ActiveRecord::Base.send(:replace_bind_variables, qStr, params.flatten)
puts " => '#{qStr}'" if ::RdfContext::debug?

if block_given?
conn.select_rows(qStr).each do |row|
yield(row)
end
else
puts "executeSQL no block given" if ::RdfContext::debug?
conn.select_rows(qStr)
end
rescue SQLite3::SQLException => e
puts "SQL Exception (ignored): #{e.message}" if ::RdfContext::debug?
end

CREATE_ASSERTED_STATEMENTS_TABLE = %(
CREATE TABLE %s_asserted_statements (
subject text not NULL,
predicate text not NULL,
object text not NULL,
context text not NULL,
termComb tinyint unsigned not NULL))

CREATE_ASSERTED_TYPE_STATEMENTS_TABLE = %(
CREATE TABLE %s_type_statements (
member text not NULL,
klass text not NULL,
context text not NULL,
termComb tinyint unsigned not NULL))

CREATE_LITERAL_STATEMENTS_TABLE = %(
CREATE TABLE %s_literal_statements (
subject text not NULL,
predicate text not NULL,
object text,
context text not NULL,
termComb tinyint unsigned not NULL,
objLanguage varchar(3),
objDatatype text))

CREATE_QUOTED_STATEMENTS_TABLE = %(
CREATE TABLE %s_quoted_statements (
subject text not NULL,
predicate text not NULL,
object text,
context text not NULL,
termComb tinyint unsigned not NULL,
objLanguage varchar(3),
objDatatype text))

CREATE_NS_BINDS_TABLE = %(
CREATE TABLE %s_namespace_binds (
prefix varchar(20) UNIQUE not NULL,
uri text,
PRIMARY KEY (prefix)))

DROP_ASSERTED_STATEMENTS_TABLE = %(DROP TABLE %s_asserted_statements)
DROP_ASSERTED_TYPE_STATEMENTS_TABLE = %(DROP TABLE %s_type_statements)
DROP_LITERAL_STATEMENTS_TABLE = %(DROP TABLE %s_literal_statements)
DROP_QUOTED_STATEMENTS_TABLE = %(DROP TABLE %s_quoted_statements)
DROP_NS_BINDS_TABLE = %(DROP TABLE %s_namespace_binds)
end
end
61 changes: 61 additions & 0 deletions spec/active_record_store_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
$:.unshift "."
require File.join(File.dirname(__FILE__), 'spec_helper')
require File.join(File.dirname(__FILE__), 'store_helper')

describe ActiveRecordStore do
before(:all) do
FileUtils.rm_rf(TMP_DIR)
Dir.mkdir(TMP_DIR)
@dbfile = File.join(TMP_DIR, "sqlite3.db")
@identifier = URIRef.new("http://identifier")
end

before(:each) do
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => @dbfile)
#::RdfContext::debug =true
@store = ActiveRecordStore.new(@identifier)
@store.setup
end

after(:all) do
FileUtils.rm_rf(TMP_DIR)
end

after(:each) do
FileUtils.rm(@dbfile) if File.file?(@dbfile)
end

subject { @store }
it_should_behave_like "Store"
it_should_behave_like "Context Aware Store"

it "should close db" do
subject.close
end

it "should find contexts with type" do
triple = Triple.new("http://foo", RDF_TYPE, "http://baz")
subject.add(triple, nil)
subject.contexts(triple).length.should == 1
end

it "should find triples with typed literal" do
triple = Triple.new("http://foo", RDF_TYPE, Literal.build_from(1.1))
subject.add(triple, nil)
subject.contexts(triple).length.should == 1
end

it "should find triples with untyped literal and lang" do
triple = Triple.new("http://foo", RDF_TYPE, Literal.untyped("foo", "en-US"))
subject.add(triple, nil)
subject.contexts(triple).length.should == 1
end

it "should find contexts pattern triple" do
triple = Triple.new("http://foo", RDF_TYPE, "http://baz")
subject.add(triple, nil)
subject.contexts(Triple.new(nil, nil, nil)).length.should == 1
end
end

0 comments on commit d49a193

Please sign in to comment.