From e8271778b7ebcb78ec3c9a719473bbc1decfa8bb Mon Sep 17 00:00:00 2001 From: Zach Dennis Date: Sat, 13 Mar 2010 21:33:03 -0500 Subject: [PATCH] About 85% through porting over MySQL on duplicate key functionality. --- lib/ar-extensions/import/mysql.rb | 50 +++++++++++ test/models/topic.rb | 1 + test/mysql/import_test.rb | 141 ++++++++++++++++++++++++++++++ test/schema/mysql_schema.rb | 32 +++++++ test/test_helper.rb | 15 ++++ 5 files changed, 239 insertions(+) create mode 100644 lib/ar-extensions/import/mysql.rb create mode 100644 test/mysql/import_test.rb create mode 100644 test/schema/mysql_schema.rb diff --git a/lib/ar-extensions/import/mysql.rb b/lib/ar-extensions/import/mysql.rb new file mode 100644 index 0000000..e78c489 --- /dev/null +++ b/lib/ar-extensions/import/mysql.rb @@ -0,0 +1,50 @@ +module ActiveRecord::Extensions::ConnectionAdapters::MysqlAdapter # :nodoc: + + include ActiveRecord::Extensions::Import::ImportSupport + include ActiveRecord::Extensions::Import::OnDuplicateKeyUpdateSupport + + # 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 ) + elsif arg.is_a?( String ) + sql << 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: + results = arr.map do |column| + qc = quote_column_name( column ) + "#{table_name}.#{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 ' + results = hsh.map do |column1, column2| + qc1 = quote_column_name( column1 ) + qc2 = quote_column_name( column2 ) + "#{table_name}.#{qc1}=VALUES( #{qc2} )" + end + results.join( ',') + end + + #return true if the statement is a duplicate key record error + def duplicate_key_update_error?(exception)# :nodoc: + exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry') + end + +end + +ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do + include ActiveRecord::Extensions::ConnectionAdapters::MysqlAdapter +end diff --git a/test/models/topic.rb b/test/models/topic.rb index c0d916f..091bb8b 100644 --- a/test/models/topic.rb +++ b/test/models/topic.rb @@ -1,6 +1,7 @@ class Topic < ActiveRecord::Base validates_presence_of :author_name has_many :books + belongs_to :parent, :class_name => "Topic" composed_of :description, :mapping => [ %w(title title), %w(author_name author_name)], :allow_nil => true, :class_name => "TopicDescription" end diff --git a/test/mysql/import_test.rb b/test/mysql/import_test.rb new file mode 100644 index 0000000..818334f --- /dev/null +++ b/test/mysql/import_test.rb @@ -0,0 +1,141 @@ +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') +require "ar-extensions/import/mysql" + +describe "#import with :on_duplicate_key_update option (mysql specific functionality)" do + macro(:perform_import){ raise "supply your own #perform_import in a context below" } + + assertion(:should_not_update_fields_not_mentioned) do + assert_equal "John Doe", @topic.reload.author_name + end + + assertion(:should_update_fields_mentioned) do + perform_import + assert_equal "Book - 2nd Edition", @topic.reload.title + assert_equal "johndoe@example.com", @topic.reload.author_email_address + end + + assertion(:should_update_fields_mentioned_with_hash_mappings) do + perform_import + assert_equal "johndoe@example.com", @topic.reload.title + assert_equal "Book - 2nd Edition", @topic.reload.author_email_address + end + + assertion(:should_update_foreign_keys) do + perform_import + assert_equal 57, @topic.reload.parent_id + end + + context "given columns and values with :validation checks turned off" do + let(:columns){ %w( id title author_name author_email_address parent_id ) } + let(:values){ [ [ 99, "Book", "John Doe", "john@doe.com", 17 ] ] } + let(:updated_values){ [ [ 99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57 ] ] } + + macro(:perform_import) do + Topic.import columns, updated_values, :on_duplicate_key_update => update_columns, :validate => false + end + + setup do + Topic.import columns, values, :validate => false + @topic = Topic.find 99 + end + + context "using string column names" do + let(:update_columns){ [ "title", "author_email_address", "parent_id" ] } + should_not_update_fields_not_mentioned + should_update_fields_mentioned + should_update_foreign_keys + end + + context "using symbol column names" do + let(:update_columns){ [ :title, :author_email_address, :parent_id ] } + should_not_update_fields_not_mentioned + should_update_fields_mentioned + should_update_foreign_keys + end + + context "using string hash map" do + let(:update_columns){ { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } } + should_not_update_fields_not_mentioned + should_update_fields_mentioned + should_update_foreign_keys + end + + context "using string hash map, but specifying column mismatches" do + let(:update_columns){ { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } } + should_not_update_fields_not_mentioned + should_update_fields_mentioned_with_hash_mappings + should_update_foreign_keys + end + + context "using symbol hash map" do + let(:update_columns){ { :title => :title, :author_email_address => :author_email_address, :parent_id => :parent_id } } + should_not_update_fields_not_mentioned + should_update_fields_mentioned + should_update_foreign_keys + end + + context "using symbol hash map, but specifying column mismatches" do + let(:update_columns){ { :title => :author_email_address, :author_email_address => :title, :parent_id => :parent_id } } + should_not_update_fields_not_mentioned + should_update_fields_mentioned_with_hash_mappings + should_update_foreign_keys + end + end + + context "given array of model instances with :validation checks turned off" do + macro(:perform_import) do + @topic.title = "Book - 2nd Edition" + @topic.author_name = "Author Should Not Change" + @topic.author_email_address = "johndoe@example.com" + @topic.parent_id = 57 + Topic.import [@topic], :on_duplicate_key_update => update_columns, :validate => false + end + + setup do + @topic = Generate(:topic, :id => 99, :author_name => "John Doe", :parent_id => 17) + end + + context "using string column names" do + let(:update_columns){ [ "title", "author_email_address", "parent_id" ] } + should_not_update_fields_not_mentioned + should_update_fields_mentioned + should_update_foreign_keys + end + + context "using symbol column names" do + let(:update_columns){ [ :title, :author_email_address, :parent_id ] } + should_not_update_fields_not_mentioned + should_update_fields_mentioned + should_update_foreign_keys + end + + context "using string hash map" do + let(:update_columns){ { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } } + should_not_update_fields_not_mentioned + should_update_fields_mentioned + should_update_foreign_keys + end + + context "using string hash map, but specifying column mismatches" do + let(:update_columns){ { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } } + should_not_update_fields_not_mentioned + should_update_fields_mentioned_with_hash_mappings + should_update_foreign_keys + end + + context "using symbol hash map" do + let(:update_columns){ { :title => :title, :author_email_address => :author_email_address, :parent_id => :parent_id } } + should_not_update_fields_not_mentioned + should_update_fields_mentioned + should_update_foreign_keys + end + + context "using symbol hash map, but specifying column mismatches" do + let(:update_columns){ { :title => :author_email_address, :author_email_address => :title, :parent_id => :parent_id } } + should_not_update_fields_not_mentioned + should_update_fields_mentioned_with_hash_mappings + should_update_foreign_keys + end + end + +end \ No newline at end of file diff --git a/test/schema/mysql_schema.rb b/test/schema/mysql_schema.rb new file mode 100644 index 0000000..8306777 --- /dev/null +++ b/test/schema/mysql_schema.rb @@ -0,0 +1,32 @@ +ActiveRecord::Schema.define do + + create_table :test_myisam, :options=>'ENGINE=MyISAM', :force=>true do |t| + t.column :my_name, :string, :null=>false + t.column :description, :string + end + + create_table :test_innodb, :options=>'ENGINE=InnoDb', :force=>true do |t| + t.column :my_name, :string, :null=>false + t.column :description, :string + end + + create_table :test_memory, :options=>'ENGINE=Memory', :force=>true do |t| + t.column :my_name, :string, :null=>false + t.column :description, :string + end + + create_table :books, :options=>'ENGINE=MyISAM', :force=>true do |t| + t.column :title, :string, :null=>false + t.column :publisher, :string, :null=>false, :default => 'Default Publisher' + t.column :author_name, :string, :null=>false + t.column :created_at, :datetime + t.column :created_on, :datetime + t.column :updated_at, :datetime + t.column :updated_on, :datetime + t.column :publish_date, :date + t.column :topic_id, :integer + t.column :for_sale, :boolean, :default => true + end + execute "ALTER TABLE books ADD FULLTEXT( `title`, `publisher`, `author_name` )" + +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 6366568..8842d20 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -22,6 +22,21 @@ class ActiveSupport::TestCase self.use_transactional_fixtures = true class << self + def assertion(name, &block) + mc = class << self ; self ; end + mc.class_eval do + define_method(name) do + it(name, &block) + end + end + end + + def macro(name, &block) + class_eval do + define_method(name, &block) + end + end + def describe(description, toplevel=nil, &blk) text = toplevel ? description : "#{name} #{description}" klass = Class.new(self)