Finished porting over MySQL import functionality.

* removed assertions on #num_inserts, that is db-specific and is handled in details by abstract_adapter_test.
This commit is contained in:
Zach Dennis 2010-03-13 22:01:01 -05:00
parent e8271778b7
commit 075104a944
7 changed files with 123 additions and 95 deletions

View file

@ -45,7 +45,7 @@ module ActiveRecord # :nodoc:
sql2insert = base_sql + values.join( ',' ) + post_sql sql2insert = base_sql + values.join( ',' ) + post_sql
insert( sql2insert, *args ) insert( sql2insert, *args )
end end
end end
number_of_inserts number_of_inserts
end end

View file

@ -289,7 +289,6 @@ class ActiveRecord::Base
values_sql, values_sql,
"#{self.class.name} Create Many Without Validations Or Callbacks" ) "#{self.class.name} Create Many Without Validations Or Callbacks" )
end end
number_inserted number_inserted
end end

View file

@ -1,17 +1,17 @@
require File.expand_path(File.dirname(__FILE__) + '/test_helper') require File.expand_path(File.dirname(__FILE__) + '/test_helper')
describe "#import" do describe "#import" do
it "should return the number of inserts performed" do it "should return the number of inserts performed" do
# see ActiveRecord::ConnectionAdapters::AbstractAdapter test for more specifics
assert_difference "Topic.count", +10 do assert_difference "Topic.count", +10 do
result = Topic.import Build(3, :topics) result = Topic.import Build(3, :topics)
assert_equal 3, result.num_inserts assert result.num_inserts > 0
result = Topic.import Build(7, :topics) result = Topic.import Build(7, :topics)
assert_equal 7, result.num_inserts assert result.num_inserts > 0
end end
end end
context "with :validation option" do context "with :validation option" do
let(:columns) { %w(title author_name) } let(:columns) { %w(title author_name) }
let(:valid_values) { [[ "LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] } let(:valid_values) { [[ "LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
@ -21,14 +21,12 @@ describe "#import" do
it "should import valid data" do it "should import valid data" do
assert_difference "Topic.count", +2 do assert_difference "Topic.count", +2 do
result = Topic.import columns, valid_values, :validate => false result = Topic.import columns, valid_values, :validate => false
assert_equal 2, result.num_inserts
end end
end end
it "should import invalid data" do it "should import invalid data" do
assert_difference "Topic.count", +2 do assert_difference "Topic.count", +2 do
result = Topic.import columns, invalid_values, :validate => false result = Topic.import columns, invalid_values, :validate => false
assert_equal 2, result.num_inserts
end end
end end
end end
@ -37,33 +35,30 @@ describe "#import" do
it "should import valid data" do it "should import valid data" do
assert_difference "Topic.count", +2 do assert_difference "Topic.count", +2 do
result = Topic.import columns, valid_values, :validate => true result = Topic.import columns, valid_values, :validate => true
assert_equal 2, result.num_inserts
end end
end end
it "should not import invalid data" do it "should not import invalid data" do
assert_no_difference "Topic.count" do assert_no_difference "Topic.count" do
result = Topic.import columns, invalid_values, :validate => true result = Topic.import columns, invalid_values, :validate => true
assert_equal 0, result.num_inserts
end end
end end
it "should report the failed instances" do it "should report the failed instances" do
results = Topic.import columns, invalid_values, :validate => true results = Topic.import columns, invalid_values, :validate => true
assert_equal invalid_values.size, results.failed_instances.size assert_equal invalid_values.size, results.failed_instances.size
results.failed_instances.each{ |e| assert_kind_of Topic, e } results.failed_instances.each{ |e| assert_kind_of Topic, e }
end end
it "should import valid data when mixed with invalid data" do it "should import valid data when mixed with invalid data" do
assert_difference "Topic.count", +2 do assert_difference "Topic.count", +2 do
result = Topic.import columns, valid_values + invalid_values, :validate => true result = Topic.import columns, valid_values + invalid_values, :validate => true
assert_equal 2, result.num_inserts
end end
assert_equal 0, Topic.find_all_by_title(invalid_values.map(&:first)).count assert_equal 0, Topic.find_all_by_title(invalid_values.map(&:first)).count
end end
end end
end end
context "with an array of unsaved model instances" do context "with an array of unsaved model instances" do
let(:topic) { Build(:topic, :title => "The RSpec Book", :author_name => "David Chelimsky")} let(:topic) { Build(:topic, :title => "The RSpec Book", :author_name => "David Chelimsky")}
let(:topics) { Build(9, :topics) } let(:topics) { Build(9, :topics) }
@ -72,13 +67,12 @@ describe "#import" do
it "should import records based on those model's attributes" do it "should import records based on those model's attributes" do
assert_difference "Topic.count", +9 do assert_difference "Topic.count", +9 do
result = Topic.import topics result = Topic.import topics
assert_equal 9, result.num_inserts
end end
Topic.import [topic] Topic.import [topic]
assert Topic.find_by_title_and_author_name("The RSpec Book", "David Chelimsky") assert Topic.find_by_title_and_author_name("The RSpec Book", "David Chelimsky")
end end
it "should not overwrite existing records" do it "should not overwrite existing records" do
topic = Generate(:topic, :title => "foobar") topic = Generate(:topic, :title => "foobar")
assert_no_difference "Topic.count" do assert_no_difference "Topic.count" do
@ -96,14 +90,12 @@ describe "#import" do
it "should import valid models" do it "should import valid models" do
assert_difference "Topic.count", +9 do assert_difference "Topic.count", +9 do
result = Topic.import topics, :validate => true result = Topic.import topics, :validate => true
assert_equal 9, result.num_inserts
end end
end end
it "should not import invalid models" do it "should not import invalid models" do
assert_no_difference "Topic.count" do assert_no_difference "Topic.count" do
result = Topic.import invalid_topics, :validate => true result = Topic.import invalid_topics, :validate => true
assert_equal 0, result.num_inserts
end end
end end
end end
@ -112,7 +104,6 @@ describe "#import" do
it "should import invalid models" do it "should import invalid models" do
assert_difference "Topic.count", +7 do assert_difference "Topic.count", +7 do
result = Topic.import invalid_topics, :validate => false result = Topic.import invalid_topics, :validate => false
assert_equal 7, result.num_inserts
end end
end end
end end
@ -124,32 +115,29 @@ describe "#import" do
it "should import records populating the supplied columns with the corresponding model instance attributes" do it "should import records populating the supplied columns with the corresponding model instance attributes" do
assert_difference "Topic.count", +2 do assert_difference "Topic.count", +2 do
result = Topic.import [:author_name, :title], topics result = Topic.import [:author_name, :title], topics
assert_equal 2, result.num_inserts
end end
# imported topics should be findable by their imported attributes # imported topics should be findable by their imported attributes
assert Topic.find_by_author_name(topics.first.author_name) assert Topic.find_by_author_name(topics.first.author_name)
assert Topic.find_by_author_name(topics.last.author_name) assert Topic.find_by_author_name(topics.last.author_name)
end end
it "should not populate fields for columns not imported" do it "should not populate fields for columns not imported" do
topics.first.author_email_address = "zach.dennis@gmail.com" topics.first.author_email_address = "zach.dennis@gmail.com"
assert_difference "Topic.count", +2 do assert_difference "Topic.count", +2 do
result = Topic.import [:author_name, :title], topics result = Topic.import [:author_name, :title], topics
assert_equal 2, result.num_inserts
end end
assert !Topic.find_by_author_email_address("zach.dennis@gmail.com") assert !Topic.find_by_author_email_address("zach.dennis@gmail.com")
end end
end end
context "ActiveRecord timestamps" do context "ActiveRecord timestamps" do
context "when the timestamps columns are present" do context "when the timestamps columns are present" do
setup do setup do
Delorean.time_travel_to("5 minutes ago") do Delorean.time_travel_to("5 minutes ago") do
assert_difference "Book.count", +1 do assert_difference "Book.count", +1 do
result = Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]] result = Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]]
assert_equal 1, result.num_inserts
end end
end end
@book = Book.last @book = Book.last
@ -158,15 +146,15 @@ describe "#import" do
it "should set the created_at column for new records" do it "should set the created_at column for new records" do
assert_equal 5.minutes.ago.strftime("%H:%M"), @book.created_at.strftime("%H:%M") assert_equal 5.minutes.ago.strftime("%H:%M"), @book.created_at.strftime("%H:%M")
end end
it "should set the created_on column for new records" do it "should set the created_on column for new records" do
assert_equal 5.minutes.ago.strftime("%H:%M"), @book.created_on.strftime("%H:%M") assert_equal 5.minutes.ago.strftime("%H:%M"), @book.created_on.strftime("%H:%M")
end end
it "should set the updated_at column for new records" do it "should set the updated_at column for new records" do
assert_equal 5.minutes.ago.strftime("%H:%M"), @book.updated_at.strftime("%H:%M") assert_equal 5.minutes.ago.strftime("%H:%M"), @book.updated_at.strftime("%H:%M")
end end
it "should set the updated_on column for new records" do it "should set the updated_on column for new records" do
assert_equal 5.minutes.ago.strftime("%H:%M"), @book.updated_on.strftime("%H:%M") assert_equal 5.minutes.ago.strftime("%H:%M"), @book.updated_on.strftime("%H:%M")
end end
@ -179,39 +167,30 @@ describe "#import" do
Delorean.time_travel_to("5 minutes ago") do Delorean.time_travel_to("5 minutes ago") do
assert_difference "Book.count", +1 do assert_difference "Book.count", +1 do
result = Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]] result = Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]]
assert_equal 1, result.num_inserts
end end
end end
ActiveRecord::Base.default_timezone = original_timezone ActiveRecord::Base.default_timezone = original_timezone
@book = Book.last @book = Book.last
end end
it "should set the created_at column for new records respecting the time zone" do it "should set the created_at and created_on timestamps for new records" do
ActiveRecord::Base.default_timezone
assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.created_at.strftime("%H:%M") assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.created_at.strftime("%H:%M")
end
it "should set the created_on column for new records respecting the time zone" do
assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.created_on.strftime("%H:%M") assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.created_on.strftime("%H:%M")
end end
it "should set the updated_at column for new records respecting the time zone" do it "should set the updated_at and updated_on timestamps for new records" do
assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.updated_at.strftime("%H:%M") assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.updated_at.strftime("%H:%M")
end
it "should set the updated_on column for new records respecting the time zone" do
assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.updated_on.strftime("%H:%M") assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @book.updated_on.strftime("%H:%M")
end end
end end
end end
context "importing with database reserved words" do context "importing with database reserved words" do
let(:group) { Build(:group, :order => "superx") } let(:group) { Build(:group, :order => "superx") }
it "should import just fine" do it "should import just fine" do
assert_difference "Group.count", +1 do assert_difference "Group.count", +1 do
result = Group.import [group] result = Group.import [group]
assert_equal 1, result.num_inserts
end end
assert_equal "superx", Group.first.order assert_equal "superx", Group.first.order
end end

View file

@ -2,36 +2,25 @@ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
require "ar-extensions/import/mysql" require "ar-extensions/import/mysql"
describe "#import with :on_duplicate_key_update option (mysql specific functionality)" do describe "#import with :on_duplicate_key_update option (mysql specific functionality)" do
extend ActiveSupport::TestCase::MySQLAssertions
asssertion_group(:should_support_on_duplicate_key_update) do
should_not_update_fields_not_mentioned
should_update_foreign_keys
should_not_update_created_at_on_timestamp_columns
should_update_updated_at_on_timestamp_columns
end
macro(:perform_import){ raise "supply your own #perform_import in a context below" } macro(:perform_import){ raise "supply your own #perform_import in a context below" }
macro(:updated_topic){ Topic.find(@topic) }
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 context "given columns and values with :validation checks turned off" do
let(:columns){ %w( id title author_name author_email_address parent_id ) } let(:columns){ %w( id title author_name author_email_address parent_id ) }
let(:values){ [ [ 99, "Book", "John Doe", "john@doe.com", 17 ] ] } 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 ] ] } let(:updated_values){ [ [ 99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57 ] ] }
macro(:perform_import) do macro(:perform_import) do |*opts|
Topic.import columns, updated_values, :on_duplicate_key_update => update_columns, :validate => false Topic.import columns, updated_values, opts.extract_options!.merge(:on_duplicate_key_update => update_columns, :validate => false)
end end
setup do setup do
@ -41,54 +30,48 @@ describe "#import with :on_duplicate_key_update option (mysql specific functiona
context "using string column names" do context "using string column names" do
let(:update_columns){ [ "title", "author_email_address", "parent_id" ] } let(:update_columns){ [ "title", "author_email_address", "parent_id" ] }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned should_update_fields_mentioned
should_update_foreign_keys
end end
context "using symbol column names" do context "using symbol column names" do
let(:update_columns){ [ :title, :author_email_address, :parent_id ] } let(:update_columns){ [ :title, :author_email_address, :parent_id ] }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned should_update_fields_mentioned
should_update_foreign_keys
end end
context "using string hash map" do context "using string hash map" do
let(:update_columns){ { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } } let(:update_columns){ { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned should_update_fields_mentioned
should_update_foreign_keys
end end
context "using string hash map, but specifying column mismatches" do 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" } } let(:update_columns){ { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned_with_hash_mappings should_update_fields_mentioned_with_hash_mappings
should_update_foreign_keys
end end
context "using symbol hash map" do context "using symbol hash map" do
let(:update_columns){ { :title => :title, :author_email_address => :author_email_address, :parent_id => :parent_id } } let(:update_columns){ { :title => :title, :author_email_address => :author_email_address, :parent_id => :parent_id } }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned should_update_fields_mentioned
should_update_foreign_keys
end end
context "using symbol hash map, but specifying column mismatches" do 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 } } let(:update_columns){ { :title => :author_email_address, :author_email_address => :title, :parent_id => :parent_id } }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned_with_hash_mappings should_update_fields_mentioned_with_hash_mappings
should_update_foreign_keys
end end
end end
context "given array of model instances with :validation checks turned off" do context "given array of model instances with :validation checks turned off" do
macro(:perform_import) do macro(:perform_import) do |*opts|
@topic.title = "Book - 2nd Edition" @topic.title = "Book - 2nd Edition"
@topic.author_name = "Author Should Not Change" @topic.author_name = "Author Should Not Change"
@topic.author_email_address = "johndoe@example.com" @topic.author_email_address = "johndoe@example.com"
@topic.parent_id = 57 @topic.parent_id = 57
Topic.import [@topic], :on_duplicate_key_update => update_columns, :validate => false Topic.import [@topic], opts.extract_options!.merge(:on_duplicate_key_update => update_columns, :validate => false)
end end
setup do setup do
@ -97,44 +80,38 @@ describe "#import with :on_duplicate_key_update option (mysql specific functiona
context "using string column names" do context "using string column names" do
let(:update_columns){ [ "title", "author_email_address", "parent_id" ] } let(:update_columns){ [ "title", "author_email_address", "parent_id" ] }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned should_update_fields_mentioned
should_update_foreign_keys
end end
context "using symbol column names" do context "using symbol column names" do
let(:update_columns){ [ :title, :author_email_address, :parent_id ] } let(:update_columns){ [ :title, :author_email_address, :parent_id ] }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned should_update_fields_mentioned
should_update_foreign_keys
end end
context "using string hash map" do context "using string hash map" do
let(:update_columns){ { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } } let(:update_columns){ { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned should_update_fields_mentioned
should_update_foreign_keys
end end
context "using string hash map, but specifying column mismatches" do 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" } } let(:update_columns){ { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned_with_hash_mappings should_update_fields_mentioned_with_hash_mappings
should_update_foreign_keys
end end
context "using symbol hash map" do context "using symbol hash map" do
let(:update_columns){ { :title => :title, :author_email_address => :author_email_address, :parent_id => :parent_id } } let(:update_columns){ { :title => :title, :author_email_address => :author_email_address, :parent_id => :parent_id } }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned should_update_fields_mentioned
should_update_foreign_keys
end end
context "using symbol hash map, but specifying column mismatches" do 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 } } let(:update_columns){ { :title => :author_email_address, :author_email_address => :title, :parent_id => :parent_id } }
should_not_update_fields_not_mentioned should_support_on_duplicate_key_update
should_update_fields_mentioned_with_hash_mappings should_update_fields_mentioned_with_hash_mappings
should_update_foreign_keys
end end
end end

View file

@ -23,7 +23,9 @@ ActiveRecord::Schema.define do
t.column :parent_id, :integer t.column :parent_id, :integer
t.column :type, :string t.column :type, :string
t.column :created_at, :datetime t.column :created_at, :datetime
t.column :created_on, :datetime
t.column :updated_at, :datetime t.column :updated_at, :datetime
t.column :updated_on, :datetime
end end
create_table :projects, :force=>true do |t| create_table :projects, :force=>true do |t|

View file

@ -0,0 +1,55 @@
class ActiveSupport::TestCase
module MySQLAssertions
def self.extended(klass)
klass.instance_eval do
assertion(:should_not_update_created_at_on_timestamp_columns) do
Delorean.time_travel_to("5 minutes from now") do
perform_import
assert_equal @topic.created_at.to_i, updated_topic.created_at.to_i
assert_equal @topic.created_on.to_i, updated_topic.created_on.to_i
end
end
assertion(:should_update_updated_at_on_timestamp_columns) do
time = Chronic.parse("5 minutes from now")
Delorean.time_travel_to(time) do
perform_import
assert_equal time.to_i, updated_topic.updated_at.to_i
assert_equal time.to_i, updated_topic.updated_on.to_i
end
end
assertion(:should_not_update_timestamps) do
Delorean.time_travel_to("5 minutes from now") do
perform_import :timestamps => false
assert_equal @topic.created_at.to_i, updated_topic.created_at.to_i
assert_equal @topic.created_on.to_i, updated_topic.created_on.to_i
assert_equal @topic.updated_at.to_i, updated_topic.updated_at.to_i
assert_equal @topic.updated_on.to_i, updated_topic.updated_on.to_i
end
end
assertion(:should_not_update_fields_not_mentioned) do
assert_equal "John Doe", updated_topic.author_name
end
assertion(:should_update_fields_mentioned) do
perform_import
assert_equal "Book - 2nd Edition", updated_topic.title
assert_equal "johndoe@example.com", updated_topic.author_email_address
end
assertion(:should_update_fields_mentioned_with_hash_mappings) do
perform_import
assert_equal "johndoe@example.com", updated_topic.title
assert_equal "Book - 2nd Edition", updated_topic.author_email_address
end
assertion(:should_update_foreign_keys) do
perform_import
assert_equal 57, updated_topic.parent_id
end
end
end
end
end

View file

@ -30,6 +30,13 @@ class ActiveSupport::TestCase
end end
end end
end end
def asssertion_group(name, &block)
mc = class << self ; self ; end
mc.class_eval do
define_method(name, &block)
end
end
def macro(name, &block) def macro(name, &block)
class_eval do class_eval do
@ -40,11 +47,20 @@ class ActiveSupport::TestCase
def describe(description, toplevel=nil, &blk) def describe(description, toplevel=nil, &blk)
text = toplevel ? description : "#{name} #{description}" text = toplevel ? description : "#{name} #{description}"
klass = Class.new(self) klass = Class.new(self)
klass.class_eval <<-RUBY_EVAL klass.class_eval <<-RUBY_EVAL
def self.name def self.name
"#{text}" "#{text}"
end end
RUBY_EVAL RUBY_EVAL
# do not inherit test methods from the superclass
klass.class_eval do
instance_methods.grep(/^test.+/) do |method|
undef_method method
end
end
klass.instance_eval &blk klass.instance_eval &blk
end end
alias_method :context, :describe alias_method :context, :describe