From 4d424be48503538793967ccd85d429febdb1ac70 Mon Sep 17 00:00:00 2001 From: Robert Mathews Date: Wed, 3 Jul 2013 11:41:25 -0400 Subject: [PATCH 01/11] return the inserted ids for postgres bulk insert --- .../adapters/abstract_adapter.rb | 7 ++++--- lib/activerecord-import/import.rb | 14 +++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/activerecord-import/adapters/abstract_adapter.rb b/lib/activerecord-import/adapters/abstract_adapter.rb index e4056857..2d420423 100644 --- a/lib/activerecord-import/adapters/abstract_adapter.rb +++ b/lib/activerecord-import/adapters/abstract_adapter.rb @@ -55,21 +55,22 @@ def insert_many( sql, values, *args ) # :nodoc: max = max_allowed_packet + ids=[] # if we can insert it all as one statement if NO_MAX_PACKET == max or total_bytes < max number_of_inserts += 1 sql2insert = base_sql + values.join( ',' ) + post_sql - insert( sql2insert, *args ) + ids = insert( sql2insert, *args ) else value_sets = self.class.get_insert_value_sets( values, sql_size, max ) value_sets.each do |values| number_of_inserts += 1 sql2insert = base_sql + values.join( ',' ) + post_sql - insert( sql2insert, *args ) + ids.concat(select_many( sql2insert)) end end - number_of_inserts + [number_of_inserts,ids] end def pre_sql_statements(options) diff --git a/lib/activerecord-import/import.rb b/lib/activerecord-import/import.rb index 94b9cd8e..89602853 100644 --- a/lib/activerecord-import/import.rb +++ b/lib/activerecord-import/import.rb @@ -218,8 +218,8 @@ def import( *args ) return_obj = if is_validating import_with_validations( column_names, array_of_attributes, options ) else - num_inserts = import_without_validations_or_callbacks( column_names, array_of_attributes, options ) - ActiveRecord::Import::Result.new([], num_inserts) + (num_inserts, ids) = import_without_validations_or_callbacks( column_names, array_of_attributes, options ) + ActiveRecord::Import::Result.new([], num_inserts, ids) end if options[:synchronize] @@ -261,12 +261,12 @@ def import_with_validations( column_names, array_of_attributes, options={} ) end array_of_attributes.compact! - num_inserts = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any? - 0 + (num_inserts, ids) = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any? + [0,[]] else import_without_validations_or_callbacks( column_names, array_of_attributes, options ) end - ActiveRecord::Import::Result.new(failed_instances, num_inserts) + ActiveRecord::Import::Result.new(failed_instances, num_inserts, ids) end # Imports the passed in +column_names+ and +array_of_attributes+ @@ -309,11 +309,11 @@ def import_without_validations_or_callbacks( column_names, array_of_attributes, post_sql_statements = connection.post_sql_statements( quoted_table_name, options ) # perform the inserts - number_inserted = connection.insert_many( [ insert_sql, post_sql_statements ].flatten, + (number_inserted,ids) = connection.insert_many( [ insert_sql, post_sql_statements ].flatten, values_sql, "#{self.class.name} Create Many Without Validations Or Callbacks" ) end - number_inserted + [number_inserted, ids] end private From 53ca11b19427a327d8ebd84151903a6ab6f7e126 Mon Sep 17 00:00:00 2001 From: Robert Mathews Date: Wed, 3 Jul 2013 12:07:25 -0400 Subject: [PATCH 02/11] passes postgres tests. Don't see any tests specify for checking the result of import?? --- lib/activerecord-import/import.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activerecord-import/import.rb b/lib/activerecord-import/import.rb index 89602853..c520b450 100644 --- a/lib/activerecord-import/import.rb +++ b/lib/activerecord-import/import.rb @@ -3,7 +3,7 @@ module ActiveRecord::Import::ConnectionAdapters ; end module ActiveRecord::Import #:nodoc: - class Result < Struct.new(:failed_instances, :num_inserts) + class Result < Struct.new(:failed_instances, :num_inserts, :ids) end module ImportSupport #:nodoc: From 63641983b03d0dd3c5ad5d680a89fe47180e0efe Mon Sep 17 00:00:00 2001 From: Robert Mathews Date: Wed, 3 Jul 2013 13:49:56 -0400 Subject: [PATCH 03/11] place the id return into every model and mark the model as clean? (since it has been saved) --- .../active_record/adapters/postgresql_adapter.rb | 6 ++++++ lib/activerecord-import/adapters/abstract_adapter.rb | 4 ++-- lib/activerecord-import/import.rb | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb b/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb index 6a17b08f..1053d54c 100644 --- a/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +++ b/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb @@ -3,5 +3,11 @@ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter include ActiveRecord::Import::PostgreSQLAdapter + + alias_method :post_sql_statements_orig, :post_sql_statements + def post_sql_statements( table_name, options ) # :nodoc: + post_sql_statements_orig(table_name, options).append(" RETURNING ID") + end + end diff --git a/lib/activerecord-import/adapters/abstract_adapter.rb b/lib/activerecord-import/adapters/abstract_adapter.rb index 2d420423..e4662fed 100644 --- a/lib/activerecord-import/adapters/abstract_adapter.rb +++ b/lib/activerecord-import/adapters/abstract_adapter.rb @@ -60,13 +60,13 @@ def insert_many( sql, values, *args ) # :nodoc: if NO_MAX_PACKET == max or total_bytes < max number_of_inserts += 1 sql2insert = base_sql + values.join( ',' ) + post_sql - ids = insert( sql2insert, *args ) + ids = select_values( sql2insert) else value_sets = self.class.get_insert_value_sets( values, sql_size, max ) value_sets.each do |values| number_of_inserts += 1 sql2insert = base_sql + values.join( ',' ) + post_sql - ids.concat(select_many( sql2insert)) + ids.concat(select_values( sql2insert)) end end diff --git a/lib/activerecord-import/import.rb b/lib/activerecord-import/import.rb index c520b450..423f0bdb 100644 --- a/lib/activerecord-import/import.rb +++ b/lib/activerecord-import/import.rb @@ -226,7 +226,11 @@ def import( *args ) sync_keys = options[:synchronize_keys] || [self.primary_key] synchronize( options[:synchronize], sync_keys) end - + # if we have ids, then set the id on the models and mark the models as clean. + return_obj.ids.each_with_index do |obj, index| + models[index].id = obj.to_i + models[index].instance_variable_get(:@changed_attributes).clear # mark the model as saved + end return_obj.num_inserts = 0 if return_obj.num_inserts.nil? return_obj end From 90b613d9764853553962951c5458b196dad75462 Mon Sep 17 00:00:00 2001 From: Robert Mathews Date: Fri, 5 Jul 2013 09:39:24 -0400 Subject: [PATCH 04/11] make the patch work for non-standard primary keys --- .../active_record/adapters/postgresql_adapter.rb | 6 +++++- lib/activerecord-import/import.rb | 10 ++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb b/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb index 1053d54c..af815ee3 100644 --- a/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +++ b/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb @@ -6,7 +6,11 @@ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter alias_method :post_sql_statements_orig, :post_sql_statements def post_sql_statements( table_name, options ) # :nodoc: - post_sql_statements_orig(table_name, options).append(" RETURNING ID") + unless options[:primary_key].blank? + post_sql_statements_orig(table_name, options).append(" RETURNING #{options[:primary_key]}") + else + post_sql_statements_orig(table_name, options) + end end end diff --git a/lib/activerecord-import/import.rb b/lib/activerecord-import/import.rb index 423f0bdb..7505e98c 100644 --- a/lib/activerecord-import/import.rb +++ b/lib/activerecord-import/import.rb @@ -166,7 +166,7 @@ def supports_on_duplicate_key_update? # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed. # * num_inserts - the number of insert statements it took to import the data def import( *args ) - options = { :validate=>true, :timestamps=>true } + options = { :validate=>true, :timestamps=>true, :primary_key=>primary_key } options.merge!( args.pop ) if args.last.is_a? Hash is_validating = options.delete( :validate ) @@ -227,9 +227,11 @@ def import( *args ) synchronize( options[:synchronize], sync_keys) end # if we have ids, then set the id on the models and mark the models as clean. - return_obj.ids.each_with_index do |obj, index| - models[index].id = obj.to_i - models[index].instance_variable_get(:@changed_attributes).clear # mark the model as saved + unless models.nil? + return_obj.ids.each_with_index do |obj, index| + models[index].id = obj.to_i + models[index].instance_variable_get(:@changed_attributes).clear # mark the model as saved + end end return_obj.num_inserts = 0 if return_obj.num_inserts.nil? return_obj From 77902706135380d21a007dde9f6b3facb97e9acf Mon Sep 17 00:00:00 2001 From: Robert Mathews Date: Fri, 5 Jul 2013 11:45:07 -0400 Subject: [PATCH 05/11] modification showing how save a tree of objects efficiently. --- lib/activerecord-import/import.rb | 37 +++++++++++++++++++++- test/import_test.rb | 51 +++++++++++++++++++------------ test/models/book.rb | 2 +- test/models/topic.rb | 2 +- test/support/factories.rb | 8 +++++ 5 files changed, 78 insertions(+), 22 deletions(-) diff --git a/lib/activerecord-import/import.rb b/lib/activerecord-import/import.rb index 7505e98c..11e10fc9 100644 --- a/lib/activerecord-import/import.rb +++ b/lib/activerecord-import/import.rb @@ -165,7 +165,42 @@ def supports_on_duplicate_key_update? # This returns an object which responds to +failed_instances+ and +num_inserts+. # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed. # * num_inserts - the number of insert statements it took to import the data - def import( *args ) + def import(*args) + if args.first.is_a?( Array ) and args.first.first.is_a? ActiveRecord::Base + models = args.first # the import argument parsing is too tangled for me ... I only want to prove the concept of saving recursively + result = import_helper(*args) + # now, for all the dirty associations, collect them into a new set of models, then recurse. + # notes: + # does not handle associations that reference themselves + # assumes that the only associations to be saved are marked with :autosave + # should probably take a hash to associations to follow. + hash={} + models.map do |val| add_objects(hash, val) end + hash.each_pair do |class_name, assocs| + clazz=Module.const_get(class_name) + assocs.each_pair do |assoc_name, subobjects| + subobjects.first.class.import(subobjects) unless subobjects.empty? + end + end + result + else + import_helper(*args) + end + end + + def add_objects(hash, val) + hash[val.class.name]||={} + val.class.reflect_on_all_autosave_associations.each do |assoc| + hash[val.class.name][assoc.name]||=[] + changed_objects = val.association(assoc.name).proxy.select {|a| a.new_record? || a.changed?} + changed_objects.each {|obj| add_objects(hash, obj)} + hash[val.class.name][assoc.name].concat changed_objects + changed_objects.each + end + hash + end + + def import_helper( *args ) options = { :validate=>true, :timestamps=>true, :primary_key=>primary_key } options.merge!( args.pop ) if args.last.is_a? Hash diff --git a/test/import_test.rb b/test/import_test.rb index 50ea2ac8..cb322bbf 100644 --- a/test/import_test.rb +++ b/test/import_test.rb @@ -11,14 +11,14 @@ assert result.num_inserts > 0 end end - + it "should not produce an error when importing empty arrays" do assert_nothing_raised do Topic.import [] Topic.import %w(title author_name), [] end end - + describe "with non-default ActiveRecord models" do context "that have a non-standard primary key (that is no sequence)" do it "should import models successfully" do @@ -28,7 +28,7 @@ end end end - + context "with :validation option" do let(:columns) { %w(title author_name) } let(:valid_values) { [[ "LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] } @@ -46,7 +46,7 @@ result = Topic.import columns, invalid_values, :validate => false end end - + it 'should raise a specific error if a column does not exist' do assert_raises ActiveRecord::Import::MissingColumnError do Topic.import ['foo'], [['bar']], :validate => false @@ -81,45 +81,45 @@ end end end - + context "with :all_or_none option" do let(:columns) { %w(title author_name) } let(:valid_values) { [[ "LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] } let(:invalid_values) { [[ "The RSpec Book", ""], ["Agile+UX", ""]] } let(:mixed_values) { valid_values + invalid_values } - + context "with validation checks turned on" do it "should import valid data" do assert_difference "Topic.count", +2 do result = Topic.import columns, valid_values, :all_or_none => true end end - + it "should not import invalid data" do assert_no_difference "Topic.count" do result = Topic.import columns, invalid_values, :all_or_none => true end end - + it "should not import valid data when mixed with invalid data" do assert_no_difference "Topic.count" do result = Topic.import columns, mixed_values, :all_or_none => true end end - + it "should report the failed instances" do results = Topic.import columns, mixed_values, :all_or_none => true assert_equal invalid_values.size, results.failed_instances.size results.failed_instances.each { |e| assert_kind_of Topic, e } end - + it "should report the zero inserts" do results = Topic.import columns, mixed_values, :all_or_none => true assert_equal 0, results.num_inserts end end end - + context "with :synchronize option" do context "synchronizing on new records" do let(:new_topics) { Build(3, :topics) } @@ -132,16 +132,16 @@ context "synchronizing on new records with explicit conditions" do let(:new_topics) { Build(3, :topics) } - + it "reloads data for existing in-memory instances" do Topic.import(new_topics, :synchronize => new_topics, :synchronize_keys => [:title] ) assert new_topics.all?(&:persisted?), "Records should have been reloaded" end end - + context "synchronizing on destroyed records with explicit conditions" do let(:new_topics) { Generate(3, :topics) } - + it "reloads data for existing in-memory instances" do new_topics.each &:destroy Topic.import(new_topics, :synchronize => new_topics, :synchronize_keys => [:title] ) @@ -296,14 +296,14 @@ assert_equal "superx", Group.first.order end end - + context "importing a datetime field" do it "should import a date with YYYY/MM/DD format just fine" do Topic.import [:author_name, :title, :last_read], [["Bob Jones", "Topic 2", "2010/05/14"]] assert_equal "2010/05/14".to_date, Topic.last.last_read.to_date end end - + context "importing through an association scope" do [ true, false ].each do |b| context "when validation is " + (b ? "enabled" : "disabled") do @@ -317,7 +317,7 @@ end end end - + describe "importing when model has default_scope" do it "doesn't import the default scope values" do assert_difference "Widget.unscoped.count", +2 do @@ -327,7 +327,7 @@ assert_not_equal default_scope_value, Widget.unscoped.find_by_w_id(1) assert_not_equal default_scope_value, Widget.unscoped.find_by_w_id(2) end - + it "imports columns that are a part of the default scope using the value specified" do assert_difference "Widget.unscoped.count", +2 do Widget.import [:w_id, :active], [[1, true], [2, false]] @@ -336,7 +336,7 @@ assert_not_equal false, Widget.unscoped.find_by_w_id(2) end end - + describe "importing serialized fields" do it "imports values for serialized fields" do assert_difference "Widget.unscoped.count", +1 do @@ -345,5 +345,18 @@ assert_equal({:a => :b}, Widget.find_by_w_id(1).data) end end + describe "importing objects with subobjects" do + let(:new_topics) { Build(3, :topic_with_book) } + it 'imports top level' do + assert_difference "Topic.count", +3 do + Topic.import new_topics + end + end + it 'imports subobjects' do + assert_difference "Book.count", +3 do + Topic.import new_topics + end + end + end end diff --git a/test/models/book.rb b/test/models/book.rb index 9e5ad90f..8d74fed1 100644 --- a/test/models/book.rb +++ b/test/models/book.rb @@ -1,3 +1,3 @@ class Book < ActiveRecord::Base - belongs_to :topic + belongs_to :topic, :inverse_of=>:books end diff --git a/test/models/topic.rb b/test/models/topic.rb index 091bb8b9..fb78ecbe 100644 --- a/test/models/topic.rb +++ b/test/models/topic.rb @@ -1,6 +1,6 @@ class Topic < ActiveRecord::Base validates_presence_of :author_name - has_many :books + has_many :books, :autosave=>true, :inverse_of=>:topic belongs_to :parent, :class_name => "Topic" composed_of :description, :mapping => [ %w(title title), %w(author_name author_name)], :allow_nil => true, :class_name => "TopicDescription" diff --git a/test/support/factories.rb b/test/support/factories.rb index 34d83e8a..ecd5eb6f 100644 --- a/test/support/factories.rb +++ b/test/support/factories.rb @@ -1,3 +1,7 @@ +Factory.sequence :book_title do |n| + "Book #{n}" +end + Factory.define :group do |m| m.sequence(:order) { |n| "Order #{n}" } end @@ -12,6 +16,10 @@ m.sequence(:author_name){ |n| "Author #{n}"} end +Factory.define :topic_with_book, :parent=>:topic do |m| + m.after_build { |topic| 1.times {|y| topic.books.build(:title=>Factory.next(:book_title), :author_name=>'Stephen King') }} +end + Factory.define :widget do |m| m.sequence(:w_id){ |n| n} end From e1fb297661425212577c67da006a20928db9083f Mon Sep 17 00:00:00 2001 From: John Naegle Date: Thu, 15 Aug 2013 14:25:31 -0500 Subject: [PATCH 06/11] - pass options down into sub-objects that are imported - rename a few variables (obj => model, val => parent) for example - set the foreign key on a sub-collection when the parent is imported - tests for importing 3 levels of objects - remove extra recursion in add_objects - new model for testing 3 levels of objects: chapter --- lib/activerecord-import/import.rb | 27 +++++++++----- test/import_test.rb | 61 +++++++++++++++++++++++++++++-- test/models/book.rb | 1 + test/models/chapter.rb | 4 ++ test/schema/generic_schema.rb | 9 ++++- test/support/factories.rb | 13 ++++++- 6 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 test/models/chapter.rb diff --git a/lib/activerecord-import/import.rb b/lib/activerecord-import/import.rb index 11e10fc9..1874edd2 100644 --- a/lib/activerecord-import/import.rb +++ b/lib/activerecord-import/import.rb @@ -167,19 +167,23 @@ def supports_on_duplicate_key_update? # * num_inserts - the number of insert statements it took to import the data def import(*args) if args.first.is_a?( Array ) and args.first.first.is_a? ActiveRecord::Base + options = {} + options.merge!( args.pop ) if args.last.is_a?(Hash) + models = args.first # the import argument parsing is too tangled for me ... I only want to prove the concept of saving recursively - result = import_helper(*args) + result = import_helper(models, options) # now, for all the dirty associations, collect them into a new set of models, then recurse. # notes: # does not handle associations that reference themselves # assumes that the only associations to be saved are marked with :autosave # should probably take a hash to associations to follow. hash={} - models.map do |val| add_objects(hash, val) end + models.each {|model| add_objects(hash, model) } + hash.each_pair do |class_name, assocs| clazz=Module.const_get(class_name) assocs.each_pair do |assoc_name, subobjects| - subobjects.first.class.import(subobjects) unless subobjects.empty? + subobjects.first.class.import(subobjects, options) unless subobjects.empty? end end result @@ -188,13 +192,16 @@ def import(*args) end end - def add_objects(hash, val) - hash[val.class.name]||={} - val.class.reflect_on_all_autosave_associations.each do |assoc| - hash[val.class.name][assoc.name]||=[] - changed_objects = val.association(assoc.name).proxy.select {|a| a.new_record? || a.changed?} - changed_objects.each {|obj| add_objects(hash, obj)} - hash[val.class.name][assoc.name].concat changed_objects + def add_objects(hash, parent) + hash[parent.class.name]||={} + parent.class.reflect_on_all_autosave_associations.each do |assoc| + hash[parent.class.name][assoc.name]||=[] + + changed_objects = parent.association(assoc.name).proxy.select {|a| a.new_record? || a.changed?} + changed_objects.each do |child| + child.send("#{assoc.foreign_key}=", parent.id) + end + hash[parent.class.name][assoc.name].concat changed_objects changed_objects.each end hash diff --git a/test/import_test.rb b/test/import_test.rb index cb322bbf..7865610a 100644 --- a/test/import_test.rb +++ b/test/import_test.rb @@ -345,16 +345,69 @@ assert_equal({:a => :b}, Widget.find_by_w_id(1).data) end end + describe "importing objects with subobjects" do - let(:new_topics) { Build(3, :topic_with_book) } + + let(:new_topics) { Build(num_topics, :topic_with_book) } + let(:new_topics_with_invalid_chapter) { + chapter = new_topics.first.books.first.chapters.first + chapter.title = nil + new_topics + } + let(:num_topics) {3} + let(:num_books) {6} + let(:num_chapters) {18} + it 'imports top level' do - assert_difference "Topic.count", +3 do + assert_difference "Topic.count", +num_topics do + Topic.import new_topics + end + end + it 'imports first level sub-objects' do + assert_difference "Book.count", +num_books do Topic.import new_topics + new_topics.each do |topic| + topic.books.each do |book| + assert_equal topic.id, book.topic_id + end + end end end - it 'imports subobjects' do - assert_difference "Book.count", +3 do + it 'imports second level sub-objects' do + assert_difference "Chapter.count", +num_chapters do Topic.import new_topics + new_topics.each do |topic| + topic.books.each do |book| + book.chapters.each do |chapter| + assert_equal book.id, chapter.book_id + end + end + end + end + end + + it "skips validation of the subobjects if requested" do + assert_difference "Chapter.count", +num_chapters do + Topic.import new_topics_with_invalid_chapter, :validate => false + end + end + + # These models dont validate associated. So we expect that books and topics get inserted, but not chapters + # Putting a transaction around everything wouldn't work, so if you want your chapters to prevent topics from + # being created, you would need to have validates_assoicated in your models and insert with validation + describe "all_or_none" do + [Book, Topic].each do |type| + it "creates #{type.to_s}" do + assert_difference "#{type.to_s}.count", send("num_#{type.to_s.downcase}s") do + Topic.import new_topics_with_invalid_chapter, :all_or_none => true + end + end + it "doesn't create chapters" do + assert_difference "Chapter.count", 0 do + Topic.import new_topics_with_invalid_chapter, :all_or_none => true + end + + end end end end diff --git a/test/models/book.rb b/test/models/book.rb index 8d74fed1..39304950 100644 --- a/test/models/book.rb +++ b/test/models/book.rb @@ -1,3 +1,4 @@ class Book < ActiveRecord::Base belongs_to :topic, :inverse_of=>:books + has_many :chapters, :autosave => true, :inverse_of => :book end diff --git a/test/models/chapter.rb b/test/models/chapter.rb new file mode 100644 index 00000000..671667bc --- /dev/null +++ b/test/models/chapter.rb @@ -0,0 +1,4 @@ +class Chapter < ActiveRecord::Base + belongs_to :book, :inverse_of=>:chapters + validates :title, :presence => true +end diff --git a/test/schema/generic_schema.rb b/test/schema/generic_schema.rb index d1f6deae..43221e47 100644 --- a/test/schema/generic_schema.rb +++ b/test/schema/generic_schema.rb @@ -65,7 +65,14 @@ t.column :topic_id, :integer t.column :for_sale, :boolean, :default => true end - + + create_table :chapters, :force => true do |t| + t.column :title, :string + t.column :book_id, :integer, :null => false + t.column :created_at, :datetime + t.column :updated_at, :datetime + end + create_table :languages, :force=>true do |t| t.column :name, :string t.column :developer_id, :integer diff --git a/test/support/factories.rb b/test/support/factories.rb index ecd5eb6f..118e5677 100644 --- a/test/support/factories.rb +++ b/test/support/factories.rb @@ -2,6 +2,10 @@ "Book #{n}" end +Factory.sequence :chapter_title do |n| + "Chapter #{n}" +end + Factory.define :group do |m| m.sequence(:order) { |n| "Order #{n}" } end @@ -17,7 +21,14 @@ end Factory.define :topic_with_book, :parent=>:topic do |m| - m.after_build { |topic| 1.times {|y| topic.books.build(:title=>Factory.next(:book_title), :author_name=>'Stephen King') }} + m.after_build do |topic| + 2.times do + book = topic.books.build(:title=>Factory.next(:book_title), :author_name=>'Stephen King') + 3.times do + book.chapters.build(:title => Factory.next(:chapter_title)) + end + end + end end Factory.define :widget do |m| From 88422a5f47ed7f44855894b6032876fe76221bab Mon Sep 17 00:00:00 2001 From: John Naegle Date: Tue, 29 Oct 2013 15:37:42 -0500 Subject: [PATCH 07/11] merge in upstream master --- .gitignore | 4 +- Brewfile | 3 + Gemfile | 71 ++++++------ Gemfile.lock | 85 --------------- README.markdown | 10 +- Rakefile | 25 +---- VERSION | 1 - activerecord-import.gemspec | 78 +++----------- gemfiles/3.1.gemfile | 4 + gemfiles/3.2.gemfile | 4 + gemfiles/4.0.gemfile | 4 + .../adapters/abstract_adapter.rb | 1 - .../adapters/em_mysql2_adapter.rb | 8 ++ .../adapters/abstract_adapter.rb | 65 ++--------- .../adapters/em_mysql2_adapter.rb | 5 + .../adapters/mysql_adapter.rb | 53 ++++++++- .../adapters/sqlite3_adapter.rb | 28 ++++- lib/activerecord-import/base.rb | 5 +- lib/activerecord-import/em_mysql2.rb | 7 ++ lib/activerecord-import/import.rb | 56 +++++----- lib/activerecord-import/synchronize.rb | 21 ++-- lib/activerecord-import/value_sets_parser.rb | 54 ++++++++++ lib/activerecord-import/version.rb | 5 + test/active_record/connection_adapter_test.rb | 62 ----------- test/adapters/em_mysql2.rb | 1 + test/database.yml.sample | 12 ++- test/em_mysql2/import_test.rb | 6 ++ test/import_test.rb | 89 +++++++-------- test/schema/generic_schema.rb | 8 +- test/schema/mysql_schema.rb | 2 +- test/sqlite3/import_test.rb | 33 ++++-- test/support/factories.rb | 53 +++++---- test/support/generate.rb | 14 +-- test/support/mysql/import_examples.rb | 101 +++++------------- test/synchronize_test.rb | 4 +- test/test_helper.rb | 9 +- test/value_sets_bytes_parser_test.rb | 96 +++++++++++++++++ test/value_sets_records_parser_test.rb | 32 ++++++ 38 files changed, 564 insertions(+), 555 deletions(-) create mode 100644 Brewfile delete mode 100644 Gemfile.lock delete mode 100644 VERSION create mode 100644 gemfiles/3.1.gemfile create mode 100644 gemfiles/3.2.gemfile create mode 100644 gemfiles/4.0.gemfile create mode 100644 lib/activerecord-import/active_record/adapters/em_mysql2_adapter.rb create mode 100644 lib/activerecord-import/adapters/em_mysql2_adapter.rb create mode 100644 lib/activerecord-import/em_mysql2.rb create mode 100644 lib/activerecord-import/value_sets_parser.rb create mode 100644 lib/activerecord-import/version.rb delete mode 100644 test/active_record/connection_adapter_test.rb create mode 100644 test/adapters/em_mysql2.rb create mode 100644 test/em_mysql2/import_test.rb create mode 100644 test/value_sets_bytes_parser_test.rb create mode 100644 test/value_sets_records_parser_test.rb diff --git a/.gitignore b/.gitignore index 29d0c49d..fdf9a3d0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ tmtags coverage rdoc pkg +*.gem +*.lock ## PROJECT::SPECIFIC log/*.log @@ -26,4 +28,4 @@ test/database.yml .bundle/ .redcar/ .rvmrc -docsite/ \ No newline at end of file +docsite/ diff --git a/Brewfile b/Brewfile new file mode 100644 index 00000000..fe64f394 --- /dev/null +++ b/Brewfile @@ -0,0 +1,3 @@ +mysql +postgresql +sqlite \ No newline at end of file diff --git a/Gemfile b/Gemfile index cc464825..a917b6fe 100644 --- a/Gemfile +++ b/Gemfile @@ -1,42 +1,39 @@ -source :gemcutter +source 'https://rubygems.org' -gem "activerecord", "~> 3.0" +gemspec -group :development do - gem "rake" - gem "jeweler", ">= 1.4.0" +# Database Adapters +platforms :ruby do + gem "em-synchrony", "~> 1.0.3" + gem "mysql2", "~> 0.3.0" + gem "pg", "~> 0.9" + gem "sqlite3-ruby", "~> 1.3.1" + gem "seamless_database_pool", "~> 1.0.13" end -group :test do - # Database Adapters - platforms :ruby do - gem "mysql", "~> 2.8.1" - gem "mysql2", "~> 0.3.0" - gem "pg", "~> 0.9" - gem "sqlite3-ruby", "~> 1.3.1" - gem "seamless_database_pool", "~> 1.0.11" - end - - platforms :jruby do - gem "jdbc-mysql" - gem "activerecord-jdbcmysql-adapter" - end - - # Support libs - gem "factory_girl", "~> 1.3.3" - gem "delorean", "~> 0.2.0" - - # Debugging - platforms :mri_18 do - gem "ruby-debug", "= 0.10.4" - end - - platforms :jruby do - gem "ruby-debug-base", "= 0.10.4" - gem "ruby-debug", "= 0.10.4" - end - - platforms :mri_19 do - gem "debugger" - end +platforms :jruby do + gem "jdbc-mysql" + gem "activerecord-jdbcmysql-adapter" end + +# Support libs +gem "factory_girl", "~> 4.2.0" +gem "delorean", "~> 0.2.0" + +# Debugging +platforms :mri_18 do + gem "ruby-debug", "= 0.10.4" +end + +platforms :jruby do + gem "ruby-debug-base", "= 0.10.4" + gem "ruby-debug", "= 0.10.4" +end + +platforms :mri_19 do + gem "debugger" +end + +version = ENV['AR_VERSION'] || "3.2" + +eval_gemfile File.expand_path("../gemfiles/#{version}.gemfile", __FILE__) diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index b3cf6b4d..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,85 +0,0 @@ -GEM - remote: http://rubygems.org/ - specs: - activemodel (3.2.7) - activesupport (= 3.2.7) - builder (~> 3.0.0) - activerecord (3.2.7) - activemodel (= 3.2.7) - activesupport (= 3.2.7) - arel (~> 3.0.2) - tzinfo (~> 0.3.29) - activerecord-jdbc-adapter (1.2.2) - activerecord-jdbcmysql-adapter (1.2.2) - activerecord-jdbc-adapter (~> 1.2.2) - jdbc-mysql (~> 5.1.0) - activesupport (3.2.7) - i18n (~> 0.6) - multi_json (~> 1.0) - arel (3.0.2) - builder (3.0.0) - chronic (0.7.0) - columnize (0.3.6) - debugger (1.2.0) - columnize (>= 0.3.1) - debugger-linecache (~> 1.1.1) - debugger-ruby_core_source (~> 1.1.3) - debugger-linecache (1.1.2) - debugger-ruby_core_source (>= 1.1.1) - debugger-ruby_core_source (1.1.3) - delorean (0.2.1) - chronic - factory_girl (1.3.3) - git (1.2.5) - i18n (0.6.0) - jdbc-mysql (5.1.13) - jeweler (1.8.4) - bundler (~> 1.0) - git (>= 1.2.5) - rake - rdoc - json (1.7.4) - json (1.7.4-java) - linecache (0.46) - rbx-require-relative (> 0.0.4) - multi_json (1.3.6) - mysql (2.8.1) - mysql2 (0.3.11) - pg (0.14.0) - rake (0.9.2.2) - rbx-require-relative (0.0.9) - rdoc (3.12) - json (~> 1.4) - ruby-debug (0.10.4) - columnize (>= 0.1) - ruby-debug-base (~> 0.10.4.0) - ruby-debug-base (0.10.4) - linecache (>= 0.3) - ruby-debug-base (0.10.4-java) - seamless_database_pool (1.0.11) - activerecord (>= 2.2.2) - sqlite3 (1.3.6) - sqlite3-ruby (1.3.3) - sqlite3 (>= 1.3.3) - tzinfo (0.3.33) - -PLATFORMS - java - ruby - -DEPENDENCIES - activerecord (~> 3.0) - activerecord-jdbcmysql-adapter - debugger - delorean (~> 0.2.0) - factory_girl (~> 1.3.3) - jdbc-mysql - jeweler (>= 1.4.0) - mysql (~> 2.8.1) - mysql2 (~> 0.3.0) - pg (~> 0.9) - rake - ruby-debug (= 0.10.4) - ruby-debug-base (= 0.10.4) - seamless_database_pool (~> 1.0.11) - sqlite3-ruby (~> 1.3.1) diff --git a/README.markdown b/README.markdown index 6aa7a9c6..ca6db120 100644 --- a/README.markdown +++ b/README.markdown @@ -1,10 +1,14 @@ # activerecord-import -activerecord-import is a library for bulk inserting data using ActiveRecord. +activerecord-import is a library for bulk inserting data using ActiveRecord. -### Rails 3.1.x and higher +### Rails 4.0 -Use the latest activerecord-import. +Use activerecord-import 0.4.0 or higher. + +### Rails 3.1.x up to, but not including 4.0 + +Use the latest in the activerecord-import 0.3.x series. ### Rails 3.0.x up to, but not including 3.1 diff --git a/Rakefile b/Rakefile index 1a508391..84d105ba 100644 --- a/Rakefile +++ b/Rakefile @@ -4,29 +4,6 @@ Bundler.setup require 'rake' require 'rake/testtask' -begin - require 'jeweler' - Jeweler::Tasks.new do |gem| - gem.name = "activerecord-import" - gem.summary = %Q{Bulk-loading extension for ActiveRecord} - gem.description = %Q{Extraction of the ActiveRecord::Base#import functionality from ar-extensions for Rails 3 and beyond} - gem.email = "zach.dennis@gmail.com" - gem.homepage = "http://github.com/zdennis/activerecord-import" - gem.authors = ["Zach Dennis"] - gem.files = FileList["VERSION", "Rakefile", "README*", "lib/**/*"] - - bundler = Bundler.load - bundler.dependencies_for(:default).each do |dependency| - gem.add_dependency dependency.name, *dependency.requirements_list - end - - # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings - end - Jeweler::GemcutterTasks.new -rescue LoadError - puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" -end - namespace :display do task :notice do puts @@ -36,7 +13,7 @@ namespace :display do end task :default => ["display:notice"] -ADAPTERS = %w(mysql mysql2 jdbcmysql postgresql sqlite3 seamless_database_pool mysqlspatial mysql2spatial spatialite postgis) +ADAPTERS = %w(mysql mysql2 em_mysql2 jdbcmysql postgresql sqlite3 seamless_database_pool mysqlspatial mysql2spatial spatialite postgis) ADAPTERS.each do |adapter| namespace :test do desc "Runs #{adapter} database tests." diff --git a/VERSION b/VERSION deleted file mode 100644 index a2268e2d..00000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.3.1 \ No newline at end of file diff --git a/activerecord-import.gemspec b/activerecord-import.gemspec index 2f0cf165..a6848f48 100644 --- a/activerecord-import.gemspec +++ b/activerecord-import.gemspec @@ -1,68 +1,22 @@ -# Generated by jeweler -# DO NOT EDIT THIS FILE DIRECTLY -# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' # -*- encoding: utf-8 -*- +require File.expand_path('../lib/activerecord-import/version', __FILE__) -Gem::Specification.new do |s| - s.name = "activerecord-import" - s.version = "0.2.10" +Gem::Specification.new do |gem| + gem.authors = ["Zach Dennis"] + gem.email = ["zach.dennis@gmail.com"] + gem.summary = "Bulk-loading extension for ActiveRecord" + gem.description = "Extraction of the ActiveRecord::Base#import functionality from ar-extensions for Rails 3 and beyond" + gem.homepage = "http://github.com/zdennis/activerecord-import" - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Zach Dennis"] - s.date = "2012-08-30" - s.description = "Extraction of the ActiveRecord::Base#import functionality from ar-extensions for Rails 3 and beyond" - s.email = "zach.dennis@gmail.com" - s.extra_rdoc_files = [ - "README.markdown" - ] - s.files = [ - "README.markdown", - "Rakefile", - "VERSION", - "lib/activerecord-import.rb", - "lib/activerecord-import/active_record/adapters/abstract_adapter.rb", - "lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb", - "lib/activerecord-import/active_record/adapters/mysql2_adapter.rb", - "lib/activerecord-import/active_record/adapters/mysql_adapter.rb", - "lib/activerecord-import/active_record/adapters/postgresql_adapter.rb", - "lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb", - "lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb", - "lib/activerecord-import/adapters/abstract_adapter.rb", - "lib/activerecord-import/adapters/mysql_adapter.rb", - "lib/activerecord-import/adapters/postgresql_adapter.rb", - "lib/activerecord-import/adapters/sqlite3_adapter.rb", - "lib/activerecord-import/base.rb", - "lib/activerecord-import/import.rb", - "lib/activerecord-import/mysql.rb", - "lib/activerecord-import/mysql2.rb", - "lib/activerecord-import/postgresql.rb", - "lib/activerecord-import/sqlite3.rb", - "lib/activerecord-import/synchronize.rb" - ] - s.homepage = "http://github.com/zdennis/activerecord-import" - s.require_paths = ["lib"] - s.rubygems_version = "1.8.24" - s.summary = "Bulk-loading extension for ActiveRecord" + gem.files = `git ls-files`.split($\) + gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.name = "activerecord-import" + gem.require_paths = ["lib"] + gem.version = ActiveRecord::Import::VERSION - if s.respond_to? :specification_version then - s.specification_version = 3 + gem.required_ruby_version = ">= 1.9.2" - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, ["~> 3.0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 1.4.0"]) - s.add_runtime_dependency(%q, ["~> 3.0"]) - else - s.add_dependency(%q, ["~> 3.0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 1.4.0"]) - s.add_dependency(%q, ["~> 3.0"]) - end - else - s.add_dependency(%q, ["~> 3.0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 1.4.0"]) - s.add_dependency(%q, ["~> 3.0"]) - end + gem.add_runtime_dependency "activerecord", ">= 3.0" + gem.add_development_dependency "rake" end - diff --git a/gemfiles/3.1.gemfile b/gemfiles/3.1.gemfile new file mode 100644 index 00000000..fd2fa391 --- /dev/null +++ b/gemfiles/3.1.gemfile @@ -0,0 +1,4 @@ +platforms :ruby do + gem 'mysql', '~> 2.8.1' + gem 'activerecord', '~> 3.1.0' +end diff --git a/gemfiles/3.2.gemfile b/gemfiles/3.2.gemfile new file mode 100644 index 00000000..179ce242 --- /dev/null +++ b/gemfiles/3.2.gemfile @@ -0,0 +1,4 @@ +platforms :ruby do + gem 'mysql', '~> 2.8.1' + gem 'activerecord', '~> 3.2.0' +end diff --git a/gemfiles/4.0.gemfile b/gemfiles/4.0.gemfile new file mode 100644 index 00000000..c279c43f --- /dev/null +++ b/gemfiles/4.0.gemfile @@ -0,0 +1,4 @@ +platforms :ruby do + gem 'mysql', '~> 2.9' + gem 'activerecord', '~> 4.0.0.rc2' +end diff --git a/lib/activerecord-import/active_record/adapters/abstract_adapter.rb b/lib/activerecord-import/active_record/adapters/abstract_adapter.rb index d191ae6b..d1b9352b 100644 --- a/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +++ b/lib/activerecord-import/active_record/adapters/abstract_adapter.rb @@ -3,7 +3,6 @@ module ActiveRecord # :nodoc: module ConnectionAdapters # :nodoc: class AbstractAdapter # :nodoc: - extend ActiveRecord::Import::AbstractAdapter::ClassMethods include ActiveRecord::Import::AbstractAdapter::InstanceMethods end end diff --git a/lib/activerecord-import/active_record/adapters/em_mysql2_adapter.rb b/lib/activerecord-import/active_record/adapters/em_mysql2_adapter.rb new file mode 100644 index 00000000..03d5bc58 --- /dev/null +++ b/lib/activerecord-import/active_record/adapters/em_mysql2_adapter.rb @@ -0,0 +1,8 @@ +require "em-synchrony" +require "em-synchrony/mysql2" +require "em-synchrony/activerecord" +require "activerecord-import/adapters/em_mysql2_adapter" + +class ActiveRecord::ConnectionAdapters::EMMysql2Adapter + include ActiveRecord::Import::EMMysql2Adapter +end diff --git a/lib/activerecord-import/adapters/abstract_adapter.rb b/lib/activerecord-import/adapters/abstract_adapter.rb index e4662fed..89823dd1 100644 --- a/lib/activerecord-import/adapters/abstract_adapter.rb +++ b/lib/activerecord-import/adapters/abstract_adapter.rb @@ -1,75 +1,22 @@ module ActiveRecord::Import::AbstractAdapter - NO_MAX_PACKET = 0 - QUERY_OVERHEAD = 8 #This was shown to be true for MySQL, but it's not clear where the overhead is from. - - module ClassMethods - def get_insert_value_sets( values, sql_size, max_bytes ) # :nodoc: - value_sets = [] - arr, current_arr_values_size, current_size = [], 0, 0 - values.each_with_index do |val,i| - comma_bytes = arr.size - sql_size_thus_far = sql_size + current_size + val.bytesize + comma_bytes - if NO_MAX_PACKET == max_bytes or sql_size_thus_far <= max_bytes - current_size += val.bytesize - arr << val - else - value_sets << arr - arr = [ val ] - current_size = val.bytesize - end - - # if we're on the last iteration push whatever we have in arr to value_sets - value_sets << arr if i == (values.size-1) - end - [ *value_sets ] - end - end - module InstanceMethods def next_value_for_sequence(sequence_name) %{#{sequence_name}.nextval} end - - # +sql+ can be a single string or an array. If it is an array all - # elements that are in position >= 1 will be appended to the final SQL. + def insert_many( sql, values, *args ) # :nodoc: - # the number of inserts default - number_of_inserts = 0 - + number_of_inserts = 1 + base_sql,post_sql = if sql.is_a?( String ) [ sql, '' ] elsif sql.is_a?( Array ) [ sql.shift, sql.join( ' ' ) ] end - - sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size - # the number of bytes the requested insert statement values will take up - values_in_bytes = values.sum {|value| value.bytesize } - - # the number of bytes (commas) it will take to comma separate our values - comma_separated_bytes = values.size-1 - - # the total number of bytes required if this statement is one statement - total_bytes = sql_size + values_in_bytes + comma_separated_bytes - - max = max_allowed_packet - - ids=[] - # if we can insert it all as one statement - if NO_MAX_PACKET == max or total_bytes < max - number_of_inserts += 1 - sql2insert = base_sql + values.join( ',' ) + post_sql - ids = select_values( sql2insert) - else - value_sets = self.class.get_insert_value_sets( values, sql_size, max ) - value_sets.each do |values| - number_of_inserts += 1 - sql2insert = base_sql + values.join( ',' ) + post_sql - ids.concat(select_values( sql2insert)) - end - end + sql2insert = base_sql + values.join( ',' ) + post_sql + ids = select_values( sql2insert, *args ) + [number_of_inserts,ids] end diff --git a/lib/activerecord-import/adapters/em_mysql2_adapter.rb b/lib/activerecord-import/adapters/em_mysql2_adapter.rb new file mode 100644 index 00000000..761ff1b5 --- /dev/null +++ b/lib/activerecord-import/adapters/em_mysql2_adapter.rb @@ -0,0 +1,5 @@ +require File.dirname(__FILE__) + "/mysql_adapter" + +module ActiveRecord::Import::EMMysql2Adapter + include ActiveRecord::Import::MysqlAdapter +end diff --git a/lib/activerecord-import/adapters/mysql_adapter.rb b/lib/activerecord-import/adapters/mysql_adapter.rb index 5254212d..5e5067b9 100644 --- a/lib/activerecord-import/adapters/mysql_adapter.rb +++ b/lib/activerecord-import/adapters/mysql_adapter.rb @@ -1,7 +1,54 @@ module ActiveRecord::Import::MysqlAdapter - include ActiveRecord::Import::ImportSupport + include ActiveRecord::Import::ImportSupport include ActiveRecord::Import::OnDuplicateKeyUpdateSupport + NO_MAX_PACKET = 0 + QUERY_OVERHEAD = 8 #This was shown to be true for MySQL, but it's not clear where the overhead is from. + + # +sql+ can be a single string or an array. If it is an array all + # elements that are in position >= 1 will be appended to the final SQL. + def insert_many( sql, values, *args ) # :nodoc: + # the number of inserts default + number_of_inserts = 0 + + base_sql,post_sql = if sql.is_a?( String ) + [ sql, '' ] + elsif sql.is_a?( Array ) + [ sql.shift, sql.join( ' ' ) ] + end + + sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size + + # the number of bytes the requested insert statement values will take up + values_in_bytes = values.sum {|value| value.bytesize } + + # the number of bytes (commas) it will take to comma separate our values + comma_separated_bytes = values.size-1 + + # the total number of bytes required if this statement is one statement + total_bytes = sql_size + values_in_bytes + comma_separated_bytes + + max = max_allowed_packet + + # if we can insert it all as one statement + if NO_MAX_PACKET == max or total_bytes < max + number_of_inserts += 1 + sql2insert = base_sql + values.join( ',' ) + post_sql + insert( sql2insert, *args ) + else + value_sets = ::ActiveRecord::Import::ValueSetsBytesParser.parse(values, + :reserved_bytes => sql_size, + :max_bytes => max) + value_sets.each do |values| + number_of_inserts += 1 + sql2insert = base_sql + values.join( ',' ) + post_sql + insert( sql2insert, *args ) + end + end + + number_of_inserts + end + # Returns the maximum number of bytes that the server will allow # in a single packet def max_allowed_packet # :nodoc: @@ -12,9 +59,9 @@ def max_allowed_packet # :nodoc: val.to_i end end - + # Returns a generated ON DUPLICATE KEY UPDATE statement given the passed - # in +args+. + # in +args+. def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc: sql = ' ON DUPLICATE KEY UPDATE ' arg = args.first diff --git a/lib/activerecord-import/adapters/sqlite3_adapter.rb b/lib/activerecord-import/adapters/sqlite3_adapter.rb index 89090ee7..fadb26c8 100644 --- a/lib/activerecord-import/adapters/sqlite3_adapter.rb +++ b/lib/activerecord-import/adapters/sqlite3_adapter.rb @@ -1,18 +1,42 @@ module ActiveRecord::Import::SQLite3Adapter include ActiveRecord::Import::ImportSupport + MIN_VERSION_FOR_IMPORT = "3.7.11" + SQLITE_LIMIT_COMPOUND_SELECT = 500 + # Override our conformance to ActiveRecord::Import::ImportSupport interface # to ensure that we only support import in supported version of SQLite. # Which INSERT statements with multiple value sets was introduced in 3.7.11. def supports_import?(current_version=self.sqlite_version) - minimum_supported_version = "3.7.11" - if current_version >= minimum_supported_version + if current_version >= MIN_VERSION_FOR_IMPORT true else false end end + # +sql+ can be a single string or an array. If it is an array all + # elements that are in position >= 1 will be appended to the final SQL. + def insert_many(sql, values, *args) # :nodoc: + number_of_inserts = 0 + base_sql,post_sql = if sql.is_a?( String ) + [ sql, '' ] + elsif sql.is_a?( Array ) + [ sql.shift, sql.join( ' ' ) ] + end + + value_sets = ::ActiveRecord::Import::ValueSetsRecordsParser.parse(values, + :max_records => SQLITE_LIMIT_COMPOUND_SELECT) + + value_sets.each do |values| + number_of_inserts += 1 + sql2insert = base_sql + values.join( ',' ) + post_sql + insert( sql2insert, *args ) + end + + number_of_inserts + end + def next_value_for_sequence(sequence_name) %{nextval('#{sequence_name}')} end diff --git a/lib/activerecord-import/base.rb b/lib/activerecord-import/base.rb index 8498357c..e84267a8 100644 --- a/lib/activerecord-import/base.rb +++ b/lib/activerecord-import/base.rb @@ -14,7 +14,7 @@ def self.base_adapter(adapter) else adapter end end - + # Loads the import functionality for a specific database adapter def self.require_adapter(adapter) require File.join(AdapterPath,"/abstract_adapter") @@ -31,4 +31,5 @@ def self.load_from_connection_pool(connection_pool) this_dir = Pathname.new File.dirname(__FILE__) require this_dir.join("import").to_s require this_dir.join("active_record/adapters/abstract_adapter").to_s -require this_dir.join("synchronize").to_s \ No newline at end of file +require this_dir.join("synchronize").to_s +require this_dir.join("value_sets_parser").to_s diff --git a/lib/activerecord-import/em_mysql2.rb b/lib/activerecord-import/em_mysql2.rb new file mode 100644 index 00000000..19a5d931 --- /dev/null +++ b/lib/activerecord-import/em_mysql2.rb @@ -0,0 +1,7 @@ +warn <<-MSG +[DEPRECATION] loading activerecord-import via 'require "activerecord-import/"' + is deprecated. Update to autorequire using 'require "activerecord-import"'. See + http://github.com/zdennis/activerecord-import/wiki/Requiring for more information +MSG + +require File.expand_path(File.join(File.dirname(__FILE__), "/../activerecord-import")) diff --git a/lib/activerecord-import/import.rb b/lib/activerecord-import/import.rb index 1874edd2..e16a594a 100644 --- a/lib/activerecord-import/import.rb +++ b/lib/activerecord-import/import.rb @@ -11,7 +11,7 @@ def supports_import? #:nodoc: true end end - + module OnDuplicateKeyUpdateSupport #:nodoc: def supports_on_duplicate_key_update? #:nodoc: true @@ -32,7 +32,7 @@ class << self tproc = lambda do ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now end - + AREXT_RAILS_COLUMNS = { :create => { "created_on" => tproc , "created_at" => tproc }, @@ -40,7 +40,7 @@ class << self "updated_at" => tproc } } AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys - + # Returns true if the current database connection adapter # supports import functionality, otherwise returns false. def supports_import?(*args) @@ -57,14 +57,14 @@ def supports_on_duplicate_key_update? rescue NoMethodError false end - - # Imports a collection of values to the database. + + # 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 than one record at a time and do not care # about having ActiveRecord objects returned for each record - # inserted. + # inserted. # # This can be used with or without validations. It does not utilize # the ActiveRecord::Callbacks during creation/modification while @@ -74,9 +74,9 @@ def supports_on_duplicate_key_update? # Model.import array_of_models # Model.import column_names, array_of_values # Model.import column_names, array_of_values, options - # + # # ==== Model.import array_of_models - # + # # With this form you can call _import_ passing in an array of model # objects that you want updated. # @@ -108,9 +108,9 @@ def supports_on_duplicate_key_update? # * +timestamps+ - true|false, tells import to not add timestamps \ # (if false) even if record timestamps is disabled in ActiveRecord::Base # - # == Examples + # == Examples # class BlogPost < ActiveRecord::Base ; end - # + # # # Example using array of model objects # posts = [ BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT', # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT2', @@ -128,7 +128,7 @@ def supports_on_duplicate_key_update? # BlogPost.import( columns, values, :validate => false ) # # # Example synchronizing existing instances in memory - # post = BlogPost.find_by_author_name( 'zdennis' ) + # post = BlogPost.where(author_name: 'zdennis').first # puts post.author_name # => 'zdennis' # columns = [ :author_name, :title ] # values = [ [ 'yoda', 'test post' ] ] @@ -143,7 +143,7 @@ def supports_on_duplicate_key_update? # == On Duplicate Key Update (MySQL only) # # The :on_duplicate_key_update option can be either an Array or a Hash. - # + # # ==== Using an Array # # The :on_duplicate_key_update option can be an array of column @@ -158,9 +158,9 @@ def supports_on_duplicate_key_update? # to model attribute name mappings. This gives you finer grained # control over what fields are updated with what attributes on your # model. Below is an example: - # - # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title } - # + # + # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title } + # # = Returns # This returns an object which responds to +failed_instances+ and +num_inserts+. # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed. @@ -222,7 +222,7 @@ def import_helper( *args ) models = args.first column_names = self.column_names.dup end - + array_of_attributes = models.map do |model| # this next line breaks sqlite.so with a segmentation fault # if model.new_record? || options[:on_duplicate_key_update] @@ -278,23 +278,23 @@ def import_helper( *args ) return_obj.num_inserts = 0 if return_obj.num_inserts.nil? return_obj end - - # TODO import_from_table needs to be implemented. + + # TODO import_from_table needs to be implemented. def import_from_table( options ) # :nodoc: end - + # Imports the passed in +column_names+ and +array_of_attributes+ # given the passed in +options+ Hash with validations. Returns an - # object with the methods +failed_instances+ and +num_inserts+. - # +failed_instances+ is an array of instances that failed validations. + # object with the methods +failed_instances+ and +num_inserts+. + # +failed_instances+ is an array of instances that failed validations. # +num_inserts+ is the number of inserts it took to import the data. See # ActiveRecord::Base.import for more information on # +column_names+, +array_of_attributes+ and +options+. def import_with_validations( column_names, array_of_attributes, options={} ) failed_instances = [] - + # create instances for each of our column/value sets - arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) + arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # keep track of the instance and the position it is currently at. if this fails # validation we'll use the index to remove it from the array_of_attributes @@ -305,7 +305,7 @@ def import_with_validations( column_names, array_of_attributes, options={} ) if not instance.valid? array_of_attributes[ i ] = nil failed_instances << instance - end + end end array_of_attributes.compact! @@ -316,7 +316,7 @@ def import_with_validations( column_names, array_of_attributes, options={} ) end ActiveRecord::Import::Result.new(failed_instances, num_inserts, ids) end - + # Imports the passed in +column_names+ and +array_of_attributes+ # given the passed in +options+ Hash. This will return the number # of insert operations it took to create these records without @@ -415,7 +415,7 @@ def add_special_rails_stamps( column_names, array_of_attributes, options ) column_names << key array_of_attributes.each { |arr| arr << value } end - + if supports_on_duplicate_key_update? if options[:on_duplicate_key_update] options[:on_duplicate_key_update] << key.to_sym if options[:on_duplicate_key_update].is_a?(Array) @@ -427,13 +427,13 @@ def add_special_rails_stamps( column_names, array_of_attributes, options ) end end end - + # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+. def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc: array_of_attributes.map do |attributes| Hash[attributes.each_with_index.map {|attr, c| [column_names[c], attr] }] end end - + end end diff --git a/lib/activerecord-import/synchronize.rb b/lib/activerecord-import/synchronize.rb index a5286d7d..c99c5204 100644 --- a/lib/activerecord-import/synchronize.rb +++ b/lib/activerecord-import/synchronize.rb @@ -1,22 +1,22 @@ module ActiveRecord # :nodoc: class Base # :nodoc: - + # Synchronizes the passed in ActiveRecord instances with data # from the database. This is like calling reload on an individual - # ActiveRecord instance but it is intended for use on multiple instances. - # + # ActiveRecord instance but it is intended for use on multiple instances. + # # This uses one query for all instance updates and then updates existing # instances rather sending one query for each instance # # == Examples # # Synchronizing existing models by matching on the primary key field - # posts = Post.find_by_author("Zach") + # posts = Post.where(author: "Zach").first # <.. out of system changes occur to change author name from Zach to Zachary..> # Post.synchronize posts # posts.first.author # => "Zachary" instead of Zach - # + # # # Synchronizing using custom key fields - # posts = Post.find_by_author("Zach") + # posts = Post.where(author: "Zach").first # <.. out of system changes occur to change the address of author 'Zach' to 1245 Foo Ln ..> # Post.synchronize posts, [:name] # queries on the :name column and not the :id column # posts.first.address # => "1245 Foo Ln" instead of whatever it was @@ -26,23 +26,24 @@ def self.synchronize(instances, keys=[self.primary_key]) conditions = {} order = "" - + key_values = keys.map { |key| instances.map(&"#{key}".to_sym) } keys.zip(key_values).each { |key, values| conditions[key] = values } order = keys.map{ |key| "#{key} ASC" }.join(",") - + klass = instances.first.class - fresh_instances = klass.find( :all, :conditions=>conditions, :order=>order ) + fresh_instances = klass.where(conditions).order(order) instances.each do |instance| matched_instance = fresh_instances.detect do |fresh_instance| keys.all?{ |key| fresh_instance.send(key) == instance.send(key) } end - + if matched_instance instance.clear_aggregation_cache instance.clear_association_cache instance.instance_variable_set '@attributes', matched_instance.attributes + instance.instance_variable_set '@attributes_cache', {} # Since the instance now accurately reflects the record in # the database, ensure that instance.persisted? is true. instance.instance_variable_set '@new_record', false diff --git a/lib/activerecord-import/value_sets_parser.rb b/lib/activerecord-import/value_sets_parser.rb new file mode 100644 index 00000000..66bc962d --- /dev/null +++ b/lib/activerecord-import/value_sets_parser.rb @@ -0,0 +1,54 @@ +module ActiveRecord::Import + class ValueSetsBytesParser + attr_reader :reserved_bytes, :max_bytes, :values + + def self.parse(values, options) + new(values, options).parse + end + + def initialize(values, options) + @values = values + @reserved_bytes = options[:reserved_bytes] + @max_bytes = options[:max_bytes] + end + + def parse + value_sets = [] + arr, current_arr_values_size, current_size = [], 0, 0 + values.each_with_index do |val,i| + comma_bytes = arr.size + bytes_thus_far = reserved_bytes + current_size + val.bytesize + comma_bytes + if bytes_thus_far <= max_bytes + current_size += val.bytesize + arr << val + else + value_sets << arr + arr = [ val ] + current_size = val.bytesize + end + + # if we're on the last iteration push whatever we have in arr to value_sets + value_sets << arr if i == (values.size-1) + end + + [ *value_sets ] + end + end + + class ValueSetsRecordsParser + attr_reader :max_records, :values + + def self.parse(values, options) + new(values, options).parse + end + + def initialize(values, options) + @values = values + @max_records = options[:max_records] + end + + def parse + @values.in_groups_of(max_records, with_fill=false) + end + end +end diff --git a/lib/activerecord-import/version.rb b/lib/activerecord-import/version.rb new file mode 100644 index 00000000..baa876d9 --- /dev/null +++ b/lib/activerecord-import/version.rb @@ -0,0 +1,5 @@ +module ActiveRecord + module Import + VERSION = "0.4.1" + end +end diff --git a/test/active_record/connection_adapter_test.rb b/test/active_record/connection_adapter_test.rb deleted file mode 100644 index 5b81e7d0..00000000 --- a/test/active_record/connection_adapter_test.rb +++ /dev/null @@ -1,62 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/../test_helper') - -describe "ActiveRecord::ConnectionAdapter::AbstractAdapter" do - context "#get_insert_value_sets - computing insert value sets" do - let(:adapter){ ActiveRecord::ConnectionAdapters::AbstractAdapter } - let(:base_sql){ "INSERT INTO atable (a,b,c)" } - let(:values){ [ "(1,2,3)", "(2,3,4)", "(3,4,5)" ] } - - context "when the max allowed bytes is 33 and the base SQL is 26 bytes" do - it "should return 3 value sets when given 3 value sets of 7 bytes a piece" do - value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 33 - assert_equal 3, value_sets.size - end - end - - context "when the max allowed bytes is 40 and the base SQL is 26 bytes" do - it "should return 3 value sets when given 3 value sets of 7 bytes a piece" do - value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 40 - assert_equal 3, value_sets.size - end - end - - context "when the max allowed bytes is 41 and the base SQL is 26 bytes" do - it "should return 2 value sets when given 2 value sets of 7 bytes a piece" do - value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 41 - assert_equal 2, value_sets.size - end - end - - context "when the max allowed bytes is 48 and the base SQL is 26 bytes" do - it "should return 2 value sets when given 2 value sets of 7 bytes a piece" do - value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 48 - assert_equal 2, value_sets.size - end - end - - context "when the max allowed bytes is 49 and the base SQL is 26 bytes" do - it "should return 1 value sets when given 1 value sets of 7 bytes a piece" do - value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 49 - assert_equal 1, value_sets.size - end - end - - context "when the max allowed bytes is 999999 and the base SQL is 26 bytes" do - it "should return 1 value sets when given 1 value sets of 7 bytes a piece" do - value_sets = adapter.get_insert_value_sets values, base_sql.size, max_allowed_bytes = 999999 - assert_equal 1, value_sets.size - end - end - end - -end - -describe "ActiveRecord::Import DB-specific adapter class" do - context "when ActiveRecord::Import is in use" do - it "should appear in the AR connection adapter class's ancestors" do - connection = ActiveRecord::Base.connection - import_class_name = 'ActiveRecord::Import::' + connection.class.name.demodulize - assert_includes connection.class.ancestors, import_class_name.constantize - end - end -end \ No newline at end of file diff --git a/test/adapters/em_mysql2.rb b/test/adapters/em_mysql2.rb new file mode 100644 index 00000000..3fa4f79c --- /dev/null +++ b/test/adapters/em_mysql2.rb @@ -0,0 +1 @@ +ENV["ARE_DB"] = "em_mysql2" diff --git a/test/database.yml.sample b/test/database.yml.sample index 85258021..2fd3350d 100644 --- a/test/database.yml.sample +++ b/test/database.yml.sample @@ -16,9 +16,14 @@ mysql2: &mysql2 mysqlspatial: <<: *mysql -mysqlspatial2: +mysql2spatial: <<: *mysql2 +em_mysql2: + <<: *common + adapter: em_mysql2 + pool: 5 + seamless_database_pool: <<: *common adapter: seamless_database_pool @@ -40,14 +45,13 @@ oracle: adapter: oracle min_messages: debug -sqlite: &sqlite3 +sqlite: adapter: sqlite dbfile: test.db -sqlite3: +sqlite3: &sqlite3 adapter: sqlite3 database: test.db spatialite: <<: *sqlite3 - diff --git a/test/em_mysql2/import_test.rb b/test/em_mysql2/import_test.rb new file mode 100644 index 00000000..cb5dea00 --- /dev/null +++ b/test/em_mysql2/import_test.rb @@ -0,0 +1,6 @@ +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') + +require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/assertions') +require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/import_examples') + +should_support_mysql_import_functionality diff --git a/test/import_test.rb b/test/import_test.rb index 7865610a..c4c5d474 100644 --- a/test/import_test.rb +++ b/test/import_test.rb @@ -6,7 +6,7 @@ assert_difference "Topic.count", +10 do result = Topic.import Build(3, :topics) assert result.num_inserts > 0 - + result = Topic.import Build(7, :topics) assert result.num_inserts > 0 end @@ -33,14 +33,14 @@ let(:columns) { %w(title author_name) } let(:valid_values) { [[ "LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] } let(:invalid_values) { [[ "The RSpec Book", ""], ["Agile+UX", ""]] } - + context "with validation checks turned off" do it "should import valid data" do assert_difference "Topic.count", +2 do result = Topic.import columns, valid_values, :validate => false end end - + it "should import invalid data" do assert_difference "Topic.count", +2 do result = Topic.import columns, invalid_values, :validate => false @@ -53,31 +53,31 @@ end end end - + context "with validation checks turned on" do it "should import valid data" do assert_difference "Topic.count", +2 do result = Topic.import columns, valid_values, :validate => true end end - + it "should not import invalid data" do assert_no_difference "Topic.count" do result = Topic.import columns, invalid_values, :validate => true end end - + it "should report the failed instances" do results = Topic.import columns, invalid_values, :validate => true assert_equal invalid_values.size, results.failed_instances.size results.failed_instances.each{ |e| assert_kind_of Topic, e } end - + it "should import valid data when mixed with invalid data" do assert_difference "Topic.count", +2 do result = Topic.import columns, valid_values + invalid_values, :validate => true end - assert_equal 0, Topic.find_all_by_title(invalid_values.map(&:first)).count + assert_equal 0, Topic.where(title: invalid_values.map(&:first)).count end end end @@ -123,20 +123,20 @@ context "with :synchronize option" do context "synchronizing on new records" do let(:new_topics) { Build(3, :topics) } - + it "doesn't reload any data (doesn't work)" do Topic.import new_topics, :synchronize => new_topics assert new_topics.all?(&:new_record?), "No record should have been reloaded" end end - + context "synchronizing on new records with explicit conditions" do let(:new_topics) { Build(3, :topics) } it "reloads data for existing in-memory instances" do Topic.import(new_topics, :synchronize => new_topics, :synchronize_keys => [:title] ) assert new_topics.all?(&:persisted?), "Records should have been reloaded" - end + end end context "synchronizing on destroyed records with explicit conditions" do @@ -146,24 +146,24 @@ new_topics.each &:destroy Topic.import(new_topics, :synchronize => new_topics, :synchronize_keys => [:title] ) assert new_topics.all?(&:persisted?), "Records should have been reloaded" - end + end end end - + context "with an array of unsaved model instances" do let(:topic) { Build(:topic, :title => "The RSpec Book", :author_name => "David Chelimsky")} let(:topics) { Build(9, :topics) } let(:invalid_topics){ Build(7, :invalid_topics)} - + it "should import records based on those model's attributes" do assert_difference "Topic.count", +9 do result = Topic.import topics end - + Topic.import [topic] - assert Topic.find_by_title_and_author_name("The RSpec Book", "David Chelimsky") + assert Topic.where(title: "The RSpec Book", author_name: "David Chelimsky").first end - + it "should not overwrite existing records" do topic = Generate(:topic, :title => "foobar") assert_no_difference "Topic.count" do @@ -179,21 +179,21 @@ end assert_equal "foobar", topic.reload.title end - + context "with validation checks turned on" do it "should import valid models" do assert_difference "Topic.count", +9 do result = Topic.import topics, :validate => true end end - + it "should not import invalid models" do assert_no_difference "Topic.count" do result = Topic.import invalid_topics, :validate => true end end end - + context "with validation checks turned off" do it "should import invalid models" do assert_difference "Topic.count", +7 do @@ -202,40 +202,41 @@ end end end - + context "with an array of columns and an array of unsaved model instances" do let(:topics) { Build(2, :topics) } - + it "should import records populating the supplied columns with the corresponding model instance attributes" do assert_difference "Topic.count", +2 do result = Topic.import [:author_name, :title], topics end - + # 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.last.author_name) + assert Topic.where(author_name: topics.first.author_name).first + assert Topic.where(author_name: topics.last.author_name).first end - + it "should not populate fields for columns not imported" do topics.first.author_email_address = "zach.dennis@gmail.com" assert_difference "Topic.count", +2 do result = Topic.import [:author_name, :title], topics end - - assert !Topic.find_by_author_email_address("zach.dennis@gmail.com") + + assert !Topic.where(author_email_address: "zach.dennis@gmail.com").first end end - + context "with an array of columns and an array of values" do it "should import ids when specified" do Topic.import [:id, :author_name, :title], [[99, "Bob Jones", "Topic 99"]] assert_equal 99, Topic.last.id end end - + context "ActiveRecord timestamps" do context "when the timestamps columns are present" do setup do + ActiveRecord::Base.default_timezone = :utc Delorean.time_travel_to("5 minutes ago") do assert_difference "Book.count", +1 do result = Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]] @@ -243,24 +244,24 @@ end @book = Book.last end - + 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.utc.strftime("%H:%M"), @book.created_at.strftime("%H:%M") end - + 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.utc.strftime("%H:%M"), @book.created_on.strftime("%H:%M") end - + 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.utc.strftime("%H:%M"), @book.updated_at.strftime("%H:%M") end - + 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.utc.strftime("%H:%M"), @book.updated_on.strftime("%H:%M") end end - + context "when a custom time zone is set" do setup do original_timezone = ActiveRecord::Base.default_timezone @@ -273,22 +274,22 @@ ActiveRecord::Base.default_timezone = original_timezone @book = Book.last end - + it "should set the created_at and created_on timestamps for new records" do 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_on.strftime("%H:%M") end - + 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_on.strftime("%H:%M") end end end - + context "importing with database reserved words" do let(:group) { Build(:group, :order => "superx") } - + it "should import just fine" do assert_difference "Group.count", +1 do result = Group.import [group] @@ -309,7 +310,7 @@ context "when validation is " + (b ? "enabled" : "disabled") do it "should automatically set the foreign key column" do books = [[ "David Chelimsky", "The RSpec Book" ], [ "Chad Fowler", "Rails Recipes" ]] - topic = Factory.create :topic + topic = FactoryGirl.create :topic topic.books.import [ :author_name, :title ], books, :validate => b assert_equal 2, topic.books.count assert topic.books.all? { |b| b.topic_id == topic.id } diff --git a/test/schema/generic_schema.rb b/test/schema/generic_schema.rb index 43221e47..8d3a9050 100644 --- a/test/schema/generic_schema.rb +++ b/test/schema/generic_schema.rb @@ -30,9 +30,9 @@ create_table :projects, :force=>true do |t| t.column :name, :string - t.column :type, :string + t.column :type, :string end - + create_table :developers, :force=>true do |t| t.column :name, :string t.column :salary, :integer, :default=>'70000' @@ -52,7 +52,7 @@ create_table :teams, :force=>true do |t| t.column :name, :string end - + create_table :books, :force=>true do |t| t.column :title, :string, :null=>false t.column :publisher, :string, :null=>false, :default => 'Default Publisher' @@ -100,7 +100,7 @@ t.column :created_at, :datetime t.column :updated_at, :datetime end - + add_index :animals, [:name], :unique => true, :name => 'uk_animals' create_table :widgets, :id => false, :force => true do |t| diff --git a/test/schema/mysql_schema.rb b/test/schema/mysql_schema.rb index 5b47078f..a4fb037c 100644 --- a/test/schema/mysql_schema.rb +++ b/test/schema/mysql_schema.rb @@ -1,5 +1,5 @@ ActiveRecord::Schema.define do - + 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' diff --git a/test/sqlite3/import_test.rb b/test/sqlite3/import_test.rb index b13347c9..6166a8a4 100644 --- a/test/sqlite3/import_test.rb +++ b/test/sqlite3/import_test.rb @@ -3,31 +3,50 @@ describe "#supports_imports?" do context "and SQLite is 3.7.11 or higher" do it "supports import" do - version = ActiveRecord::ConnectionAdapters::SQLiteAdapter::Version.new("3.7.11") + version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new("3.7.11") assert ActiveRecord::Base.supports_import?(version) - version = ActiveRecord::ConnectionAdapters::SQLiteAdapter::Version.new("3.7.12") + version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new("3.7.12") assert ActiveRecord::Base.supports_import?(version) end end context "and SQLite less than 3.7.11" do it "doesn't support import" do - version = ActiveRecord::ConnectionAdapters::SQLiteAdapter::Version.new("3.7.10") + version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new("3.7.10") assert !ActiveRecord::Base.supports_import?(version) end end end describe "#import" do - it "import with a single insert on SQLite 3.7.11 or higher" do - assert_difference "Topic.count", +10 do - result = Topic.import Build(3, :topics) + it "imports with a single insert on SQLite 3.7.11 or higher" do + assert_difference "Topic.count", +507 do + result = Topic.import Build(7, :topics) assert_equal 1, result.num_inserts, "Failed to issue a single INSERT statement. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed" + assert_equal 7, Topic.count, "Failed to insert all records. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed" - result = Topic.import Build(7, :topics) + result = Topic.import Build(500, :topics) assert_equal 1, result.num_inserts, "Failed to issue a single INSERT statement. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed" + assert_equal 507, Topic.count, "Failed to insert all records. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed" + end + end + + it "imports with a two inserts on SQLite 3.7.11 or higher" do + assert_difference "Topic.count", +501 do + result = Topic.import Build(501, :topics) + assert_equal 2, result.num_inserts, "Failed to issue a two INSERT statements. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed" + assert_equal 501, Topic.count, "Failed to insert all records. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed" end end + + it "imports with a five inserts on SQLite 3.7.11 or higher" do + assert_difference "Topic.count", +2500 do + result = Topic.import Build(2500, :topics) + assert_equal 5, result.num_inserts, "Failed to issue a two INSERT statements. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed" + assert_equal 2500, Topic.count, "Failed to insert all records. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed" + end + end + end diff --git a/test/support/factories.rb b/test/support/factories.rb index 118e5677..81d87aa7 100644 --- a/test/support/factories.rb +++ b/test/support/factories.rb @@ -1,36 +1,33 @@ -Factory.sequence :book_title do |n| - "Book #{n}" -end - -Factory.sequence :chapter_title do |n| - "Chapter #{n}" -end +FactoryGirl.define do + sequence(:book_title) {|n| "Book #{n}"} + sequence(:chapter_title) {|n| "Chapter #{n}"} -Factory.define :group do |m| - m.sequence(:order) { |n| "Order #{n}" } -end + factory :group do + sequence(:order) { |n| "Order #{n}" } + end -Factory.define :invalid_topic, :class => "Topic" do |m| - m.sequence(:title){ |n| "Title #{n}"} - m.author_name nil -end + factory :invalid_topic, :class => "Topic" do + sequence(:title){ |n| "Title #{n}"} + author_name nil + end -Factory.define :topic do |m| - m.sequence(:title){ |n| "Title #{n}"} - m.sequence(:author_name){ |n| "Author #{n}"} -end + factory :topic do + sequence(:title){ |n| "Title #{n}"} + sequence(:author_name){ |n| "Author #{n}"} + end -Factory.define :topic_with_book, :parent=>:topic do |m| - m.after_build do |topic| - 2.times do - book = topic.books.build(:title=>Factory.next(:book_title), :author_name=>'Stephen King') - 3.times do - book.chapters.build(:title => Factory.next(:chapter_title)) + factory :widget do + sequence(:w_id){ |n| n} + end + + factory :topic_with_book, :parent=>:topic do |m| + after(:build) do |topic| + 2.times do + book = topic.books.build(:title=>FactoryGirl.generate(:book_title), :author_name=>'Stephen King') + 3.times do + book.chapters.build(:title => FactoryGirl.generate(:chapter_title)) + end end end end end - -Factory.define :widget do |m| - m.sequence(:w_id){ |n| n} -end diff --git a/test/support/generate.rb b/test/support/generate.rb index 1e6ee1a1..7fc999d3 100644 --- a/test/support/generate.rb +++ b/test/support/generate.rb @@ -3,13 +3,13 @@ def Build(*args) n = args.shift if args.first.is_a?(Numeric) factory = args.shift factory_girl_args = args.shift || {} - + if n Array.new.tap do |collection| - n.times.each { collection << Factory.build(factory.to_s.singularize.to_sym, factory_girl_args) } + n.times.each { collection << FactoryGirl.build(factory.to_s.singularize.to_sym, factory_girl_args) } end else - Factory.build(factory.to_s.singularize.to_sym, factory_girl_args) + FactoryGirl.build(factory.to_s.singularize.to_sym, factory_girl_args) end end @@ -17,13 +17,13 @@ def Generate(*args) n = args.shift if args.first.is_a?(Numeric) factory = args.shift factory_girl_args = args.shift || {} - + if n Array.new.tap do |collection| - n.times.each { collection << Factory.create(factory.to_s.singularize.to_sym, factory_girl_args) } + n.times.each { collection << FactoryGirl.create(factory.to_s.singularize.to_sym, factory_girl_args) } end else - Factory.create(factory.to_s.singularize.to_sym, factory_girl_args) + FactoryGirl.create(factory.to_s.singularize.to_sym, factory_girl_args) end end -end \ No newline at end of file +end diff --git a/test/support/mysql/import_examples.rb b/test/support/mysql/import_examples.rb index cf05299f..0c73000a 100644 --- a/test/support/mysql/import_examples.rb +++ b/test/support/mysql/import_examples.rb @@ -1,115 +1,72 @@ # encoding: UTF-8 def should_support_mysql_import_functionality - - describe "building insert value sets" do - it "should properly build insert value set based on max packet allowed" do - values = [ - "('1','2','3')", - "('4','5','6')", - "('7','8','9')" ] - - adapter = ActiveRecord::Base.connection.class - values_size_in_bytes = values.sum {|value| value.bytesize } - base_sql_size_in_bytes = 15 - max_bytes = 30 - - value_sets = adapter.get_insert_value_sets( values, base_sql_size_in_bytes, max_bytes ) - assert_equal 3, value_sets.size, 'Three value sets were expected!' - - # Each element in the value_sets array must be an array - value_sets.each_with_index { |e,i| - assert_kind_of Array, e, "Element #{i} was expected to be an Array!" } - - # Each element in the values array should have a 1:1 correlation to the elements - # in the returned value_sets arrays - assert_equal values[0], value_sets[0].first - assert_equal values[1], value_sets[1].first - assert_equal values[2], value_sets[2].first - end - - context "data contains multi-byte chars" do - it "should properly build insert value set based on max packet allowed" do - # each accented e should be 2 bytes, so each entry is 6 bytes instead of 5 - values = [ - "('é')", - "('é')" ] - - adapter = ActiveRecord::Base.connection.class - base_sql_size_in_bytes = 15 - max_bytes = 26 - - values_size_in_bytes = values.sum {|value| value.bytesize } - value_sets = adapter.get_insert_value_sets( values, base_sql_size_in_bytes, max_bytes ) - - assert_equal 2, value_sets.size, 'Two value sets were expected!' - end - end - end + # Forcefully disable strict mode for this session. + ActiveRecord::Base.connection.execute "set sql_mode=''" 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(:updated_topic){ Topic.find(@topic) } - + 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 |*opts| Topic.import columns, updated_values, opts.extract_options!.merge(: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_support_on_duplicate_key_update should_update_fields_mentioned end - + context "using symbol column names" do let(:update_columns){ [ :title, :author_email_address, :parent_id ] } should_support_on_duplicate_key_update should_update_fields_mentioned end - + context "using string hash map" do let(:update_columns){ { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } } should_support_on_duplicate_key_update should_update_fields_mentioned 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_support_on_duplicate_key_update should_update_fields_mentioned_with_hash_mappings end - + context "using symbol hash map" do let(:update_columns){ { :title => :title, :author_email_address => :author_email_address, :parent_id => :parent_id } } should_support_on_duplicate_key_update should_update_fields_mentioned 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_support_on_duplicate_key_update should_update_fields_mentioned_with_hash_mappings end end - + context "given array of model instances with :validation checks turned off" do macro(:perform_import) do |*opts| @topic.title = "Book - 2nd Edition" @@ -118,73 +75,73 @@ def should_support_mysql_import_functionality @topic.parent_id = 57 Topic.import [@topic], opts.extract_options!.merge(: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_support_on_duplicate_key_update should_update_fields_mentioned end - + context "using symbol column names" do let(:update_columns){ [ :title, :author_email_address, :parent_id ] } should_support_on_duplicate_key_update should_update_fields_mentioned end - + context "using string hash map" do let(:update_columns){ { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } } should_support_on_duplicate_key_update should_update_fields_mentioned 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_support_on_duplicate_key_update should_update_fields_mentioned_with_hash_mappings end - + context "using symbol hash map" do let(:update_columns){ { :title => :title, :author_email_address => :author_email_address, :parent_id => :parent_id } } should_support_on_duplicate_key_update should_update_fields_mentioned 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_support_on_duplicate_key_update should_update_fields_mentioned_with_hash_mappings end end - + end describe "#import with :synchronization option" do - let(:topics){ Array.new } + let(:topics){ Array.new } let(:values){ [ [topics.first.id, "Jerry Carter"], [topics.last.id, "Chad Fowler"] ]} let(:columns){ %W(id author_name) } - + setup do topics << Topic.create!(:title=>"LDAP", :author_name=>"Big Bird") - topics << Topic.create!(:title=>"Rails Recipes", :author_name=>"Elmo") + topics << Topic.create!(:title=>"Rails Recipes", :author_name=>"Elmo") end - + it "synchronizes passed in ActiveRecord model instances with the data just imported" do columns2update = [ 'author_name' ] - + expected_count = Topic.count Topic.import( columns, values, :validate=>false, :on_duplicate_key_update=>columns2update, :synchronize=>topics ) - + assert_equal expected_count, Topic.count, "no new records should have been created!" assert_equal "Jerry Carter", topics.first.author_name, "wrong author!" assert_equal "Chad Fowler", topics.last.author_name, "wrong author!" end end -end \ No newline at end of file +end diff --git a/test/synchronize_test.rb b/test/synchronize_test.rb index 02eb14fa..d5398f84 100644 --- a/test/synchronize_test.rb +++ b/test/synchronize_test.rb @@ -3,14 +3,14 @@ describe ".synchronize" do let(:topics){ Generate(3, :topics) } let(:titles){ %w(one two three) } - + setup do # update records outside of ActiveRecord knowing about it Topic.connection.execute( "UPDATE #{Topic.table_name} SET title='#{titles[0]}_haha' WHERE id=#{topics[0].id}", "Updating record 1 without ActiveRecord" ) Topic.connection.execute( "UPDATE #{Topic.table_name} SET title='#{titles[1]}_haha' WHERE id=#{topics[1].id}", "Updating record 2 without ActiveRecord" ) Topic.connection.execute( "UPDATE #{Topic.table_name} SET title='#{titles[2]}_haha' WHERE id=#{topics[2].id}", "Updating record 3 without ActiveRecord" ) end - + it "reloads data for the specified records" do Book.synchronize topics diff --git a/test/test_helper.rb b/test/test_helper.rb index a28e5b4a..59be4b0e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,7 +4,6 @@ $LOAD_PATH.unshift(File.dirname(__FILE__)) require "fileutils" -require "rubygems" ENV["RAILS_ENV"] = "test" @@ -18,14 +17,14 @@ require "active_support/test_case" require "delorean" -require "ruby-debug" +require "ruby-debug" if RUBY_VERSION.to_f < 1.9 adapter = ENV["ARE_DB"] || "sqlite3" FileUtils.mkdir_p 'log' ActiveRecord::Base.logger = Logger.new("log/test.log") ActiveRecord::Base.logger.level = Logger::DEBUG -ActiveRecord::Base.configurations["test"] = YAML.load(test_dir.join("database.yml").open)[adapter] +ActiveRecord::Base.configurations["test"] = YAML.load_file(test_dir.join("database.yml"))[adapter] require "activerecord-import" ActiveRecord::Base.establish_connection "test" @@ -46,6 +45,4 @@ Dir[File.dirname(__FILE__) + "/models/*.rb"].each{ |file| require file } # Prevent this deprecation warning from breaking the tests. -module Rake::DeprecatedObjectDSL - remove_method :import -end +Rake::FileList.send(:remove_method, :import) diff --git a/test/value_sets_bytes_parser_test.rb b/test/value_sets_bytes_parser_test.rb new file mode 100644 index 00000000..a14b9b81 --- /dev/null +++ b/test/value_sets_bytes_parser_test.rb @@ -0,0 +1,96 @@ +require File.expand_path(File.dirname(__FILE__) + '/test_helper') + +require 'activerecord-import/value_sets_parser' + +describe ActiveRecord::Import::ValueSetsBytesParser do + context "#parse - computing insert value sets" do + let(:parser){ ActiveRecord::Import::ValueSetsBytesParser } + let(:base_sql){ "INSERT INTO atable (a,b,c)" } + let(:values){ [ "(1,2,3)", "(2,3,4)", "(3,4,5)" ] } + + context "when the max allowed bytes is 33 and the base SQL is 26 bytes" do + it "should return 3 value sets when given 3 value sets of 7 bytes a piece" do + value_sets = parser.parse values, :reserved_bytes => base_sql.size, :max_bytes => 33 + assert_equal 3, value_sets.size + end + end + + context "when the max allowed bytes is 40 and the base SQL is 26 bytes" do + it "should return 3 value sets when given 3 value sets of 7 bytes a piece" do + value_sets = parser.parse values, :reserved_bytes => base_sql.size, :max_bytes => 40 + assert_equal 3, value_sets.size + end + end + + context "when the max allowed bytes is 41 and the base SQL is 26 bytes" do + it "should return 2 value sets when given 2 value sets of 7 bytes a piece" do + value_sets = parser.parse values, :reserved_bytes => base_sql.size, :max_bytes => 41 + assert_equal 2, value_sets.size + end + end + + context "when the max allowed bytes is 48 and the base SQL is 26 bytes" do + it "should return 2 value sets when given 2 value sets of 7 bytes a piece" do + value_sets = parser.parse values, :reserved_bytes => base_sql.size, :max_bytes => 48 + assert_equal 2, value_sets.size + end + end + + context "when the max allowed bytes is 49 and the base SQL is 26 bytes" do + it "should return 1 value sets when given 1 value sets of 7 bytes a piece" do + value_sets = parser.parse values, :reserved_bytes => base_sql.size, :max_bytes => 49 + assert_equal 1, value_sets.size + end + end + + context "when the max allowed bytes is 999999 and the base SQL is 26 bytes" do + it "should return 1 value sets when given 1 value sets of 7 bytes a piece" do + value_sets = parser.parse values, :reserved_bytes => base_sql.size, :max_bytes => 999999 + assert_equal 1, value_sets.size + end + end + + it "should properly build insert value set based on max packet allowed" do + values = [ + "('1','2','3')", + "('4','5','6')", + "('7','8','9')" ] + + values_size_in_bytes = values.sum {|value| value.bytesize } + base_sql_size_in_bytes = 15 + max_bytes = 30 + + value_sets = parser.parse values, reserved_bytes: base_sql_size_in_bytes, max_bytes: max_bytes + assert_equal 3, value_sets.size, 'Three value sets were expected!' + + # Each element in the value_sets array must be an array + value_sets.each_with_index { |e,i| + assert_kind_of Array, e, "Element #{i} was expected to be an Array!" } + + # Each element in the values array should have a 1:1 correlation to the elements + # in the returned value_sets arrays + assert_equal values[0], value_sets[0].first + assert_equal values[1], value_sets[1].first + assert_equal values[2], value_sets[2].first + end + + context "data contains multi-byte chars" do + it "should properly build insert value set based on max packet allowed" do + # each accented e should be 2 bytes, so each entry is 6 bytes instead of 5 + values = [ + "('é')", + "('é')" ] + + base_sql_size_in_bytes = 15 + max_bytes = 26 + + values_size_in_bytes = values.sum {|value| value.bytesize } + value_sets = parser.parse values, reserved_bytes: base_sql_size_in_bytes, max_bytes: max_bytes + + assert_equal 2, value_sets.size, 'Two value sets were expected!' + end + end + end + +end + diff --git a/test/value_sets_records_parser_test.rb b/test/value_sets_records_parser_test.rb new file mode 100644 index 00000000..7f95d9eb --- /dev/null +++ b/test/value_sets_records_parser_test.rb @@ -0,0 +1,32 @@ +require File.expand_path(File.dirname(__FILE__) + '/test_helper') + +require 'activerecord-import/value_sets_parser' + +describe "ActiveRecord::Import::ValueSetsRecordsParser" do + context "#parse - computing insert value sets" do + let(:parser){ ActiveRecord::Import::ValueSetsRecordsParser } + let(:base_sql){ "INSERT INTO atable (a,b,c)" } + let(:values){ [ "(1,2,3)", "(2,3,4)", "(3,4,5)" ] } + + context "when the max number of records is 1" do + it "should return 3 value sets when given 3 values sets" do + value_sets = parser.parse values, :max_records => 1 + assert_equal 3, value_sets.size + end + end + + context "when the max number of records is 2" do + it "should return 2 value sets when given 3 values sets" do + value_sets = parser.parse values, :max_records => 2 + assert_equal 2, value_sets.size + end + end + + context "when the max number of records is 3" do + it "should return 1 value sets when given 3 values sets" do + value_sets = parser.parse values, :max_records => 3 + assert_equal 1, value_sets.size + end + end + end +end From 9562dd4e1fb669e26be57e556f6c77232dc761b3 Mon Sep 17 00:00:00 2001 From: John Naegle Date: Wed, 19 Mar 2014 14:42:33 -0500 Subject: [PATCH 08/11] remove .proxy, doesn't seem to be valid in rails 4 --- lib/activerecord-import/import.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/activerecord-import/import.rb b/lib/activerecord-import/import.rb index e16a594a..c8b7c0d0 100644 --- a/lib/activerecord-import/import.rb +++ b/lib/activerecord-import/import.rb @@ -120,7 +120,7 @@ def supports_on_duplicate_key_update? # # Example using column_names and array_of_values # columns = [ :author_name, :title ] # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ] - # BlogPost.import columns, values + # BlogPost.import columns, values # # # Example using column_names, array_of_value and options # columns = [ :author_name, :title ] @@ -142,7 +142,7 @@ def supports_on_duplicate_key_update? # # == On Duplicate Key Update (MySQL only) # - # The :on_duplicate_key_update option can be either an Array or a Hash. + # The :on_duplicate_key_update option can be either an Array or a Hash. # # ==== Using an Array # @@ -169,17 +169,17 @@ def import(*args) if args.first.is_a?( Array ) and args.first.first.is_a? ActiveRecord::Base options = {} options.merge!( args.pop ) if args.last.is_a?(Hash) - + models = args.first # the import argument parsing is too tangled for me ... I only want to prove the concept of saving recursively result = import_helper(models, options) - # now, for all the dirty associations, collect them into a new set of models, then recurse. - # notes: + # now, for all the dirty associations, collect them into a new set of models, then recurse. + # notes: # does not handle associations that reference themselves # assumes that the only associations to be saved are marked with :autosave # should probably take a hash to associations to follow. hash={} models.each {|model| add_objects(hash, model) } - + hash.each_pair do |class_name, assocs| clazz=Module.const_get(class_name) assocs.each_pair do |assoc_name, subobjects| @@ -191,14 +191,14 @@ def import(*args) import_helper(*args) end end - + def add_objects(hash, parent) hash[parent.class.name]||={} parent.class.reflect_on_all_autosave_associations.each do |assoc| hash[parent.class.name][assoc.name]||=[] - - changed_objects = parent.association(assoc.name).proxy.select {|a| a.new_record? || a.changed?} - changed_objects.each do |child| + + changed_objects = parent.association(assoc.name).select {|a| a.new_record? || a.changed?} + changed_objects.each do |child| child.send("#{assoc.foreign_key}=", parent.id) end hash[parent.class.name][assoc.name].concat changed_objects @@ -357,7 +357,7 @@ def import_without_validations_or_callbacks( column_names, array_of_attributes, post_sql_statements = connection.post_sql_statements( quoted_table_name, options ) # perform the inserts - (number_inserted,ids) = connection.insert_many( [ insert_sql, post_sql_statements ].flatten, + (number_inserted,ids) = connection.insert_many( [ insert_sql, post_sql_statements ].flatten, values_sql, "#{self.class.name} Create Many Without Validations Or Callbacks" ) end From f42fe819f6bb3629d8947e0b049182b113b0f85d Mon Sep 17 00:00:00 2001 From: John Naegle Date: Wed, 19 Mar 2014 15:09:27 -0500 Subject: [PATCH 09/11] force activerecord version to 4.0 --- Gemfile | 2 +- activerecord-import.gemspec | 2 +- gemfiles/4.0.gemfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index a917b6fe..22f12bc5 100644 --- a/Gemfile +++ b/Gemfile @@ -34,6 +34,6 @@ platforms :mri_19 do gem "debugger" end -version = ENV['AR_VERSION'] || "3.2" +version = ENV['AR_VERSION'] || "4.0" eval_gemfile File.expand_path("../gemfiles/#{version}.gemfile", __FILE__) diff --git a/activerecord-import.gemspec b/activerecord-import.gemspec index a6848f48..5e7bf2ec 100644 --- a/activerecord-import.gemspec +++ b/activerecord-import.gemspec @@ -17,6 +17,6 @@ Gem::Specification.new do |gem| gem.required_ruby_version = ">= 1.9.2" - gem.add_runtime_dependency "activerecord", ">= 3.0" + gem.add_runtime_dependency "activerecord", ">= 4.0" gem.add_development_dependency "rake" end diff --git a/gemfiles/4.0.gemfile b/gemfiles/4.0.gemfile index c279c43f..d1b8a129 100644 --- a/gemfiles/4.0.gemfile +++ b/gemfiles/4.0.gemfile @@ -1,4 +1,4 @@ platforms :ruby do gem 'mysql', '~> 2.9' - gem 'activerecord', '~> 4.0.0.rc2' + gem 'activerecord', '~> 4.0.4' end From d11df613bea4ffbc9aa59e456a2e340ed1e3ce37 Mon Sep 17 00:00:00 2001 From: Robert Mathews Date: Wed, 9 Apr 2014 16:22:19 -0400 Subject: [PATCH 10/11] remove unused line --- lib/activerecord-import/import.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/activerecord-import/import.rb b/lib/activerecord-import/import.rb index c8b7c0d0..858df9c9 100644 --- a/lib/activerecord-import/import.rb +++ b/lib/activerecord-import/import.rb @@ -181,7 +181,6 @@ def import(*args) models.each {|model| add_objects(hash, model) } hash.each_pair do |class_name, assocs| - clazz=Module.const_get(class_name) assocs.each_pair do |assoc_name, subobjects| subobjects.first.class.import(subobjects, options) unless subobjects.empty? end From 7a79ecd6485550da39b8153aac601517abfa408b Mon Sep 17 00:00:00 2001 From: John Naegle Date: Sun, 13 Jul 2014 22:13:02 -0500 Subject: [PATCH 11/11] relax AR version to >= 3.2 --- activerecord-import.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord-import.gemspec b/activerecord-import.gemspec index 5e7bf2ec..84a1f222 100644 --- a/activerecord-import.gemspec +++ b/activerecord-import.gemspec @@ -17,6 +17,6 @@ Gem::Specification.new do |gem| gem.required_ruby_version = ">= 1.9.2" - gem.add_runtime_dependency "activerecord", ">= 4.0" + gem.add_runtime_dependency "activerecord", ">= 3.2" gem.add_development_dependency "rake" end