Skip to content
Browse files

ActiveRecordStore uses ActiveRecord::Base.connection

  • Loading branch information...
1 parent 19a5526 commit d49a1939d91731792718e75ebb68bd21becfde51 @gkellogg committed Dec 17, 2010
Showing with 337 additions and 1 deletion.
  1. +3 −1 History.rdoc
  2. +1 −0 lib/rdf_context.rb
  3. +272 −0 lib/rdf_context/store/active_record_store.rb
  4. +61 −0 spec/active_record_store_spec.rb
View
4 History.rdoc
@@ -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.
@@ -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
View
1 lib/rdf_context.rb
@@ -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"
View
272 lib/rdf_context/store/active_record_store.rb
@@ -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
View
61 spec/active_record_store_spec.rb
@@ -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.
Something went wrong with that request. Please try again.