activerecord-import 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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