Skip to content

Commit

Permalink
Added generic support for all adapters to support the import function…
Browse files Browse the repository at this point in the history
…ality. All tests pass with Mysql and PostgreSQL support. PostgreSQL support for the import functionality works, but it relies on single INSERT statement creation. By adding single INSERT statement creation and making it compatible with import all adapters should work flawlessly. All tests pass right now.

git-svn-id: svn+ssh://rubyforge.org/var/svn/arext/trunk@41 5128a5ed-121c-0410-8d5c-98af306bc9be
  • Loading branch information
zachdennis committed Dec 6, 2006
1 parent b4fff3a commit 141e872
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 131 deletions.
1 change: 1 addition & 0 deletions ar-extensions/Rakefile
Expand Up @@ -19,6 +19,7 @@ namespace :test do
task adapter.to_sym do |t|
ENV['ARE_DB'] = adapter
Dir[ File.join( DIR, 'tests', 'test_*.rb' ) ].each{ |f| require f }
Dir[ File.join( DIR, 'tests', adapter.to_s, 'test_*.rb' ) ].each{ |f| require f }
end
end

Expand Down
10 changes: 5 additions & 5 deletions ar-extensions/config/postgresql.schema
@@ -1,5 +1,5 @@
CREATE TABLE topics (
id integer NOT NULL,
id serial NOT NULL,
title character varying(255) default NULL,
author_name character varying(255) default NULL,
author_email_address character varying(255) default NULL,
Expand All @@ -9,20 +9,20 @@ CREATE TABLE topics (
content text,
approved bool default TRUE,
replies_count integer default 0,
parent_id integer default NULL,
parent_id serial default NULL,
type character varying(50) default NULL,
PRIMARY KEY (id)
);

CREATE TABLE projects (
id integer NOT NULL,
id serial NOT NULL,
name character varying(100) default NULL,
type character varying(255) NOT NULL,
PRIMARY KEY (id)
);

CREATE TABLE developers (
id integer NOT NULL,
id serial NOT NULL,
name character varying(100) default NULL,
salary integer default 70000,
created_at timestamp default NULL,
Expand All @@ -31,7 +31,7 @@ CREATE TABLE developers (
);

CREATE TABLE books (
id integer NOT NULL,
id serial NOT NULL,
title character varying(255) NOT NULL,
publisher character varying(255) NOT NULL,
author_name character varying(255) NOT NULL,
Expand Down
5 changes: 5 additions & 0 deletions ar-extensions/init.rb
Expand Up @@ -2,11 +2,16 @@

dir = File.dirname( __FILE__ )
require File.join( dir, 'lib', 'extensions' )

require File.join( dir, 'lib', 'fulltext' )
require File.join( dir, 'lib', 'fulltext', 'mysql' )

db_adapters_path = File.join( dir, 'lib', 'adapters' )

require File.join( dir, 'lib', 'import' )
require File.join( dir, 'lib', 'import', 'mysql' )
require File.join( dir, 'lib', 'import', 'postgresql' )

require File.join( dir, 'lib', 'finders' )

require File.join( db_adapters_path, 'abstract_adapter' )
Expand Down
60 changes: 20 additions & 40 deletions ar-extensions/lib/fulltext.rb
@@ -1,54 +1,34 @@
require 'forwardable'

module ActiveRecord::Extensions::FullTextSupport
module ActiveRecord::Extensions::FullTextSearching
class FullTextSearchingNotSupported < StandardError ; end

class MySQLFullTextExtension < ActiveRecord::Extensions::AbstractExtension
extend Forwardable

class << self
extend Forwardable

def register( fulltext_key, options )
@fulltext_registry ||= ActiveRecord::Extensions::Registry.new
@fulltext_registry.register( fulltext_key, options )
end

def registry
@fulltext_registry
end

def_delegator :@fulltext_registry, :registers?, :registers?
module FullTextSupport
def supports_full_text_searching?
true
end

RGX = /^match_(.+)/

def process( key, val, caller )
match_data = key.to_s.match( RGX )
return nil unless match_data
fulltext_identifier = match_data.captures[0].to_sym
if self.class.registers?( fulltext_identifier )
fields = self.class.registry[fulltext_identifier][:fields]
str = "MATCH ( #{fields.join( ',' )} ) AGAINST (#{caller.connection.quote(val)})"
return ActiveRecord::Extensions::Result.new( str, nil )
end
nil
end

def_delegator 'ActiveRecord::Extensions::FullTextSupport::MySQLFullTextExtension', :register
end
ActiveRecord::Extensions.register MySQLFullTextExtension.new, :adapters=>[:mysql]


module ClassMethods

module ClassMethods
def fulltext( fulltext_key, options )
ActiveRecord::Extensions::FullTextSupport::MySQLFullTextExtension.register( fulltext_key, options )
connection.register_fulltext_extension( fulltext_key, options )
rescue NoMethodError
# raise FullTextSearchingNotSupported.new
# DO NOT RAISE EXCEPTION, PRINT A WARNING AND DO NOTHING
ActiveRecord::Base.logger.warn "FullTextSearching is not supported for adapter!"
end

end
end

ActiveRecord::Base.extend( ActiveRecord::Extensions::FullTextSupport::ClassMethods )
class ActiveRecord::Base
def self.supports_full_text_searching?
connection.supports_full_text_searching?
rescue NoMethodError
false
end
end

ActiveRecord::Base.extend( ActiveRecord::Extensions::FullTextSearching::ClassMethods )



Expand Down
44 changes: 44 additions & 0 deletions ar-extensions/lib/fulltext/mysql.rb
@@ -0,0 +1,44 @@
class ActiveRecord::Extensions::FullTextSearching::MySQLFullTextExtension < ActiveRecord::Extensions::AbstractExtension
extend Forwardable

class << self
extend Forwardable

def register( fulltext_key, options )
@fulltext_registry ||= ActiveRecord::Extensions::Registry.new
@fulltext_registry.register( fulltext_key, options )
end

def registry
@fulltext_registry
end

def_delegator :@fulltext_registry, :registers?, :registers?
end

RGX = /^match_(.+)/

def process( key, val, caller )
match_data = key.to_s.match( RGX )
return nil unless match_data
fulltext_identifier = match_data.captures[0].to_sym
if self.class.registers?( fulltext_identifier )
fields = self.class.registry[fulltext_identifier][:fields]
str = "MATCH ( #{fields.join( ',' )} ) AGAINST (#{caller.connection.quote(val)})"
return ActiveRecord::Extensions::Result.new( str, nil )
end
nil
end

def_delegator 'ActiveRecord::Extensions::FullTextSupport::MySQLFullTextExtension', :register
end
ActiveRecord::Extensions.register ActiveRecord::Extensions::FullTextSearching::MySQLFullTextExtension.new, :adapters=>[:mysql]


class ActiveRecord::ConnectionAdapters::MysqlAdapter
include ActiveRecord::Extensions::FullTextSearching::FullTextSupport

def register_fulltext_extension( fulltext_key, options )
ActiveRecord::Extensions::FullTextSearching::MySQLFullTextExtension.register( fulltext_key, options )
end
end
123 changes: 46 additions & 77 deletions ar-extensions/lib/import.rb
@@ -1,8 +1,32 @@
module ActiveRecord::Extensions::ConnectionAdapters ; end
module ActiveRecord::Extensions::Import ; end
module ActiveRecord::Extensions::Import
module ImportSupport
def supports_import?
true
end
end

module OnDuplicateKeyUpdateSupport
def supports_on_duplicate_key_update?
true
end
end
end

module ActiveRecord::Extensions::Import::Base

def supports_import?
connection.supports_import?
rescue NoMethodError
false
end

def supports_on_duplicate_key_update?
connection.supports_on_duplicate_key_update?
rescue NoMethodError
false
end

# Imports a collection of values to the database. This is more efficient than
# using ActiveRecord::Base#create or ActiveRecord::Base#save multiple times. This
# method works well if you want to create more then one record at a time and do not
Expand Down Expand Up @@ -123,7 +147,7 @@ def import_with_validations( column_names, array_of_attributes, options={} ) # :
end
end
array_of_attributes.compact!

if not array_of_attributes.empty?
import_without_validations_or_callbacks( column_names, array_of_attributes )
end
Expand All @@ -135,12 +159,29 @@ def import_with_validations( column_names, array_of_attributes, options={} ) # :
# records without validations or callbacks.
def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
escaped_column_names = quote_column_names( column_names )
columns = []
array_of_attributes.first.each_with_index { |arr,i| columns << columns_hash[ column_names[i] ] }

if not supports_import?
columns_sql = "(" + escaped_column_names.join( ',' ) + ")"
insert_statements, values = [], []
array_of_attributes.each do |arr|
my_values = []
arr.each_with_index do |val,j|
my_values << connection.quote( val, columns[j] )
# puts columns[j].inspect
# exit
end
insert_statements << "INSERT INTO #{self.table_name} #{columns_sql} VALUES(" + my_values.join( ',' ) + ")"
connection.execute( insert_statements.last )
end
return
else


# generate the sql
insert_sql = connection.multiple_value_sets_insert_sql( table_name, escaped_column_names, options )

columns = []
array_of_attributes.first.each_with_index { |arr,i| columns << columns_hash[ column_names[i] ] }
values_sql = connection.values_sql_for_column_names_and_attributes( columns, array_of_attributes )
post_sql_statements = connection.post_sql_statements( table_name, options )

Expand All @@ -149,6 +190,7 @@ def import_without_validations_or_callbacks( column_names, array_of_attributes,
[ insert_sql, post_sql_statements ].flatten,
values_sql,
"#{self.class.name} Create Many Without Validations Or Callbacks" )
end
end

# Returns an array of quoted column names
Expand All @@ -171,76 +213,3 @@ def validations_array_for_column_names_and_attributes( column_names, array_of_at

ActiveRecord::Base.extend( ActiveRecord::Extensions::Import::Base )



module ActiveRecord::Extensions::ConnectionAdapters::MysqlAdapter

# Returns an array of post SQL statements given the passed in options.
def post_sql_statements( table_name, options )
post_sql_statements = []
if options[:on_duplicate_key_update]
post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
end
post_sql_statements
end

def multiple_value_sets_insert_sql( table_name, column_names, options )
"INSERT #{options[:ignore]?'IGNORE':''} INTO #{table_name} (#{column_names.join(', ')}) "
end

# Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
# in +args+.
def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
sql = ' ON DUPLICATE KEY UPDATE '
arg = args.first
if arg.is_a?( Array )
sql << sql_for_on_duplicate_key_update_as_array( table_name, arg )
elsif arg.is_a?( Hash )
sql << sql_for_on_duplicate_key_update_as_hash( table_name, arg )
else
raise ArgumentError.new( "Expected Array or Hash" )
end
sql
end

def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
qt = quote_column_name( table_name )
results = arr.map do |column|
qc = quote_column_name( column )
"#{qt}.#{qc}=VALUES( #{qc} )"
end
results.join( ',' )
end

def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
sql = ' ON DUPLICATE KEY UPDATE '
qt = quote_column_name( table_name )
results = hsh.map do |column1, column2|
qc1 = quote_column_name( column1 )
qc2 = quote_column_name( column2 )
"#{qt}.#{qc1}=VALUES( #{qc2} )"
end
results.join( ',')
end

# Returns SQL the VALUES for an INSERT statement given the passed in +columns+
# and +array_of_attributes+.
def values_sql_for_column_names_and_attributes( columns, array_of_attributes ) # :nodoc:
values = []
array_of_attributes.each do |arr|
my_values = []
arr.each_with_index do |val,j|
my_values << quote( val, columns[j] )
end
values << my_values
end
values_arr = values.map{ |arr| '(' + arr.join( ',' ) + ')' }
values_arr[0] = "VALUES" + values_arr[0]
values_arr
end

end

#ActiveRecord::Base.extend( ActiveRecord::Extensions::ConnectionAdapters::MysqlAdapter )
ActiveRecord::ConnectionAdapters::MysqlAdapter.send( 'include', ActiveRecord::Extensions::ConnectionAdapters::MysqlAdapter )

0 comments on commit 141e872

Please sign in to comment.