activerecord-import 1.0.4 → 1.1.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
  SHA256:
3
- metadata.gz: cbf4c01d6a3f043ed541f7331d6a19e651cbc035fcc3030b17f1e741a6d4e8b7
4
- data.tar.gz: a740491de16c78d4d94ccb428c69342fd8a809f235ce9d42fc41237a4d15c7e6
3
+ metadata.gz: 4c61743fafaad0de04ccf2c6bff4439fe66aed03e951e5c42e4f7926cf8dac39
4
+ data.tar.gz: c58d8992957e546b73bb7f7b90eaf2a741f007d2109a16db0e3b3d71b376e1da
5
5
  SHA512:
6
- metadata.gz: 48e62637f6493cd5446cd78aee3db3b4a58e646fb0bc07d3583a662f89f52b03c2a079c83fc2cae807c5577f922e1e3219354cf36dff8ae8cc81ae68266e711f
7
- data.tar.gz: 2f40f5fd61975589f8af932a3dbf5b6d14d2cfc6884ebe7019c85956c8c380d3faebffb1b600a7fa04536fe262ed40688946c8d35b8cf5b1bfedd1ddff20af5a
6
+ metadata.gz: 98638b63235eae1c16f27d3be932b8ce4e8f236b24350766792a4fc563d199cb1d048d25efa5d22abf3a21017fa07b9474c3168a4be6c1ba5aaa6e0843d584a3
7
+ data.tar.gz: 5c62bed2684f1b6d60d7e26b8ad13c6b7173a64835eeca9474082122425ab1f340f9063868224eda2539878210887701022e1ff6e6a37593872682bf90768d3a
data/.gitignore CHANGED
@@ -24,6 +24,7 @@ pkg
24
24
  log/*.log
25
25
  test.db
26
26
  test/database.yml
27
+ benchmarks/log/
27
28
 
28
29
  .ruby-*
29
30
  .bundle/
data/.travis.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  language: ruby
2
2
  cache: bundler
3
3
  rvm:
4
- - 2.5.5
4
+ - 2.5.8
5
5
 
6
6
  env:
7
7
  global:
@@ -11,6 +11,7 @@ env:
11
11
  - AR_VERSION=5.1
12
12
  - AR_VERSION=5.2
13
13
  - AR_VERSION=6.0
14
+ - AR_VERSION=6.1
14
15
 
15
16
  matrix:
16
17
  include:
@@ -28,7 +29,7 @@ matrix:
28
29
  fast_finish: true
29
30
 
30
31
  addons:
31
- postgresql: "9.5"
32
+ postgresql: "10"
32
33
  apt:
33
34
  sources:
34
35
  - travis-ci/sqlite3
@@ -37,13 +38,14 @@ addons:
37
38
  - sqlite3
38
39
  - mysql-server
39
40
  - mysql-client
40
- - postgresql-9.5-postgis-2.4
41
+ - postgresql-10-postgis-2.4
41
42
 
42
43
  before_install:
44
+ - sudo apt-get update
43
45
  - gem update --system
44
46
  - sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;"
45
47
  - sudo mysql_upgrade
46
- - sudo service mysql restart
48
+ - sudo service mysql restart
47
49
 
48
50
  before_script:
49
51
  - mysql -e 'create database activerecord_import_test;'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,47 @@
1
+ ## Changes in 1.1.0
2
+
3
+ ### New Features
4
+
5
+ * Add batch progress reporting. Thanks to @gee-forr via \##729.
6
+
7
+ ## Changes in 1.0.8
8
+
9
+ ### Fixes
10
+
11
+ * Use correct method for clearing query cache. Thanks to @EtienneDepaulis via \##719.
12
+
13
+ ## Changes in 1.0.7
14
+
15
+ ### New Features
16
+
17
+ * Use @@max_allowed_packet session variable instead of querying SHOW VARIABLES. Thanks to @diclophis via \#706.
18
+ * Add option :track_validation_failures. When this is set to true, failed_instances will be an array of arrays, with each inner array having the form [:index_in_dataset, :object_with_errors]. Thanks to @rorymckinley via \#684.
19
+
20
+ ### Fixes
21
+
22
+ * Prevent mass-assignment errors in Rails strict mode. Thanks to @diclophis via \##709.
23
+
24
+ ## Changes in 1.0.6
25
+
26
+ ### Fixes
27
+
28
+ * Handle after_initialize callbacks. Thanks to @AhMohsen46 via \#691 and
29
+ \#692.
30
+ * Fix regression introduced in 1.0.4. Explicity allow adapters to
31
+ support on duplicate key update. Thanks to @dsobiera, @jkowens via \#696.
32
+
33
+ ## Changes in 1.0.5
34
+
35
+ ### Fixes
36
+
37
+ * Allow serialized attributes to be returned from import. Thanks to @timanovsky, @jkowens via \#660.
38
+ * Return ActiveRecord::Connection from
39
+ ActiveREcord::Base#establish_connection. Thanks to @reverentF via
40
+ \#663.
41
+ * Support PostgreSQL array. Thanks to @ujihisa via \#669.
42
+ * Skip loading association ids when column changed. Thanks to @Aristat
43
+ via \#673.
44
+
1
45
  ## Changes in 1.0.4
2
46
 
3
47
  ### Fixes
@@ -262,7 +306,7 @@
262
306
  Thanks to @jkowens via \#301.
263
307
  * Allow for custom timestamp columns. Thanks to @mojidabckuu, @jkowens
264
308
  via \#401.
265
-
309
+
266
310
  ### Fixes
267
311
 
268
312
  * Fix ActiveRecord 5 issue coercing boolean values when serializing
@@ -274,7 +318,7 @@
274
318
 
275
319
  * Fix issue where PostgreSQL cannot recognize columns if names
276
320
  include mixed case characters. Thanks to @hugobgranja via \#379.
277
- * Fix an issue for ActiveRecord 5 where serialized fields with
321
+ * Fix an issue for ActiveRecord 5 where serialized fields with
278
322
  default values were not being typecast. Thanks to @whistlerbrk,
279
323
  @jkowens via \#386.
280
324
  * Add option :force_single_insert for MySQL to make sure a single
data/Gemfile CHANGED
@@ -6,8 +6,11 @@ version = ENV['AR_VERSION'].to_f
6
6
 
7
7
  mysql2_version = '0.3.0'
8
8
  mysql2_version = '0.4.0' if version >= 4.2
9
+ mysql2_version = '0.5.0' if version >= 6.1
9
10
  sqlite3_version = '1.3.0'
10
11
  sqlite3_version = '1.4.0' if version >= 6.0
12
+ pg_version = '0.9'
13
+ pg_version = '1.1' if version >= 6.1
11
14
 
12
15
  group :development, :test do
13
16
  gem 'rubocop', '~> 0.40.0'
@@ -17,7 +20,7 @@ end
17
20
  # Database Adapters
18
21
  platforms :ruby do
19
22
  gem "mysql2", "~> #{mysql2_version}"
20
- gem "pg", "~> 0.9"
23
+ gem "pg", "~> #{pg_version}"
21
24
  gem "sqlite3", "~> #{sqlite3_version}"
22
25
  gem "seamless_database_pool", "~> 1.0.20"
23
26
  end
@@ -47,6 +50,7 @@ end
47
50
 
48
51
  platforms :ruby do
49
52
  gem "pry-byebug"
53
+ gem "pry", "~> 0.12.0"
50
54
  gem "rb-readline"
51
55
  end
52
56
 
data/LICENSE CHANGED
@@ -1,56 +1,21 @@
1
- Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
2
- You can redistribute it and/or modify it under either the terms of the
3
- 2-clause BSDL (see the file BSDL), or the conditions below:
4
-
5
- 1. You may make and give away verbatim copies of the source form of the
6
- software without restriction, provided that you duplicate all of the
7
- original copyright notices and associated disclaimers.
8
-
9
- 2. You may modify your copy of the software in any way, provided that
10
- you do at least ONE of the following:
11
-
12
- a) place your modifications in the Public Domain or otherwise
13
- make them Freely Available, such as by posting said
14
- modifications to Usenet or an equivalent medium, or by allowing
15
- the author to include your modifications in the software.
16
-
17
- b) use the modified software only within your corporation or
18
- organization.
19
-
20
- c) give non-standard binaries non-standard names, with
21
- instructions on where to get the original software distribution.
22
-
23
- d) make other distribution arrangements with the author.
24
-
25
- 3. You may distribute the software in object code or binary form,
26
- provided that you do at least ONE of the following:
27
-
28
- a) distribute the binaries and library files of the software,
29
- together with instructions (in the manual page or equivalent)
30
- on where to get the original distribution.
31
-
32
- b) accompany the distribution with the machine-readable source of
33
- the software.
34
-
35
- c) give non-standard binaries non-standard names, with
36
- instructions on where to get the original software distribution.
37
-
38
- d) make other distribution arrangements with the author.
39
-
40
- 4. You may modify and include the part of the software into any other
41
- software (possibly commercial). But some files in the distribution
42
- are not written by the author, so that they are not under these terms.
43
-
44
- For the list of those files and their copying conditions, see the
45
- file LEGAL.
46
-
47
- 5. The scripts and library files supplied as input to or produced as
48
- output from the software do not automatically fall under the
49
- copyright of the software, but belong to whomever generated them,
50
- and may be sold commercially, and may be aggregated with this
51
- software.
52
-
53
- 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
54
- IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
55
- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
56
- PURPOSE.
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Zach Dennis <zach.dennis@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.markdown CHANGED
@@ -60,6 +60,8 @@ The gem provides the following high-level features:
60
60
 
61
61
  #### Introduction
62
62
 
63
+ This gem adds an `import` method (or `bulk_import`, for compatibility with gems like `elasticsearch-model`; see [Conflicts With Other Gems](#conflicts-with-other-gems)) to ActiveRecord classes.
64
+
63
65
  Without `activerecord-import`, you'd write something like this:
64
66
 
65
67
  ```ruby
@@ -229,9 +231,22 @@ columns = [ :title ]
229
231
  Book.import columns, books, batch_size: 2
230
232
  ```
231
233
 
234
+ If your import is particularly large or slow (possibly due to [callbacks](#callbacks)) whilst batch importing, you might want a way to report back on progress. This is supported by passing a callable as the `batch_progress` option. e.g:
235
+
236
+ ```ruby
237
+ my_proc = ->(rows_size, num_batches, current_batch_number, batch_duration_in_secs) {
238
+ # Using the arguments provided to the callable, you can
239
+ # send an email, post to a websocket,
240
+ # update slack, alert if import is taking too long, etc.
241
+ }
242
+
243
+ Book.import columns, books, batch_size: 2, batch_progress: my_proc
244
+ ```
245
+
232
246
  #### Recursive
233
247
 
234
- NOTE: This only works with PostgreSQL.
248
+ NOTE: This only works with PostgreSQL and ActiveRecord objects. This won't work with
249
+ hashes or arrays as recursive inputs.
235
250
 
236
251
  Assume that Books <code>has_many</code> Reviews.
237
252
 
@@ -247,20 +262,21 @@ Book.import books, recursive: true
247
262
 
248
263
  ### Options
249
264
 
250
- Key | Options | Default | Description
251
- ----------------------- | --------------------- | ------------------ | -----------
252
- :validate | `true`/`false` | `true` | Whether or not to run `ActiveRecord` validations (uniqueness skipped). This option will always be true when using `import!`.
253
- :validate_uniqueness | `true`/`false` | `false` | Whether or not to run uniqueness validations, has potential pitfalls, use with caution (requires `>= v0.27.0`).
254
- :validate_with_context | `Symbol` |`:create`/`:update` | Allows passing an ActiveModel validation context for each model. Default is `:create` for new records and `:update` for existing ones.
255
- :on_duplicate_key_ignore| `true`/`false` | `false` | Allows skipping records with duplicate keys. See [here](https://github.com/zdennis/activerecord-import/#duplicate-key-ignore) for more details.
256
- :ignore | `true`/`false` | `false` | Alias for :on_duplicate_key_ignore.
257
- :on_duplicate_key_update| :all, `Array`, `Hash` | N/A | Allows upsert logic to be used. See [here](https://github.com/zdennis/activerecord-import/#duplicate-key-update) for more details.
258
- :synchronize | `Array` | N/A | An array of ActiveRecord instances. This synchronizes existing instances in memory with updates from the import.
259
- :timestamps | `true`/`false` | `true` | Enables/disables timestamps on imported records.
260
- :recursive | `true`/`false` | `false` | Imports has_many/has_one associations (PostgreSQL only).
261
- :batch_size | `Integer` | total # of records | Max number of records to insert per import
262
- :raise_error | `true`/`false` | `false` | Raises an exception at the first invalid record. This means there will not be a result object returned. The `import!` method is a shortcut for this.
263
- :all_or_none | `true`/`false` | `false` | Will not import any records if there is a record with validation errors.
265
+ Key | Options | Default | Description
266
+ ------------------------- | --------------------- | ------------------ | -----------
267
+ :validate | `true`/`false` | `true` | Whether or not to run `ActiveRecord` validations (uniqueness skipped). This option will always be true when using `import!`.
268
+ :validate_uniqueness | `true`/`false` | `false` | Whether or not to run uniqueness validations, has potential pitfalls, use with caution (requires `>= v0.27.0`).
269
+ :validate_with_context | `Symbol` |`:create`/`:update` | Allows passing an ActiveModel validation context for each model. Default is `:create` for new records and `:update` for existing ones.
270
+ :track_validation_failures| `true`/`false` | `false` | When this is set to true, `failed_instances` will be an array of arrays, with each inner array having the form `[:index_in_dataset, :object_with_errors]`
271
+ :on_duplicate_key_ignore | `true`/`false` | `false` | Allows skipping records with duplicate keys. See [here](https://github.com/zdennis/activerecord-import/#duplicate-key-ignore) for more details.
272
+ :ignore | `true`/`false` | `false` | Alias for :on_duplicate_key_ignore.
273
+ :on_duplicate_key_update | :all, `Array`, `Hash` | N/A | Allows upsert logic to be used. See [here](https://github.com/zdennis/activerecord-import/#duplicate-key-update) for more details.
274
+ :synchronize | `Array` | N/A | An array of ActiveRecord instances. This synchronizes existing instances in memory with updates from the import.
275
+ :timestamps | `true`/`false` | `true` | Enables/disables timestamps on imported records.
276
+ :recursive | `true`/`false` | `false` | Imports has_many/has_one associations (PostgreSQL only).
277
+ :batch_size | `Integer` | total # of records | Max number of records to insert per import
278
+ :raise_error | `true`/`false` | `false` | Raises an exception at the first invalid record. This means there will not be a result object returned. The `import!` method is a shortcut for this.
279
+ :all_or_none | `true`/`false` | `false` | Will not import any records if there is a record with validation errors.
264
280
 
265
281
  #### Duplicate Key Ignore
266
282
 
@@ -526,7 +542,7 @@ require 'activerecord-import'
526
542
  ### Load Path Setup
527
543
  To understand how rubygems loads code you can reference the following:
528
544
 
529
- http://guides.rubygems.org/patterns/#loading_code
545
+ http://guides.rubygems.org/patterns/#loading-code
530
546
 
531
547
  And an example of how active_record dynamically load adapters:
532
548
 
@@ -613,7 +629,7 @@ You can triage issues which may include reproducing bug reports or asking for vi
613
629
 
614
630
  # License
615
631
 
616
- This is licensed under the ruby license.
632
+ This is licensed under the MIT license.
617
633
 
618
634
  # Author
619
635
 
@@ -6,8 +6,8 @@ Gem::Specification.new do |gem|
6
6
  gem.email = ["zach.dennis@gmail.com"]
7
7
  gem.summary = "Bulk insert extension for ActiveRecord"
8
8
  gem.description = "A library for bulk inserting data using ActiveRecord."
9
- gem.homepage = "http://github.com/zdennis/activerecord-import"
10
- gem.license = "Ruby"
9
+ gem.homepage = "https://github.com/zdennis/activerecord-import"
10
+ gem.license = "MIT"
11
11
 
12
12
  gem.files = `git ls-files`.split($\)
13
13
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib"]
17
17
  gem.version = ActiveRecord::Import::VERSION
18
18
 
19
- gem.required_ruby_version = ">= 1.9.2"
19
+ gem.required_ruby_version = ">= 2.0.0"
20
20
 
21
21
  gem.add_runtime_dependency "activerecord", ">= 3.2"
22
22
  gem.add_development_dependency "rake"
data/gemfiles/6.0.gemfile CHANGED
@@ -1 +1,2 @@
1
1
  gem 'activerecord', '~> 6.0.0'
2
+ gem 'composite_primary_keys', '~> 12.0'
data/gemfiles/6.1.gemfile CHANGED
@@ -1 +1 @@
1
- gem 'activerecord', '~> 6.1.0.alpha', github: "rails/rails"
1
+ gem 'activerecord', '~> 6.1.0'
@@ -66,7 +66,7 @@ module ActiveRecord::Import::AbstractAdapter
66
66
  end
67
67
 
68
68
  def supports_on_duplicate_key_update?
69
- true
69
+ false
70
70
  end
71
71
  end
72
72
  end
@@ -1,5 +1,6 @@
1
1
  module ActiveRecord::Import::MysqlAdapter
2
2
  include ActiveRecord::Import::ImportSupport
3
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
3
4
 
4
5
  NO_MAX_PACKET = 0
5
6
  QUERY_OVERHEAD = 8 # This was shown to be true for MySQL, but it's not clear where the overhead is from.
@@ -55,9 +56,9 @@ module ActiveRecord::Import::MysqlAdapter
55
56
  # in a single packet
56
57
  def max_allowed_packet # :nodoc:
57
58
  @max_allowed_packet ||= begin
58
- result = execute( "SHOW VARIABLES like 'max_allowed_packet'" )
59
+ result = execute( "SELECT @@max_allowed_packet" )
59
60
  # original Mysql gem responds to #fetch_row while Mysql2 responds to #first
60
- val = result.respond_to?(:fetch_row) ? result.fetch_row[1] : result.first[1]
61
+ val = result.respond_to?(:fetch_row) ? result.fetch_row[0] : result.first[0]
61
62
  val.to_i
62
63
  end
63
64
  end
@@ -1,5 +1,6 @@
1
1
  module ActiveRecord::Import::PostgreSQLAdapter
2
2
  include ActiveRecord::Import::ImportSupport
3
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
3
4
 
4
5
  MIN_VERSION_FOR_UPSERT = 90_500
5
6
 
@@ -27,7 +28,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
27
28
  else
28
29
  select_values( sql2insert, *args )
29
30
  end
30
- query_cache.clear if query_cache_enabled
31
+ clear_query_cache if query_cache_enabled
31
32
  end
32
33
 
33
34
  if options[:returning].blank?
@@ -1,5 +1,6 @@
1
1
  module ActiveRecord::Import::SQLite3Adapter
2
2
  include ActiveRecord::Import::ImportSupport
3
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
3
4
 
4
5
  MIN_VERSION_FOR_IMPORT = "3.7.11".freeze
5
6
  MIN_VERSION_FOR_UPSERT = "3.24.0".freeze
@@ -11,6 +11,12 @@ module ActiveRecord::Import #:nodoc:
11
11
  end
12
12
  end
13
13
 
14
+ module OnDuplicateKeyUpdateSupport #:nodoc:
15
+ def supports_on_duplicate_key_update? #:nodoc:
16
+ true
17
+ end
18
+ end
19
+
14
20
  class MissingColumnError < StandardError
15
21
  def initialize(name, index)
16
22
  super "Missing column for value <#{name}> at index #{index}"
@@ -241,8 +247,9 @@ end
241
247
 
242
248
  module ActiveRecord::Import::Connection
243
249
  def establish_connection(args = nil)
244
- super(args)
250
+ conn = super(args)
245
251
  ActiveRecord::Import.load_from_connection_pool connection_pool
252
+ conn
246
253
  end
247
254
  end
248
255
 
@@ -540,7 +547,7 @@ class ActiveRecord::Base
540
547
  alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
541
548
 
542
549
  def import_helper( *args )
543
- options = { validate: true, timestamps: true }
550
+ options = { validate: true, timestamps: true, track_validation_failures: false }
544
551
  options.merge!( args.pop ) if args.last.is_a? Hash
545
552
  # making sure that current model's primary key is used
546
553
  options[:primary_key] = primary_key
@@ -575,7 +582,7 @@ class ActiveRecord::Base
575
582
  if respond_to?(:timestamp_attributes_for_update, true)
576
583
  send(:timestamp_attributes_for_update).map(&:to_sym)
577
584
  else
578
- new.send(:timestamp_attributes_for_update_in_model)
585
+ allocate.send(:timestamp_attributes_for_update_in_model)
579
586
  end
580
587
  end
581
588
 
@@ -696,14 +703,18 @@ class ActiveRecord::Base
696
703
  # keep track of the instance and the position it is currently at. if this fails
697
704
  # validation we'll use the index to remove it from the array_of_attributes
698
705
  arr.each_with_index do |hsh, i|
699
- model = new
700
- hsh.each_pair { |k, v| model[k] = v }
706
+ # utilize block initializer syntax to prevent failure when 'mass_assignment_sanitizer = :strict'
707
+ model = new do |m|
708
+ hsh.each_pair { |k, v| m[k] = v }
709
+ end
710
+
701
711
  next if validator.valid_model?(model)
702
712
  raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
713
+
703
714
  array_of_attributes[i] = nil
704
715
  failure = model.dup
705
716
  failure.errors.send(:initialize_dup, model.errors)
706
- failed_instances << failure
717
+ failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
707
718
  end
708
719
  array_of_attributes.compact!
709
720
  end
@@ -794,17 +805,29 @@ class ActiveRecord::Base
794
805
  if supports_import?
795
806
  # generate the sql
796
807
  post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
808
+ import_size = values_sql.size
809
+
810
+ batch_size = options[:batch_size] || import_size
811
+ run_proc = options[:batch_size].to_i.positive? && options[:batch_progress].respond_to?( :call )
812
+ progress_proc = options[:batch_progress]
813
+ current_batch = 0
814
+ batches = (import_size / batch_size.to_f).ceil
797
815
 
798
- batch_size = options[:batch_size] || values_sql.size
799
816
  values_sql.each_slice(batch_size) do |batch_values|
817
+ batch_started_at = Time.now.to_i
818
+
800
819
  # perform the inserts
801
820
  result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
802
821
  batch_values,
803
822
  options,
804
- "#{model_name} Create Many Without Validations Or Callbacks" )
823
+ "#{model_name} Create Many" )
824
+
805
825
  number_inserted += result.num_inserts
806
826
  ids += result.ids
807
827
  results += result.results
828
+ current_batch += 1
829
+
830
+ progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
808
831
  end
809
832
  else
810
833
  transaction(requires_new: true) do
@@ -835,6 +858,19 @@ class ActiveRecord::Base
835
858
  end
836
859
  end
837
860
 
861
+ deserialize_value = lambda do |column, value|
862
+ column = columns_hash[column]
863
+ return value unless column
864
+ if respond_to?(:type_caster)
865
+ type = type_for_attribute(column.name)
866
+ type.deserialize(value)
867
+ elsif column.respond_to?(:type_cast_from_database)
868
+ column.type_cast_from_database(value)
869
+ else
870
+ value
871
+ end
872
+ end
873
+
838
874
  if models.size == import_result.results.size
839
875
  columns = Array(options[:returning])
840
876
  single_column = "#{columns.first}=" if columns.size == 1
@@ -842,10 +878,12 @@ class ActiveRecord::Base
842
878
  model = models[index]
843
879
 
844
880
  if single_column
845
- model.send(single_column, result)
881
+ val = deserialize_value.call(columns.first, result)
882
+ model.send(single_column, val)
846
883
  else
847
884
  columns.each_with_index do |column, col_index|
848
- model.send("#{column}=", result[col_index])
885
+ val = deserialize_value.call(column, result[col_index])
886
+ model.send("#{column}=", val)
849
887
  end
850
888
  end
851
889
  end
@@ -866,10 +904,12 @@ class ActiveRecord::Base
866
904
 
867
905
  # Sync belongs_to association ids with foreign key field
868
906
  def load_association_ids(model)
907
+ changed_columns = model.changed
869
908
  association_reflections = model.class.reflect_on_all_associations(:belongs_to)
870
909
  association_reflections.each do |association_reflection|
871
910
  column_name = association_reflection.foreign_key
872
911
  next if association_reflection.options[:polymorphic]
912
+ next if changed_columns.include?(column_name)
873
913
  association = model.association(association_reflection.name)
874
914
  association = association.target
875
915
  next if association.blank? || model.public_send(column_name).present?
@@ -949,7 +989,7 @@ class ActiveRecord::Base
949
989
  elsif column
950
990
  if respond_to?(:type_caster) # Rails 5.0 and higher
951
991
  type = type_for_attribute(column.name)
952
- val = type.type == :boolean ? type.cast(val) : type.serialize(val)
992
+ val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
953
993
  connection_memo.quote(val)
954
994
  elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
955
995
  connection_memo.quote(column.type_cast_from_user(val), column)
@@ -977,7 +1017,7 @@ class ActiveRecord::Base
977
1017
  timestamp_columns[:create] = timestamp_attributes_for_create_in_model
978
1018
  timestamp_columns[:update] = timestamp_attributes_for_update_in_model
979
1019
  else
980
- instance = new
1020
+ instance = allocate
981
1021
  timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
982
1022
  timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
983
1023
  end
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/array'
2
+
1
3
  module ActiveRecord::Import
2
4
  class ValueSetTooLargeError < StandardError
3
5
  attr_reader :size
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Import
3
- VERSION = "1.0.4".freeze
3
+ VERSION = "1.1.0".freeze
4
4
  end
5
5
  end
data/test/import_test.rb CHANGED
@@ -252,6 +252,16 @@ describe "#import" do
252
252
  end
253
253
  end
254
254
 
255
+ it "should index the failed instances by their poistion in the set if `track_failures` is true" do
256
+ index_offset = valid_values.length
257
+ results = Topic.import columns, valid_values + invalid_values, validate: true, track_validation_failures: true
258
+ assert_equal invalid_values.size, results.failed_instances.size
259
+ invalid_values.each_with_index do |value_set, index|
260
+ assert_equal index + index_offset, results.failed_instances[index].first
261
+ assert_equal value_set.first, results.failed_instances[index].last.title
262
+ end
263
+ end
264
+
255
265
  it "should set ids in valid models if adapter supports setting primary key of imported objects" do
256
266
  if ActiveRecord::Base.supports_setting_primary_key_of_imported_objects?
257
267
  Topic.import (invalid_models + valid_models), validate: true
@@ -395,6 +405,15 @@ describe "#import" do
395
405
  assert_equal 3, result.num_inserts if Topic.supports_import?
396
406
  end
397
407
  end
408
+
409
+ it "should accept and call an optional callable to run after each batch" do
410
+ lambda_called = 0
411
+
412
+ my_proc = ->(_row_count, _batches, _batch, _duration) { lambda_called += 1 }
413
+ Topic.import Build(10, :topics), batch_size: 4, batch_progress: my_proc
414
+
415
+ assert_equal 3, lambda_called
416
+ end
398
417
  end
399
418
 
400
419
  context "with :synchronize option" do
@@ -900,4 +919,33 @@ describe "#import" do
900
919
  end
901
920
  end
902
921
  end
922
+ describe "importing model with after_initialize callback" do
923
+ let(:columns) { %w(name size) }
924
+ let(:valid_values) { [%w("Deer", "Small"), %w("Monkey", "Medium")] }
925
+ let(:invalid_values) do
926
+ [
927
+ { name: "giraffe", size: "Large" },
928
+ { size: "Medium" } # name is missing
929
+ ]
930
+ end
931
+ context "with validation checks turned off" do
932
+ it "should import valid data" do
933
+ Animal.import(columns, valid_values, validate: false)
934
+ assert_equal 2, Animal.count
935
+ end
936
+ it "should raise ArgumentError" do
937
+ assert_raise(ArgumentError) { Animal.import(invalid_values, validate: false) }
938
+ end
939
+ end
940
+
941
+ context "with validation checks turned on" do
942
+ it "should import valid data" do
943
+ Animal.import(columns, valid_values, validate: true)
944
+ assert_equal 2, Animal.count
945
+ end
946
+ it "should raise ArgumentError" do
947
+ assert_raise(ArgumentError) { Animal.import(invalid_values, validate: true) }
948
+ end
949
+ end
950
+ end
903
951
  end
@@ -0,0 +1,6 @@
1
+ class Animal < ActiveRecord::Base
2
+ after_initialize :validate_name_presence, if: :new_record?
3
+ def validate_name_presence
4
+ raise ArgumentError if name.nil?
5
+ end
6
+ end
@@ -16,6 +16,7 @@ ActiveRecord::Schema.define do
16
16
 
17
17
  create_table :vendors, id: :uuid, force: :cascade do |t|
18
18
  t.string :name, null: true
19
+ t.text :hours
19
20
  t.text :preferences
20
21
 
21
22
  if t.respond_to?(:json)
@@ -127,6 +127,15 @@ def should_support_postgresql_import_functionality
127
127
  end
128
128
  end
129
129
 
130
+ context "when a returning column is a serialized attribute" do
131
+ let(:vendor) { Vendor.new(hours: { monday: '8-5' }) }
132
+ let(:result) { Vendor.import([vendor], returning: %w(hours)) }
133
+
134
+ it "creates records" do
135
+ assert_difference("Vendor.count", +1) { result }
136
+ end
137
+ end
138
+
130
139
  context "when primary key and returning overlap" do
131
140
  let(:result) { Book.import(books, returning: %w(id title)) }
132
141
 
@@ -138,6 +138,15 @@ def should_support_recursive_import
138
138
  books.each do |book|
139
139
  assert_equal book.topic_id, second_new_topic.id
140
140
  end
141
+
142
+ books.each { |book| book.topic_id = nil }
143
+ assert_no_difference "Book.count", books.size do
144
+ Book.import books, validate: false, on_duplicate_key_update: [:topic_id]
145
+ end
146
+
147
+ books.each do |book|
148
+ assert_equal book.topic_id, nil
149
+ end
141
150
  end
142
151
 
143
152
  unless ENV["SKIP_COMPOSITE_PK"]
data/test/test_helper.rb CHANGED
@@ -48,7 +48,15 @@ adapter = ENV["ARE_DB"] || "sqlite3"
48
48
  FileUtils.mkdir_p 'log'
49
49
  ActiveRecord::Base.logger = Logger.new("log/test.log")
50
50
  ActiveRecord::Base.logger.level = Logger::DEBUG
51
- ActiveRecord::Base.configurations["test"] = YAML.load_file(test_dir.join("database.yml"))[adapter]
51
+
52
+ if ENV['AR_VERSION'].to_f >= 6.0
53
+ yaml_config = YAML.load_file(test_dir.join("database.yml"))[adapter]
54
+ config = ActiveRecord::DatabaseConfigurations::HashConfig.new("test", adapter, yaml_config)
55
+ ActiveRecord::Base.configurations.configurations << config
56
+ else
57
+ ActiveRecord::Base.configurations["test"] = YAML.load_file(test_dir.join("database.yml"))[adapter]
58
+ end
59
+
52
60
  ActiveRecord::Base.default_timezone = :utc
53
61
 
54
62
  require "activerecord-import"
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: 1.0.4
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Dennis
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-12-24 00:00:00.000000000 Z
11
+ date: 2021-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -67,7 +67,7 @@ files:
67
67
  - benchmarks/models/test_innodb.rb
68
68
  - benchmarks/models/test_memory.rb
69
69
  - benchmarks/models/test_myisam.rb
70
- - benchmarks/schema/mysql_schema.rb
70
+ - benchmarks/schema/mysql2_schema.rb
71
71
  - gemfiles/3.2.gemfile
72
72
  - gemfiles/4.0.gemfile
73
73
  - gemfiles/4.1.gemfile
@@ -121,6 +121,7 @@ files:
121
121
  - test/makara_postgis/import_test.rb
122
122
  - test/models/account.rb
123
123
  - test/models/alarm.rb
124
+ - test/models/animal.rb
124
125
  - test/models/bike_maker.rb
125
126
  - test/models/book.rb
126
127
  - test/models/car.rb
@@ -166,11 +167,11 @@ files:
166
167
  - test/travis/database.yml
167
168
  - test/value_sets_bytes_parser_test.rb
168
169
  - test/value_sets_records_parser_test.rb
169
- homepage: http://github.com/zdennis/activerecord-import
170
+ homepage: https://github.com/zdennis/activerecord-import
170
171
  licenses:
171
- - Ruby
172
+ - MIT
172
173
  metadata: {}
173
- post_install_message:
174
+ post_install_message:
174
175
  rdoc_options: []
175
176
  require_paths:
176
177
  - lib
@@ -178,15 +179,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
178
179
  requirements:
179
180
  - - ">="
180
181
  - !ruby/object:Gem::Version
181
- version: 1.9.2
182
+ version: 2.0.0
182
183
  required_rubygems_version: !ruby/object:Gem::Requirement
183
184
  requirements:
184
185
  - - ">="
185
186
  - !ruby/object:Gem::Version
186
187
  version: '0'
187
188
  requirements: []
188
- rubygems_version: 3.0.6
189
- signing_key:
189
+ rubygems_version: 3.0.8
190
+ signing_key:
190
191
  specification_version: 4
191
192
  summary: Bulk insert extension for ActiveRecord
192
193
  test_files:
@@ -211,6 +212,7 @@ test_files:
211
212
  - test/makara_postgis/import_test.rb
212
213
  - test/models/account.rb
213
214
  - test/models/alarm.rb
215
+ - test/models/animal.rb
214
216
  - test/models/bike_maker.rb
215
217
  - test/models/book.rb
216
218
  - test/models/car.rb