activerecord-import 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1f525ceb164067e5f2c49042b1a8f3312eab016a
4
- data.tar.gz: c9817adb82be93259525dc52a85b42bf7829d585
3
+ metadata.gz: 0f9f37b3a5f830a11ae96be25ce6f5c4b17a0644
4
+ data.tar.gz: 54a38cb34a3dcea2712144881922db6391dffb95
5
5
  SHA512:
6
- metadata.gz: 974e9a0491e43cd9a9a22e167f1b9dede5bcdececbb3d480c2b4edf2e293e50c0cf3e85f39b4d10f624c541a0651db5de2f85bad0d5ca7142c4de28017e1809f
7
- data.tar.gz: 4e31a0c10174ba285b08d47abbac06c2089c62488c5f3775d24c6b993b4c7bd465d051e5238e6df511da6ad8b152f47f5827b41cf316d5e8713dc9bd2c64e9bc
6
+ metadata.gz: e5711460969d86ad105727048bb1f86aa323f9bf9f48239b845ec7c2b95d5113e8bf0cf2e7bdafaca5d7a53dc5ce93b57e85e8468b69982c394e8d46d7dbadc5
7
+ data.tar.gz: 3877050ee37099c68deafb1f3532d781a4efbf3f50c049787e24676d7cc5131df70782e00f17abaef866766c30212b7cceceee41031cf7e2c834c20f18fbd381
@@ -8,7 +8,6 @@ env:
8
8
  # https://github.com/discourse/discourse/blob/master/.travis.yml
9
9
  - RUBY_GC_MALLOC_LIMIT=50000000
10
10
  matrix:
11
- - AR_VERSION=3.1
12
11
  - AR_VERSION=3.2
13
12
  - AR_VERSION=4.0
14
13
  - AR_VERSION=4.1
@@ -16,6 +15,13 @@ env:
16
15
  - AR_VERSION=5.0
17
16
 
18
17
  matrix:
18
+ include:
19
+ - rvm: jruby-9.0.5.0
20
+ env: AR_VERSION=4.2
21
+ script:
22
+ - bundle exec rake test:jdbcmysql
23
+ - bundle exec rake test:jdbcpostgresql
24
+
19
25
  fast_finish: true
20
26
 
21
27
  before_script:
@@ -33,9 +39,11 @@ addons:
33
39
 
34
40
  script:
35
41
  - bundle exec rake test:mysql2
42
+ - bundle exec rake test:mysql2_makara
36
43
  - bundle exec rake test:mysql2spatial
37
44
  - bundle exec rake test:postgis
38
45
  - bundle exec rake test:postgresql
46
+ - bundle exec rake test:postgresql_makara
39
47
  - bundle exec rake test:seamless_database_pool
40
48
  - bundle exec rake test:spatialite
41
49
  - bundle exec rake test:sqlite3
@@ -1,3 +1,26 @@
1
+ ## Changes in 0.14.0
2
+
3
+ ### New Features
4
+
5
+ * Support for ActiveRecord 3.1 has been dropped. Thanks to @sferik via \#254
6
+ * SQLite3 has learned the :recursive option. Thanks to @jkowens via \#281
7
+ * :on_duplicate_key_ignore will be ignored when imports are being done with :recursive. Thanks to @jkowens via \#268
8
+ * :activerecord-import learned how to tell PostgreSQL to return no data back from the import via the :no_returning boolean option. Thanks to @makaroni4 via \#276
9
+
10
+ ### Fixes
11
+
12
+ * Polymorphic associations will not import the :type column. Thanks to @seanlinsley via \#282 and \#283
13
+ * ~2X speed increase for importing models with validations. Thanks to @jkowens via \#266
14
+
15
+ ### Misc
16
+
17
+ * Benchmark HTML report has been fixed. Thanks to @jkowens via \#264
18
+ * seamless_database_pool has been updated to work with AR 5.0. Thanks to @jkowens via \#280
19
+ * Code cleanup, removal of redundant condition checks. Thanks to @pavlik4k via \#273
20
+ * Code cleanup, removal of deprecated `alias_method_chain`. Thanks to @codeodor via \#271
21
+
22
+
23
+
1
24
  ## Changes in 0.13.0
2
25
 
3
26
  ### New Features
data/Gemfile CHANGED
@@ -11,7 +11,7 @@ platforms :ruby do
11
11
  gem "mysql2", "~> 0.3.0"
12
12
  gem "pg", "~> 0.9"
13
13
  gem "sqlite3", "~> 1.3.10"
14
- gem "seamless_database_pool", "~> 1.0.13"
14
+ gem "seamless_database_pool", "~> 1.0.18"
15
15
  end
16
16
 
17
17
  platforms :jruby do
data/Rakefile CHANGED
@@ -13,7 +13,19 @@ namespace :display do
13
13
  end
14
14
  task default: ["display:notice"]
15
15
 
16
- ADAPTERS = %w(mysql2 jdbcmysql jdbcpostgresql postgresql sqlite3 seamless_database_pool mysql2spatial spatialite postgis).freeze
16
+ ADAPTERS = %w(
17
+ mysql2
18
+ mysql2_makara
19
+ mysql2spatial
20
+ jdbcmysql
21
+ jdbcpostgresql
22
+ postgresql
23
+ postgresql_makara
24
+ postgis
25
+ sqlite3
26
+ spatialite
27
+ seamless_database_pool
28
+ ).freeze
17
29
  ADAPTERS.each do |adapter|
18
30
  namespace :test do
19
31
  desc "Runs #{adapter} database tests."
@@ -18,6 +18,6 @@ Gem::Specification.new do |gem|
18
18
 
19
19
  gem.required_ruby_version = ">= 1.9.2"
20
20
 
21
- gem.add_runtime_dependency "activerecord", ">= 3.0"
21
+ gem.add_runtime_dependency "activerecord", ">= 3.2"
22
22
  gem.add_development_dependency "rake"
23
23
  end
@@ -126,7 +126,9 @@ class BenchmarkBase
126
126
  # Deletes all records from all ActiveRecord subclasses
127
127
  def delete_all
128
128
  ActiveRecord::Base.send( :subclasses ).each do |subclass|
129
- subclass.delete_all if subclass.respond_to? :delete_all
129
+ if subclass.table_exists? && subclass.respond_to?(:delete_all)
130
+ subclass.delete_all
131
+ end
130
132
  end
131
133
  end
132
134
 
@@ -51,7 +51,7 @@ EOT
51
51
  else
52
52
  time = result.tms.real.round_to( 3 )
53
53
  speedup = ( result_set.first.tms.real / result.tms.real ).round
54
- times << result == result_set.first ? time.to_s : "#{time} (#{speedup}x speedup)"
54
+ times << (result == result_set.first ? time.to_s : "#{time} (#{speedup}x speedup)")
55
55
  end
56
56
  end
57
57
 
@@ -12,7 +12,8 @@ ActiveSupport.on_load(:active_record) do
12
12
  ActiveRecord::Import.load_from_connection_pool connection_pool
13
13
  conn
14
14
  end
15
- alias_method_chain :establish_connection, :activerecord_import
15
+ alias establish_connection_without_activerecord_import establish_connection
16
+ alias establish_connection establish_connection_with_activerecord_import
16
17
  end
17
18
  end
18
19
  end
@@ -47,7 +47,10 @@ module ActiveRecord::Import::AbstractAdapter
47
47
 
48
48
  if supports_on_duplicate_key_update?
49
49
  if options[:on_duplicate_key_ignore] && respond_to?(:sql_for_on_duplicate_key_ignore)
50
- post_sql_statements << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
50
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
51
+ unless options[:recursive]
52
+ post_sql_statements << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
53
+ end
51
54
  elsif options[:on_duplicate_key_update]
52
55
  post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
53
56
  end
@@ -26,7 +26,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
26
26
  end
27
27
 
28
28
  def post_sql_statements( table_name, options ) # :nodoc:
29
- if options[:primary_key].blank?
29
+ if options[:no_returning] || options[:primary_key].blank?
30
30
  super(table_name, options)
31
31
  else
32
32
  super(table_name, options) << "RETURNING #{options[:primary_key]}"
@@ -19,6 +19,8 @@ module ActiveRecord::Import::SQLite3Adapter
19
19
  # elements that are in position >= 1 will be appended to the final SQL.
20
20
  def insert_many(sql, values, *args) # :nodoc:
21
21
  number_of_inserts = 0
22
+ ids = []
23
+
22
24
  base_sql, post_sql = if sql.is_a?( String )
23
25
  [sql, '']
24
26
  elsif sql.is_a?( Array )
@@ -31,13 +33,19 @@ module ActiveRecord::Import::SQLite3Adapter
31
33
  value_sets.each do |value_set|
32
34
  number_of_inserts += 1
33
35
  sql2insert = base_sql + value_set.join( ',' ) + post_sql
34
- insert( sql2insert, *args )
36
+ first_insert_id = insert( sql2insert, *args )
37
+ last_insert_id = first_insert_id + value_set.size - 1
38
+ ids.concat((first_insert_id..last_insert_id).to_a)
35
39
  end
36
40
 
37
- [number_of_inserts, []]
41
+ [number_of_inserts, ids]
38
42
  end
39
43
 
40
44
  def next_value_for_sequence(sequence_name)
41
45
  %{nextval('#{sequence_name}')}
42
46
  end
47
+
48
+ def support_setting_primary_key_of_imported_objects?
49
+ true
50
+ end
43
51
  end
@@ -7,8 +7,10 @@ module ActiveRecord::Import
7
7
 
8
8
  def self.base_adapter(adapter)
9
9
  case adapter
10
+ when 'mysql2_makara' then 'mysql2'
10
11
  when 'mysql2spatial' then 'mysql2'
11
12
  when 'spatialite' then 'sqlite3'
13
+ when 'postgresql_makara' then 'postgresql'
12
14
  when 'postgis' then 'postgresql'
13
15
  else adapter
14
16
  end
@@ -61,13 +61,14 @@ class ActiveRecord::Associations::CollectionAssociation
61
61
 
62
62
  models.each do |m|
63
63
  m.public_send "#{symbolized_foreign_key}=", owner_primary_key_value
64
+ m.public_send "#{reflection.type}=", owner.class.name if reflection.type
64
65
  end
65
66
 
66
67
  return model_klass.import column_names, models, options
67
68
 
68
69
  # supports empty array
69
70
  elsif args.last.is_a?( Array ) && args.last.empty?
70
- return ActiveRecord::Import::Result.new([], 0, []) if args.last.empty?
71
+ return ActiveRecord::Import::Result.new([], 0, [])
71
72
 
72
73
  # supports 2-element array and array
73
74
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
@@ -82,6 +83,11 @@ class ActiveRecord::Associations::CollectionAssociation
82
83
  array_of_attributes.each { |attrs| attrs << owner_primary_key_value }
83
84
  end
84
85
 
86
+ if reflection.type
87
+ column_names << reflection.type
88
+ array_of_attributes.each { |attrs| attrs << owner.class.name }
89
+ end
90
+
85
91
  return model_klass.import column_names, array_of_attributes, options
86
92
  else
87
93
  raise ArgumentError, "Invalid arguments!"
@@ -168,7 +174,8 @@ class ActiveRecord::Base
168
174
  # * +ignore+ - true|false, tells import to use MySQL's INSERT IGNORE
169
175
  # to discard records that contain duplicate keys.
170
176
  # * +on_duplicate_key_ignore+ - true|false, tells import to use
171
- # Postgres 9.5+ ON CONFLICT DO NOTHING.
177
+ # Postgres 9.5+ ON CONFLICT DO NOTHING. Cannot be enabled on a
178
+ # recursive import.
172
179
  # * +on_duplicate_key_update+ - an Array or Hash, tells import to
173
180
  # use MySQL's ON DUPLICATE KEY UPDATE or Postgres 9.5+ ON CONFLICT
174
181
  # DO UPDATE ability. See On Duplicate Key Update below.
@@ -350,18 +357,13 @@ class ActiveRecord::Base
350
357
  # this next line breaks sqlite.so with a segmentation fault
351
358
  # if model.new_record? || options[:on_duplicate_key_update]
352
359
  column_names.map do |name|
353
- name = name.to_s
354
- if respond_to?(:defined_enums) && defined_enums.key?(name) # ActiveRecord 5
355
- model.read_attribute(name)
356
- else
357
- model.read_attribute_before_type_cast(name)
358
- end
360
+ model.read_attribute_before_type_cast(name.to_s)
359
361
  end
360
362
  # end
361
363
  end
362
364
  # supports empty array
363
365
  elsif args.last.is_a?( Array ) && args.last.empty?
364
- return ActiveRecord::Import::Result.new([], 0, []) if args.last.empty?
366
+ return ActiveRecord::Import::Result.new([], 0, [])
365
367
  # supports 2-element array and array
366
368
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
367
369
  column_names, array_of_attributes = args
@@ -387,7 +389,19 @@ class ActiveRecord::Base
387
389
  end
388
390
 
389
391
  return_obj = if is_validating
390
- import_with_validations( column_names, array_of_attributes, options )
392
+ if models
393
+ import_with_validations( column_names, array_of_attributes, options ) do |failed|
394
+ models.each_with_index do |model, i|
395
+ model = model.dup if options[:recursive]
396
+ next if model.valid?(options[:validate_with_context])
397
+ model.send(:raise_record_invalid) if options[:raise_error]
398
+ array_of_attributes[i] = nil
399
+ failed << model
400
+ end
401
+ end
402
+ else
403
+ import_with_validations( column_names, array_of_attributes, options )
404
+ end
391
405
  else
392
406
  (num_inserts, ids) = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
393
407
  ActiveRecord::Import::Result.new([], num_inserts, ids)
@@ -424,21 +438,24 @@ class ActiveRecord::Base
424
438
  def import_with_validations( column_names, array_of_attributes, options = {} )
425
439
  failed_instances = []
426
440
 
427
- # create instances for each of our column/value sets
428
- arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
441
+ if block_given?
442
+ yield failed_instances
443
+ else
444
+ # create instances for each of our column/value sets
445
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
429
446
 
430
- # keep track of the instance and the position it is currently at. if this fails
431
- # validation we'll use the index to remove it from the array_of_attributes
432
- arr.each_with_index do |hsh, i|
433
- instance = new do |model|
447
+ # keep track of the instance and the position it is currently at. if this fails
448
+ # validation we'll use the index to remove it from the array_of_attributes
449
+ model = new
450
+ arr.each_with_index do |hsh, i|
434
451
  hsh.each_pair { |k, v| model[k] = v }
452
+ next if model.valid?(options[:validate_with_context])
453
+ raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
454
+ array_of_attributes[i] = nil
455
+ failed_instances << model.dup
435
456
  end
436
-
437
- next if instance.valid?(options[:validate_with_context])
438
- raise(ActiveRecord::RecordInvalid, instance) if options[:raise_error]
439
- array_of_attributes[i] = nil
440
- failed_instances << instance
441
457
  end
458
+
442
459
  array_of_attributes.compact!
443
460
 
444
461
  num_inserts, ids = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
@@ -501,7 +518,7 @@ class ActiveRecord::Base
501
518
  end
502
519
  else
503
520
  values_sql.each do |values|
504
- connection.execute(insert_sql + values)
521
+ ids << connection.insert(insert_sql + values)
505
522
  number_inserted += 1
506
523
  end
507
524
  end
@@ -517,7 +534,7 @@ class ActiveRecord::Base
517
534
  model.id = id.to_i
518
535
  if model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
519
536
  model.clear_changes_information
520
- else # Rails 3.1
537
+ else # Rails 3.2
521
538
  model.instance_variable_get(:@changed_attributes).clear
522
539
  end
523
540
  model.instance_variable_set(:@new_record, false)
@@ -590,7 +607,7 @@ class ActiveRecord::Base
590
607
  connection_memo.quote(type_caster.type_cast_for_database(column.name, val))
591
608
  elsif column.respond_to?(:type_cast_from_user) # Rails 4.2 and higher
592
609
  connection_memo.quote(column.type_cast_from_user(val), column)
593
- else # Rails 3.1, 3.2, 4.0 and 4.1
610
+ else # Rails 3.2, 4.0 and 4.1
594
611
  if serialized_attributes.include?(column.name)
595
612
  val = serialized_attributes[column.name].dump(val)
596
613
  end
@@ -636,9 +653,7 @@ class ActiveRecord::Base
636
653
 
637
654
  # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
638
655
  def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
639
- array_of_attributes.map do |attributes|
640
- Hash[attributes.each_with_index.map { |attr, c| [column_names[c], attr] }]
641
- end
656
+ array_of_attributes.map { |values| Hash[column_names.zip(values)] }
642
657
  end
643
658
  end
644
659
  end
@@ -47,7 +47,7 @@ module ActiveRecord # :nodoc:
47
47
  instance.clear_changes_information # Rails 4.2 and higher
48
48
  else
49
49
  instance.instance_variable_set :@attributes_cache, {} # Rails 4.0, 4.1
50
- instance.changed_attributes.clear # Rails 3.1, 3.2
50
+ instance.changed_attributes.clear # Rails 3.2
51
51
  instance.previous_changes.clear
52
52
  end
53
53
 
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Import
3
- VERSION = "0.13.0".freeze
3
+ VERSION = "0.14.0".freeze
4
4
  end
5
5
  end
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "mysql2_makara"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "postgresql"
@@ -12,12 +12,8 @@ mysql2: &mysql2
12
12
  mysql2spatial:
13
13
  <<: *mysql2
14
14
 
15
- seamless_database_pool:
16
- <<: *common
17
- adapter: seamless_database_pool
18
- pool_adapter: mysql2
19
- master:
20
- host: localhost
15
+ mysql2_makara:
16
+ <<: *mysql2
21
17
 
22
18
  postgresql: &postgresql
23
19
  <<: *common
@@ -25,6 +21,9 @@ postgresql: &postgresql
25
21
  adapter: postgresql
26
22
  min_messages: warning
27
23
 
24
+ postresql_makara:
25
+ <<: *postgresql
26
+
28
27
  postgis:
29
28
  <<: *postgresql
30
29
 
@@ -33,6 +32,14 @@ oracle:
33
32
  adapter: oracle
34
33
  min_messages: debug
35
34
 
35
+ seamless_database_pool:
36
+ <<: *common
37
+ adapter: seamless_database_pool
38
+ prepared_statements: false
39
+ pool_adapter: mysql2
40
+ master:
41
+ host: localhost
42
+
36
43
  sqlite:
37
44
  adapter: sqlite
38
45
  dbfile: test.db
@@ -363,36 +363,30 @@ describe "#import" do
363
363
  end
364
364
 
365
365
  context "importing through an association scope" do
366
- [true, false].each do |bool|
367
- context "when validation is " + (bool ? "enabled" : "disabled") do
368
- it "should automatically set the foreign key column" do
369
- books = [["David Chelimsky", "The RSpec Book"], ["Chad Fowler", "Rails Recipes"]]
370
- topic = FactoryGirl.create :topic
371
- topic.books.import [:author_name, :title], books, validate: bool
372
- assert_equal 2, topic.books.count
373
- assert topic.books.all? { |book| book.topic_id == topic.id }
366
+ { has_many: :chapters, polymorphic: :discounts }.each do |association_type, association|
367
+ let(:book) { FactoryGirl.create :book }
368
+ let(:scope) { book.public_send association }
369
+ let(:klass) { { chapters: Chapter, discounts: Discount }[association] }
370
+ let(:column) { { chapters: :title, discounts: :amount }[association] }
371
+ let(:val1) { { chapters: 'A', discounts: 5 }[association] }
372
+ let(:val2) { { chapters: 'B', discounts: 6 }[association] }
373
+
374
+ context "for #{association_type}" do
375
+ it "works importing models" do
376
+ scope.import [
377
+ klass.new(column => val1),
378
+ klass.new(column => val2)
379
+ ]
380
+
381
+ assert_equal [val1, val2], scope.map(&column).sort
374
382
  end
375
- end
376
- end
377
383
 
378
- it "works importing models" do
379
- topic = FactoryGirl.create :topic
380
- books = [
381
- Book.new(author_name: "Author #1", title: "Book #1"),
382
- Book.new(author_name: "Author #2", title: "Book #2"),
383
- ]
384
- topic.books.import books
385
- assert_equal 2, topic.books.count
386
- assert topic.books.detect { |b| b.title == "Book #1" && b.author_name == "Author #1" }
387
- assert topic.books.detect { |b| b.title == "Book #2" && b.author_name == "Author #2" }
388
- end
384
+ it "works importing array of columns and values" do
385
+ scope.import [column], [[val1], [val2]]
389
386
 
390
- it "works importing array of columns and values" do
391
- topic = FactoryGirl.create :topic
392
- topic.books.import [:author_name, :title], [["Author #1", "Book #1"], ["Author #2", "Book #2"]]
393
- assert_equal 2, topic.books.count
394
- assert topic.books.detect { |b| b.title == "Book #1" && b.author_name == "Author #1" }
395
- assert topic.books.detect { |b| b.title == "Book #2" && b.author_name == "Author #2" }
387
+ assert_equal [val1, val2], scope.map(&column).sort
388
+ end
389
+ end
396
390
  end
397
391
  end
398
392
 
@@ -451,6 +445,25 @@ describe "#import" do
451
445
  end
452
446
  end
453
447
 
448
+ context 'When importing arrays of values with Enum fields' do
449
+ let(:columns) { [:author_name, :title, :status] }
450
+ let(:values) { [['Author #1', 'Book #1', 0], ['Author #2', 'Book #2', 1]] }
451
+
452
+ it 'should be able to import enum fields' do
453
+ Book.delete_all if Book.count > 0
454
+ Book.import columns, values
455
+ assert_equal 2, Book.count
456
+
457
+ if ENV['AR_VERSION'].to_i >= 5.0
458
+ assert_equal 'draft', Book.first.read_attribute('status')
459
+ assert_equal 'published', Book.last.read_attribute('status')
460
+ else
461
+ assert_equal 0, Book.first.read_attribute('status')
462
+ assert_equal 1, Book.last.read_attribute('status')
463
+ end
464
+ end
465
+ end
466
+
454
467
  describe "importing when model has default_scope" do
455
468
  it "doesn't import the default scope values" do
456
469
  assert_difference "Widget.unscoped.count", +2 do
@@ -0,0 +1,6 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../support/assertions')
4
+ require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/import_examples')
5
+
6
+ should_support_mysql_import_functionality
@@ -1,5 +1,7 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
2
 
3
+ should_support_recursive_import
4
+
3
5
  describe "#supports_imports?" do
4
6
  context "and SQLite is 3.7.11 or higher" do
5
7
  it "supports import" do
@@ -49,4 +49,9 @@ FactoryGirl.define do
49
49
  end
50
50
  end
51
51
  end
52
+
53
+ factory :book do
54
+ title 'Tortilla Flat'
55
+ author_name 'John Steinbeck'
56
+ end
52
57
  end
@@ -1,5 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  def should_support_postgresql_import_functionality
3
+ should_support_recursive_import
4
+
3
5
  describe "#supports_imports?" do
4
6
  it "should support import" do
5
7
  assert ActiveRecord::Base.supports_import?
@@ -18,112 +20,6 @@ def should_support_postgresql_import_functionality
18
20
  end
19
21
  end
20
22
 
21
- describe "importing objects with associations" do
22
- let(:new_topics) { Build(num_topics, :topic_with_book) }
23
- let(:new_topics_with_invalid_chapter) do
24
- chapter = new_topics.first.books.first.chapters.first
25
- chapter.title = nil
26
- new_topics
27
- end
28
- let(:num_topics) { 3 }
29
- let(:num_books) { 6 }
30
- let(:num_chapters) { 18 }
31
- let(:num_endnotes) { 24 }
32
-
33
- let(:new_question_with_rule) { FactoryGirl.build :question, :with_rule }
34
-
35
- it 'imports top level' do
36
- assert_difference "Topic.count", +num_topics do
37
- Topic.import new_topics, recursive: true
38
- new_topics.each do |topic|
39
- assert_not_nil topic.id
40
- end
41
- end
42
- end
43
-
44
- it 'imports first level associations' do
45
- assert_difference "Book.count", +num_books do
46
- Topic.import new_topics, recursive: true
47
- new_topics.each do |topic|
48
- topic.books.each do |book|
49
- assert_equal topic.id, book.topic_id
50
- end
51
- end
52
- end
53
- end
54
-
55
- it 'imports polymorphic associations' do
56
- discounts = Array.new(1) { |i| Discount.new(amount: i) }
57
- books = Array.new(1) { |i| Book.new(author_name: "Author ##{i}", title: "Book ##{i}") }
58
- books.each do |book|
59
- book.discounts << discounts
60
- end
61
- Book.import books, recursive: true
62
- books.each do |book|
63
- book.discounts.each do |discount|
64
- assert_not_nil discount.discountable_id
65
- assert_equal 'Book', discount.discountable_type
66
- end
67
- end
68
- end
69
-
70
- [{ recursive: false }, {}].each do |import_options|
71
- it "skips recursion for #{import_options}" do
72
- assert_difference "Book.count", 0 do
73
- Topic.import new_topics, import_options
74
- end
75
- end
76
- end
77
-
78
- it 'imports deeper nested associations' do
79
- assert_difference "Chapter.count", +num_chapters do
80
- assert_difference "EndNote.count", +num_endnotes do
81
- Topic.import new_topics, recursive: true
82
- new_topics.each do |topic|
83
- topic.books.each do |book|
84
- book.chapters.each do |chapter|
85
- assert_equal book.id, chapter.book_id
86
- end
87
- book.end_notes.each do |endnote|
88
- assert_equal book.id, endnote.book_id
89
- end
90
- end
91
- end
92
- end
93
- end
94
- end
95
-
96
- it "skips validation of the associations if requested" do
97
- assert_difference "Chapter.count", +num_chapters do
98
- Topic.import new_topics_with_invalid_chapter, validate: false, recursive: true
99
- end
100
- end
101
-
102
- it 'imports has_one associations' do
103
- assert_difference 'Rule.count' do
104
- Question.import [new_question_with_rule], recursive: true
105
- end
106
- end
107
-
108
- # These models dont validate associated. So we expect that books and topics get inserted, but not chapters
109
- # Putting a transaction around everything wouldn't work, so if you want your chapters to prevent topics from
110
- # being created, you would need to have validates_associated in your models and insert with validation
111
- describe "all_or_none" do
112
- [Book, Topic, EndNote].each do |type|
113
- it "creates #{type}" do
114
- assert_difference "#{type}.count", send("num_#{type.to_s.downcase}s") do
115
- Topic.import new_topics_with_invalid_chapter, all_or_none: true, recursive: true
116
- end
117
- end
118
- end
119
- it "doesn't create chapters" do
120
- assert_difference "Chapter.count", 0 do
121
- Topic.import new_topics_with_invalid_chapter, all_or_none: true, recursive: true
122
- end
123
- end
124
- end
125
- end
126
-
127
23
  describe "with query cache enabled" do
128
24
  setup do
129
25
  unless ActiveRecord::Base.connection.query_cache_enabled
@@ -147,6 +43,20 @@ def should_support_postgresql_import_functionality
147
43
  end
148
44
  end
149
45
  end
46
+
47
+ describe "no_returning" do
48
+ let(:books) { [Book.new(author_name: "foo", title: "bar")] }
49
+
50
+ it "creates records" do
51
+ assert_difference "Book.count", +1 do
52
+ Book.import books, no_returning: true
53
+ end
54
+ end
55
+
56
+ it "returns no ids" do
57
+ assert_equal [], Book.import(books, no_returning: true).ids
58
+ end
59
+ end
150
60
  end
151
61
  end
152
62
 
@@ -164,28 +74,32 @@ def should_support_postgresql_upsert_functionality
164
74
  let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
165
75
  let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
166
76
 
167
- macro(:perform_import) do |*opts|
168
- Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_ignore: value, validate: false)
169
- end
170
-
171
77
  setup do
172
78
  Topic.import columns, values, validate: false
173
- @topic = Topic.find 99
174
79
  end
175
80
 
176
- context "using true" do
177
- let(:value) { true }
178
- should_not_update_updated_at_on_timestamp_columns
81
+ it "should not update any records" do
82
+ result = Topic.import columns, updated_values, on_duplicate_key_ignore: true, validate: false
83
+ assert_equal [], result.ids
179
84
  end
85
+ end
86
+
87
+ context "with :on_duplicate_key_ignore and :recursive enabled" do
88
+ let(:new_topic) { Build(1, :topic_with_book) }
89
+ let(:mixed_topics) { Build(1, :topic_with_book) + new_topic + Build(1, :topic_with_book) }
180
90
 
181
- context "using hash with :conflict_target" do
182
- let(:value) { { conflict_target: :id } }
183
- should_not_update_updated_at_on_timestamp_columns
91
+ setup do
92
+ Topic.import new_topic, recursive: true
184
93
  end
185
94
 
186
- context "using hash with :constraint_target" do
187
- let(:value) { { constraint_name: :topics_pkey } }
188
- should_not_update_updated_at_on_timestamp_columns
95
+ # Recursive import depends on the primary keys of the parent model being returned
96
+ # on insert. With on_duplicate_key_ignore enabled, not all ids will be returned
97
+ # and it is possible that a model will be assigned the wrong id and then its children
98
+ # would be associated with the wrong parent.
99
+ it ":on_duplicate_key_ignore is ignored" do
100
+ assert_raise ActiveRecord::RecordNotUnique do
101
+ Topic.import mixed_topics, recursive: true, on_duplicate_key_ignore: true
102
+ end
189
103
  end
190
104
  end
191
105
 
@@ -294,19 +208,6 @@ def should_support_postgresql_upsert_functionality
294
208
  should_update_updated_at_on_timestamp_columns
295
209
  end
296
210
  end
297
-
298
- context "with recursive: true" do
299
- let(:new_topics) { Build(1, :topic_with_book) }
300
-
301
- it "imports objects with associations" do
302
- assert_difference "Topic.count", +1 do
303
- Topic.import new_topics, recursive: true, on_duplicate_key_update: [:updated_at], validate: false
304
- new_topics.each do |topic|
305
- assert_not_nil topic.id
306
- end
307
- end
308
- end
309
- end
310
211
  end
311
212
  end
312
213
  end
@@ -5,65 +5,76 @@ def should_support_basic_on_duplicate_key_update
5
5
  macro(:perform_import) { raise "supply your own #perform_import in a context below" }
6
6
  macro(:updated_topic) { Topic.find(@topic.id) }
7
7
 
8
- context "with :on_duplicate_key_update and validation checks turned off" do
9
- asssertion_group(:should_support_on_duplicate_key_update) do
10
- should_not_update_fields_not_mentioned
11
- should_update_foreign_keys
12
- should_not_update_created_at_on_timestamp_columns
13
- should_update_updated_at_on_timestamp_columns
8
+ context "with :on_duplicate_key_update" do
9
+ describe "argument safety" do
10
+ it "should not modify the passed in :on_duplicate_key_update columns array" do
11
+ assert_nothing_raised do
12
+ columns = %w(title author_name).freeze
13
+ Topic.import columns, [%w(foo, bar)], on_duplicate_key_update: columns
14
+ end
15
+ end
14
16
  end
15
17
 
16
- let(:columns) { %w( id title author_name author_email_address parent_id ) }
17
- let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
18
- let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
18
+ context "with validation checks turned off" do
19
+ asssertion_group(:should_support_on_duplicate_key_update) do
20
+ should_not_update_fields_not_mentioned
21
+ should_update_foreign_keys
22
+ should_not_update_created_at_on_timestamp_columns
23
+ should_update_updated_at_on_timestamp_columns
24
+ end
19
25
 
20
- macro(:perform_import) do |*opts|
21
- Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: update_columns, validate: false)
22
- end
26
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
27
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
28
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
23
29
 
24
- setup do
25
- Topic.import columns, values, validate: false
26
- @topic = Topic.find 99
27
- end
30
+ macro(:perform_import) do |*opts|
31
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: update_columns, validate: false)
32
+ end
28
33
 
29
- context "using an empty array" do
30
- let(:update_columns) { [] }
31
- should_not_update_fields_not_mentioned
32
- should_update_updated_at_on_timestamp_columns
33
- end
34
+ setup do
35
+ Topic.import columns, values, validate: false
36
+ @topic = Topic.find 99
37
+ end
34
38
 
35
- context "using string column names" do
36
- let(:update_columns) { %w(title author_email_address parent_id) }
37
- should_support_on_duplicate_key_update
38
- should_update_fields_mentioned
39
- end
39
+ context "using an empty array" do
40
+ let(:update_columns) { [] }
41
+ should_not_update_fields_not_mentioned
42
+ should_update_updated_at_on_timestamp_columns
43
+ end
40
44
 
41
- context "using symbol column names" do
42
- let(:update_columns) { [:title, :author_email_address, :parent_id] }
43
- should_support_on_duplicate_key_update
44
- should_update_fields_mentioned
45
+ context "using string column names" do
46
+ let(:update_columns) { %w(title author_email_address parent_id) }
47
+ should_support_on_duplicate_key_update
48
+ should_update_fields_mentioned
49
+ end
50
+
51
+ context "using symbol column names" do
52
+ let(:update_columns) { [:title, :author_email_address, :parent_id] }
53
+ should_support_on_duplicate_key_update
54
+ should_update_fields_mentioned
55
+ end
45
56
  end
46
- end
47
57
 
48
- context "with a table that has a non-standard primary key" do
49
- let(:columns) { [:promotion_id, :code] }
50
- let(:values) { [[1, 'DISCOUNT1']] }
51
- let(:updated_values) { [[1, 'DISCOUNT2']] }
52
- let(:update_columns) { [:code] }
58
+ context "with a table that has a non-standard primary key" do
59
+ let(:columns) { [:promotion_id, :code] }
60
+ let(:values) { [[1, 'DISCOUNT1']] }
61
+ let(:updated_values) { [[1, 'DISCOUNT2']] }
62
+ let(:update_columns) { [:code] }
53
63
 
54
- macro(:perform_import) do |*opts|
55
- Promotion.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: update_columns, validate: false)
56
- end
57
- macro(:updated_promotion) { Promotion.find(@promotion.promotion_id) }
64
+ macro(:perform_import) do |*opts|
65
+ Promotion.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: update_columns, validate: false)
66
+ end
67
+ macro(:updated_promotion) { Promotion.find(@promotion.promotion_id) }
58
68
 
59
- setup do
60
- Promotion.import columns, values, validate: false
61
- @promotion = Promotion.find 1
62
- end
69
+ setup do
70
+ Promotion.import columns, values, validate: false
71
+ @promotion = Promotion.find 1
72
+ end
63
73
 
64
- it "should update specified columns" do
65
- perform_import
66
- assert_equal 'DISCOUNT2', updated_promotion.code
74
+ it "should update specified columns" do
75
+ perform_import
76
+ assert_equal 'DISCOUNT2', updated_promotion.code
77
+ end
67
78
  end
68
79
  end
69
80
 
@@ -0,0 +1,122 @@
1
+ def should_support_recursive_import
2
+ describe "importing objects with associations" do
3
+ let(:new_topics) { Build(num_topics, :topic_with_book) }
4
+ let(:new_topics_with_invalid_chapter) do
5
+ chapter = new_topics.first.books.first.chapters.first
6
+ chapter.title = nil
7
+ new_topics
8
+ end
9
+ let(:num_topics) { 3 }
10
+ let(:num_books) { 6 }
11
+ let(:num_chapters) { 18 }
12
+ let(:num_endnotes) { 24 }
13
+
14
+ let(:new_question_with_rule) { FactoryGirl.build :question, :with_rule }
15
+
16
+ it 'imports top level' do
17
+ assert_difference "Topic.count", +num_topics do
18
+ Topic.import new_topics, recursive: true
19
+ new_topics.each do |topic|
20
+ assert_not_nil topic.id
21
+ end
22
+ end
23
+ end
24
+
25
+ it 'imports first level associations' do
26
+ assert_difference "Book.count", +num_books do
27
+ Topic.import new_topics, recursive: true
28
+ new_topics.each do |topic|
29
+ topic.books.each do |book|
30
+ assert_equal topic.id, book.topic_id
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ it 'imports polymorphic associations' do
37
+ discounts = Array.new(1) { |i| Discount.new(amount: i) }
38
+ books = Array.new(1) { |i| Book.new(author_name: "Author ##{i}", title: "Book ##{i}") }
39
+ books.each do |book|
40
+ book.discounts << discounts
41
+ end
42
+ Book.import books, recursive: true
43
+ books.each do |book|
44
+ book.discounts.each do |discount|
45
+ assert_not_nil discount.discountable_id
46
+ assert_equal 'Book', discount.discountable_type
47
+ end
48
+ end
49
+ end
50
+
51
+ [{ recursive: false }, {}].each do |import_options|
52
+ it "skips recursion for #{import_options}" do
53
+ assert_difference "Book.count", 0 do
54
+ Topic.import new_topics, import_options
55
+ end
56
+ end
57
+ end
58
+
59
+ it 'imports deeper nested associations' do
60
+ assert_difference "Chapter.count", +num_chapters do
61
+ assert_difference "EndNote.count", +num_endnotes do
62
+ Topic.import new_topics, recursive: true
63
+ new_topics.each do |topic|
64
+ topic.books.each do |book|
65
+ book.chapters.each do |chapter|
66
+ assert_equal book.id, chapter.book_id
67
+ end
68
+ book.end_notes.each do |endnote|
69
+ assert_equal book.id, endnote.book_id
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ it "skips validation of the associations if requested" do
78
+ assert_difference "Chapter.count", +num_chapters do
79
+ Topic.import new_topics_with_invalid_chapter, validate: false, recursive: true
80
+ end
81
+ end
82
+
83
+ it 'imports has_one associations' do
84
+ assert_difference 'Rule.count' do
85
+ Question.import [new_question_with_rule], recursive: true
86
+ end
87
+ end
88
+
89
+ # These models dont validate associated. So we expect that books and topics get inserted, but not chapters
90
+ # Putting a transaction around everything wouldn't work, so if you want your chapters to prevent topics from
91
+ # being created, you would need to have validates_associated in your models and insert with validation
92
+ describe "all_or_none" do
93
+ [Book, Topic, EndNote].each do |type|
94
+ it "creates #{type}" do
95
+ assert_difference "#{type}.count", send("num_#{type.to_s.downcase}s") do
96
+ Topic.import new_topics_with_invalid_chapter, all_or_none: true, recursive: true
97
+ end
98
+ end
99
+ end
100
+ it "doesn't create chapters" do
101
+ assert_difference "Chapter.count", 0 do
102
+ Topic.import new_topics_with_invalid_chapter, all_or_none: true, recursive: true
103
+ end
104
+ end
105
+ end
106
+
107
+ # If adapter supports on_duplicate_key_update, it is only applied to top level models so that SQL with invalid
108
+ # columns, keys, etc isn't generated for child associations when doing recursive import
109
+ describe "on_duplicate_key_update" do
110
+ let(:new_topics) { Build(1, :topic_with_book) }
111
+
112
+ it "imports objects with associations" do
113
+ assert_difference "Topic.count", +1 do
114
+ Topic.import new_topics, recursive: true, on_duplicate_key_update: [:updated_at], validate: false
115
+ new_topics.each do |topic|
116
+ assert_not_nil topic.id
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -5,6 +5,16 @@ common: &common
5
5
  host: localhost
6
6
  database: activerecord_import_test
7
7
 
8
+ jdbcpostgresql: &postgresql
9
+ <<: *common
10
+ username: postgres
11
+ adapter: jdbcpostgresql
12
+ min_messages: warning
13
+
14
+ jdbcmysql: &mysql2
15
+ <<: *common
16
+ adapter: jdbcmysql
17
+
8
18
  mysql2: &mysql2
9
19
  <<: *common
10
20
  adapter: mysql2
@@ -12,12 +22,13 @@ mysql2: &mysql2
12
22
  mysql2spatial:
13
23
  <<: *mysql2
14
24
 
15
- seamless_database_pool:
25
+ mysql2_makara:
26
+ <<: *mysql2
27
+
28
+ oracle:
16
29
  <<: *common
17
- adapter: seamless_database_pool
18
- pool_adapter: mysql2
19
- master:
20
- host: localhost
30
+ adapter: oracle
31
+ min_messages: debug
21
32
 
22
33
  postgresql: &postgresql
23
34
  <<: *common
@@ -25,13 +36,19 @@ postgresql: &postgresql
25
36
  adapter: postgresql
26
37
  min_messages: warning
27
38
 
39
+ postresql_makara:
40
+ <<: *postgresql
41
+
28
42
  postgis:
29
43
  <<: *postgresql
30
44
 
31
- oracle:
45
+ seamless_database_pool:
32
46
  <<: *common
33
- adapter: oracle
34
- min_messages: debug
47
+ adapter: seamless_database_pool
48
+ pool_adapter: mysql2
49
+ prepared_statements: false
50
+ master:
51
+ host: localhost
35
52
 
36
53
  sqlite:
37
54
  adapter: sqlite
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-import
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Dennis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-01 00:00:00.000000000 Z
11
+ date: 2016-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
19
+ version: '3.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '3.0'
26
+ version: '3.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -69,7 +69,6 @@ files:
69
69
  - benchmarks/models/test_memory.rb
70
70
  - benchmarks/models/test_myisam.rb
71
71
  - benchmarks/schema/mysql_schema.rb
72
- - gemfiles/3.1.gemfile
73
72
  - gemfiles/3.2.gemfile
74
73
  - gemfiles/4.0.gemfile
75
74
  - gemfiles/4.1.gemfile
@@ -100,9 +99,11 @@ files:
100
99
  - test/adapters/jdbcmysql.rb
101
100
  - test/adapters/jdbcpostgresql.rb
102
101
  - test/adapters/mysql2.rb
102
+ - test/adapters/mysql2_makara.rb
103
103
  - test/adapters/mysql2spatial.rb
104
104
  - test/adapters/postgis.rb
105
105
  - test/adapters/postgresql.rb
106
+ - test/adapters/postgresql_makara.rb
106
107
  - test/adapters/seamless_database_pool.rb
107
108
  - test/adapters/spatialite.rb
108
109
  - test/adapters/sqlite3.rb
@@ -121,6 +122,7 @@ files:
121
122
  - test/models/topic.rb
122
123
  - test/models/widget.rb
123
124
  - test/mysql2/import_test.rb
125
+ - test/mysql2_makara/import_test.rb
124
126
  - test/mysqlspatial2/import_test.rb
125
127
  - test/postgis/import_test.rb
126
128
  - test/postgresql/import_test.rb
@@ -135,6 +137,7 @@ files:
135
137
  - test/support/mysql/import_examples.rb
136
138
  - test/support/postgresql/import_examples.rb
137
139
  - test/support/shared_examples/on_duplicate_key_update.rb
140
+ - test/support/shared_examples/recursive_import.rb
138
141
  - test/synchronize_test.rb
139
142
  - test/test_helper.rb
140
143
  - test/travis/database.yml
@@ -160,7 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
163
  version: '0'
161
164
  requirements: []
162
165
  rubyforge_project:
163
- rubygems_version: 2.4.5.1
166
+ rubygems_version: 2.5.1
164
167
  signing_key:
165
168
  specification_version: 4
166
169
  summary: Bulk-loading extension for ActiveRecord
@@ -168,9 +171,11 @@ test_files:
168
171
  - test/adapters/jdbcmysql.rb
169
172
  - test/adapters/jdbcpostgresql.rb
170
173
  - test/adapters/mysql2.rb
174
+ - test/adapters/mysql2_makara.rb
171
175
  - test/adapters/mysql2spatial.rb
172
176
  - test/adapters/postgis.rb
173
177
  - test/adapters/postgresql.rb
178
+ - test/adapters/postgresql_makara.rb
174
179
  - test/adapters/seamless_database_pool.rb
175
180
  - test/adapters/spatialite.rb
176
181
  - test/adapters/sqlite3.rb
@@ -189,6 +194,7 @@ test_files:
189
194
  - test/models/topic.rb
190
195
  - test/models/widget.rb
191
196
  - test/mysql2/import_test.rb
197
+ - test/mysql2_makara/import_test.rb
192
198
  - test/mysqlspatial2/import_test.rb
193
199
  - test/postgis/import_test.rb
194
200
  - test/postgresql/import_test.rb
@@ -203,6 +209,7 @@ test_files:
203
209
  - test/support/mysql/import_examples.rb
204
210
  - test/support/postgresql/import_examples.rb
205
211
  - test/support/shared_examples/on_duplicate_key_update.rb
212
+ - test/support/shared_examples/recursive_import.rb
206
213
  - test/synchronize_test.rb
207
214
  - test/test_helper.rb
208
215
  - test/travis/database.yml
@@ -1,3 +0,0 @@
1
- platforms :ruby do
2
- gem 'activerecord', '~> 3.1.0'
3
- end