activerecord-import 1.0.4 → 1.1.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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +6 -4
- data/CHANGELOG.md +46 -2
- data/Gemfile +5 -1
- data/LICENSE +21 -56
- data/README.markdown +33 -17
- data/activerecord-import.gemspec +3 -3
- data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +0 -0
- data/gemfiles/6.0.gemfile +1 -0
- data/gemfiles/6.1.gemfile +1 -1
- data/lib/activerecord-import/adapters/abstract_adapter.rb +1 -1
- data/lib/activerecord-import/adapters/mysql_adapter.rb +3 -2
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +2 -1
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +1 -0
- data/lib/activerecord-import/import.rb +52 -12
- data/lib/activerecord-import/value_sets_parser.rb +2 -0
- data/lib/activerecord-import/version.rb +1 -1
- data/test/import_test.rb +48 -0
- data/test/models/animal.rb +6 -0
- data/test/schema/postgresql_schema.rb +1 -0
- data/test/support/postgresql/import_examples.rb +9 -0
- data/test/support/shared_examples/recursive_import.rb +9 -0
- data/test/test_helper.rb +9 -1
- metadata +12 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c61743fafaad0de04ccf2c6bff4439fe66aed03e951e5c42e4f7926cf8dac39
|
4
|
+
data.tar.gz: c58d8992957e546b73bb7f7b90eaf2a741f007d2109a16db0e3b3d71b376e1da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98638b63235eae1c16f27d3be932b8ce4e8f236b24350766792a4fc563d199cb1d048d25efa5d22abf3a21017fa07b9474c3168a4be6c1ba5aaa6e0843d584a3
|
7
|
+
data.tar.gz: 5c62bed2684f1b6d60d7e26b8ad13c6b7173a64835eeca9474082122425ab1f340f9063868224eda2539878210887701022e1ff6e6a37593872682bf90768d3a
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
language: ruby
|
2
2
|
cache: bundler
|
3
3
|
rvm:
|
4
|
-
- 2.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: "
|
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-
|
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", "~>
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
251
|
-
|
252
|
-
:validate
|
253
|
-
:validate_uniqueness
|
254
|
-
:validate_with_context
|
255
|
-
:
|
256
|
-
:
|
257
|
-
:
|
258
|
-
:
|
259
|
-
:
|
260
|
-
:
|
261
|
-
:
|
262
|
-
:
|
263
|
-
:
|
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/#
|
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
|
632
|
+
This is licensed under the MIT license.
|
617
633
|
|
618
634
|
# Author
|
619
635
|
|
data/activerecord-import.gemspec
CHANGED
@@ -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 = "
|
10
|
-
gem.license = "
|
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 = ">=
|
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"
|
File without changes
|
data/gemfiles/6.0.gemfile
CHANGED
data/gemfiles/6.1.gemfile
CHANGED
@@ -1 +1 @@
|
|
1
|
-
gem 'activerecord', '~> 6.1.0
|
1
|
+
gem 'activerecord', '~> 6.1.0'
|
@@ -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( "
|
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[
|
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
|
-
|
31
|
+
clear_query_cache if query_cache_enabled
|
31
32
|
end
|
32
33
|
|
33
34
|
if options[:returning].blank?
|
@@ -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
|
-
|
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
|
-
|
700
|
-
|
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
|
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
|
-
|
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
|
-
|
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 =
|
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
|
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
|
@@ -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
|
-
|
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
|
+
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:
|
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/
|
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:
|
170
|
+
homepage: https://github.com/zdennis/activerecord-import
|
170
171
|
licenses:
|
171
|
-
-
|
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:
|
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.
|
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
|