activerecord-import 0.27.0 → 0.28.1

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: 00e3b31f9b8fbe29c0ed8b6a58b6a1afeb0a3f67
4
- data.tar.gz: b854ffc00c14c4cfc64a67819a367b35941cdb80
3
+ metadata.gz: dbd7badbb2e98c6725ddaf5edbb713c4f79bfc01
4
+ data.tar.gz: 0912df3959070820db94063d0172a43f4b027a9f
5
5
  SHA512:
6
- metadata.gz: 164ff30dd1c1f19becb91dd2e7c0d3c9d022489b5065a337743b5eb84a497e58dd6ddaee43a401127cf06c60f3320d4501c962dd91885127b86acea4542315d9
7
- data.tar.gz: 0dad63cbd8a90ee1f31483796a470088cbd4f35eca9579f707e394500ab6e2ab9524150e49d3a2bee60b79b367c8974855dd5af413c33b326ed4796d0852e118
6
+ metadata.gz: 22fb70781c58df1795c41e5b8e954b8a89e6c85be0dbf33f2c2fb9265847778f99f414cbd7fe9ea945d6aa43f89d7557ea41541f938fa357a7bbcb2c0e6114df
7
+ data.tar.gz: b480446464f626518ac128b2e79d4e4dbb2b50e8e52d6781812a0ee1594e5328430214ad03be9590c09dcfd07661ae54130b53ccde1b9b7d2131d2f40c80a633
@@ -1,3 +1,22 @@
1
+ ## Changes in 0.28.1
2
+
3
+ ### Fixes
4
+
5
+ * Fix issue where ActiveRecord presence validations were being mutated.
6
+ Limited custom presence validation to bulk imports.
7
+
8
+ ## Changes in 0.28.0
9
+
10
+ ### New Features
11
+
12
+ * Allow updated timestamps to be manually set.Thanks to @Rob117, @jkowens via \#570.
13
+
14
+ ### Fixes
15
+
16
+ * Fix validating presence of belongs_to associations. Existence
17
+ of the parent record is not validated, but the foreign key field
18
+ cannot be empty. Thanks to @Rob117, @jkowens via \#575.
19
+
1
20
  ## Changes in 0.27.0
2
21
 
3
22
  ### New Features
@@ -21,12 +21,401 @@ and then the reviews:
21
21
  That would be about 4M SQL insert statements vs 3, which results in vastly improved performance. In our case, it converted
22
22
  an 18 hour batch process to <2 hrs.
23
23
 
24
+ The gem provides the following high-level features:
25
+
26
+ * activerecord-import can work with raw columns and arrays of values (fastest)
27
+ * activerecord-import works with model objects (faster)
28
+ * activerecord-import can perform validations (fast)
29
+ * activerecord-import can perform on duplicate key updates (requires MySQL or Postgres 9.5+)
30
+
24
31
  ## Table of Contents
25
32
 
33
+ * [Examples](#examples)
34
+ * [Introduction](#introduction)
35
+ * [Columns and Arrays](#columns-and-arrays)
36
+ * [Hashes](#hashes)
37
+ * [ActiveRecord Models](#activerecord-models)
38
+ * [Batching](#batching)
39
+ * [Recursive](#recursive)
40
+ * [Options](#options)
41
+ * [Duplicate Key Ignore](#duplicate-key-ignore)
42
+ * [Duplicate Key Update](#duplicate-key-update)
43
+ * [Return Info](#return-info)
44
+ * [Counter Cache](#counter-cache)
45
+ * [ActiveRecord Timestamps](#activerecord-timestamps)
26
46
  * [Callbacks](#callbacks)
47
+ * [Supported Adapters](#supported-adapters)
27
48
  * [Additional Adapters](#additional-adapters)
49
+ * [Requiring](#requiring)
50
+ * [Autoloading via Bundler](#autoloading-via-bundler)
51
+ * [Manually Loading](#manually-loading)
28
52
  * [Load Path Setup](#load-path-setup)
53
+ * [Conflicts With Other Gems](#conflicts-with-other-gems)
29
54
  * [More Information](#more-information)
55
+ * [Contributing](#contributing)
56
+ * [Running Tests](#running-tests)
57
+
58
+ ### Examples
59
+
60
+ #### Introduction
61
+
62
+ Without `activerecord-import`, you'd write something like this:
63
+
64
+ ```ruby
65
+ 10.times do |i|
66
+ Book.create! :name => "book #{i}"
67
+ end
68
+ ```
69
+
70
+ This would end up making 10 SQL calls. YUCK! With `activerecord-import`, you can instead do this:
71
+
72
+ ```ruby
73
+ books = []
74
+ 10.times do |i|
75
+ books << Book.new(:name => "book #{i}")
76
+ end
77
+ Book.import books # or use import!
78
+ ```
79
+
80
+ and only have 1 SQL call. Much better!
81
+
82
+ #### Columns and Arrays
83
+
84
+ The `import` method can take an array of column names (string or symbols) and an array of arrays. Each child array represents an individual record and its list of values in the same order as the columns. This is the fastest import mechanism and also the most primitive.
85
+
86
+ ```ruby
87
+ columns = [ :title, :author ]
88
+ values = [ ['Book1', 'FooManChu'], ['Book2', 'Bob Jones'] ]
89
+
90
+ # Importing without model validations
91
+ Book.import columns, values, :validate => false
92
+
93
+ # Import with model validations
94
+ Book.import columns, values, :validate => true
95
+
96
+ # when not specified :validate defaults to true
97
+ Book.import columns, values
98
+ ```
99
+
100
+ #### Hashes
101
+
102
+ The `import` method can take an array of hashes. The keys map to the column names in the database.
103
+
104
+ ```ruby
105
+ values = [{ title: 'Book1', author: 'FooManChu' }, { title: 'Book2', author: 'Bob Jones'}]
106
+
107
+ # Importing without model validations
108
+ Book.import values, validate: false
109
+
110
+ # Import with model validations
111
+ Book.import values, validate: true
112
+
113
+ # when not specified :validate defaults to true
114
+ Book.import values
115
+ ```
116
+ h2. Import Using Hashes and Explicit Column Names
117
+
118
+ The `import` method can take an array of column names and an array of hash objects. The column names are used to determine what fields of data should be imported. The following example will only import books with the `title` field:
119
+
120
+ ```ruby
121
+ books = [
122
+ { title: "Book 1", author: "FooManChu" },
123
+ { title: "Book 2", author: "Bob Jones" }
124
+ ]
125
+ columns = [ :title ]
126
+
127
+ # without validations
128
+ Book.import columns, books, validate: false
129
+
130
+ # with validations
131
+ Book.import columns, books, validate: true
132
+
133
+ # when not specified :validate defaults to true
134
+ Book.import columns, books
135
+
136
+ # result in table books
137
+ # title | author
138
+ #--------|--------
139
+ # Book 1 | NULL
140
+ # Book 2 | NULL
141
+
142
+ ```
143
+
144
+ Using hashes will only work if the columns are consistent in every hash of the array. If this does not hold, an exception will be raised. There are two workarounds: use the array to instantiate an array of ActiveRecord objects and then pass that into `import` or divide the array into multiple ones with consistent columns and import each one separately.
145
+
146
+ See https://github.com/zdennis/activerecord-import/issues/507 for discussion.
147
+
148
+ ```ruby
149
+ arr = [
150
+ { bar: 'abc' },
151
+ { baz: 'xyz' },
152
+ { bar: '123', baz: '456' }
153
+ ]
154
+
155
+ # An exception will be raised
156
+ Foo.import arr
157
+
158
+ # better
159
+ arr.map! { |args| Foo.new(args) }
160
+ Foo.import arr
161
+
162
+ # better
163
+ arr.group_by(&:keys).each_value do |v|
164
+ Foo.import v
165
+ end
166
+ ```
167
+
168
+ #### ActiveRecord Models
169
+
170
+ The `import` method can take an array of models. The attributes will be pulled off from each model by looking at the columns available on the model.
171
+
172
+ ```ruby
173
+ books = [
174
+ Book.new(:title => "Book 1", :author => "FooManChu"),
175
+ Book.new(:title => "Book 2", :author => "Bob Jones")
176
+ ]
177
+
178
+ # without validations
179
+ Book.import books, :validate => false
180
+
181
+ # with validations
182
+ Book.import books, :validate => true
183
+
184
+ # when not specified :validate defaults to true
185
+ Book.import books
186
+ ```
187
+
188
+ The `import` method can take an array of column names and an array of models. The column names are used to determine what fields of data should be imported. The following example will only import books with the `title` field:
189
+
190
+ ```ruby
191
+ books = [
192
+ Book.new(:title => "Book 1", :author => "FooManChu"),
193
+ Book.new(:title => "Book 2", :author => "Bob Jones")
194
+ ]
195
+ columns = [ :title ]
196
+
197
+ # without validations
198
+ Book.import columns, books, :validate => false
199
+
200
+ # with validations
201
+ Book.import columns, books, :validate => true
202
+
203
+ # when not specified :validate defaults to true
204
+ Book.import columns, books
205
+
206
+ # result in table books
207
+ # title | author
208
+ #--------|--------
209
+ # Book 1 | NULL
210
+ # Book 2 | NULL
211
+
212
+ ```
213
+
214
+ #### Batching
215
+
216
+ The `import` method can take a `batch_size` option to control the number of rows to insert per INSERT statement. The default is the total number of records being inserted so there is a single INSERT statement.
217
+
218
+ ```ruby
219
+ books = [
220
+ Book.new(:title => "Book 1", :author => "FooManChu"),
221
+ Book.new(:title => "Book 2", :author => "Bob Jones"),
222
+ Book.new(:title => "Book 1", :author => "John Doe"),
223
+ Book.new(:title => "Book 2", :author => "Richard Wright")
224
+ ]
225
+ columns = [ :title ]
226
+
227
+ # 2 INSERT statements for 4 records
228
+ Book.import columns, books, :batch_size => 2
229
+ ```
230
+
231
+ #### Recursive
232
+
233
+ NOTE: This only works with PostgreSQL.
234
+
235
+ Assume that Books <code>has_many</code> Reviews.
236
+
237
+ ```ruby
238
+ books = []
239
+ 10.times do |i|
240
+ book = Book.new(:name => "book #{i}")
241
+ book.reviews.build(:title => "Excellent")
242
+ books << book
243
+ end
244
+ Book.import books, recursive: true
245
+ ```
246
+
247
+ ### Options
248
+
249
+ Key | Options | Default | Description
250
+ ----------------------- | --------------------- | ------------------ | -----------
251
+ :validate | `true`/`false` | `true` | Whether or not to run `ActiveRecord` validations (uniqueness skipped).
252
+ :validate_uniqueness | `true`/`false` | `false` | Whether or not to run uniqueness validations, has potential pitfalls, use with caution (requires `>= v0.27.0`).
253
+ :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.
254
+ :ignore | `true`/`false` | `false` | Alias for :on_duplicate_key_ignore.
255
+ :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.
256
+ :synchronize | `Array` | N/A | An array of ActiveRecord instances. This synchronizes existing instances in memory with updates from the import.
257
+ :timestamps | `true`/`false` | `true` | Enables/disables timestamps on imported records.
258
+ :recursive | `true`/`false` | `false` | Imports has_many/has_one associations (PostgreSQL only).
259
+ :batch_size | `Integer` | total # of records | Max number of records to insert per import
260
+ :raise_error | `true`/`false` | `false` | Throws an exception if there are invalid records. `import!` is a shortcut for this.
261
+
262
+
263
+ #### Duplicate Key Ignore
264
+
265
+ [MySQL](http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html), [SQLite](https://www.sqlite.org/lang_insert.html), and [PostgreSQL](https://www.postgresql.org/docs/current/static/sql-insert.html#SQL-ON-CONFLICT) (9.5+) support `on_duplicate_key_ignore` which allows you to skip records if a primary or unique key constraint is violated.
266
+
267
+ For Postgres 9.5+ it adds `ON CONFLICT DO NOTHING`, for MySQL it uses `INSERT IGNORE`, and for SQLite it uses `INSERT OR IGNORE`. Cannot be enabled on a recursive import. For database adapters that normally support setting primary keys on imported objects, this option prevents that from occurring.
268
+
269
+ ```ruby
270
+ book = Book.create! title: "Book1", author: "FooManChu"
271
+ book.title = "Updated Book Title"
272
+ book.author = "Bob Barker"
273
+
274
+ Book.import [book], on_duplicate_key_ignore: true
275
+
276
+ book.reload.title # => "Book1" (stayed the same)
277
+ book.reload.author # => "FooManChu" (stayed the same)
278
+ ```
279
+
280
+ The option `:on_duplicate_key_ignore` is bypassed when `:recursive` is enabled for [PostgreSQL imports](https://github.com/zdennis/activerecord-import/wiki#recursive-example-postgresql-only).
281
+
282
+ #### Duplicate Key Update
283
+
284
+ MySQL, PostgreSQL (9.5+), and SQLite (3.24.0+) support `on duplicate key update` (also known as "upsert") which allows you to specify fields whose values should be updated if a primary or unique key constraint is violated.
285
+
286
+ One big difference between MySQL and PostgreSQL support is that MySQL will handle any conflict that happens, but PostgreSQL requires that you specify which columns the conflict would occur over. SQLite models its upsert support after PostgreSQL.
287
+
288
+ This will use MySQL's `ON DUPLICATE KEY UPDATE` or Postgres/SQLite `ON CONFLICT DO UPDATE` to do upsert.
289
+
290
+ Basic Update
291
+
292
+ ```ruby
293
+ book = Book.create! title: "Book1", author: "FooManChu"
294
+ book.title = "Updated Book Title"
295
+ book.author = "Bob Barker"
296
+
297
+ # MySQL version
298
+ Book.import [book], on_duplicate_key_update: [:title]
299
+
300
+ # PostgreSQL version
301
+ Book.import [book], on_duplicate_key_update: {conflict_target: [:id], columns: [:title]}
302
+
303
+ # PostgreSQL shorthand version (conflict target must be primary key)
304
+ Book.import [book], on_duplicate_key_update: [:title]
305
+
306
+ book.reload.title # => "Updated Book Title" (changed)
307
+ book.reload.author # => "FooManChu" (stayed the same)
308
+ ```
309
+
310
+ Using the value from another column
311
+
312
+ ```ruby
313
+ book = Book.create! title: "Book1", author: "FooManChu"
314
+ book.title = "Updated Book Title"
315
+
316
+ # MySQL version
317
+ Book.import [book], on_duplicate_key_update: {author: :title}
318
+
319
+ # PostgreSQL version (no shorthand version)
320
+ Book.import [book], on_duplicate_key_update: {
321
+ conflict_target: [:id], columns: {author: :title}
322
+ }
323
+
324
+ book.reload.title # => "Book1" (stayed the same)
325
+ book.reload.author # => "Updated Book Title" (changed)
326
+ ```
327
+
328
+ Using Custom SQL
329
+
330
+ ```ruby
331
+ book = Book.create! title: "Book1", author: "FooManChu"
332
+ book.author = "Bob Barker"
333
+
334
+ # MySQL version
335
+ Book.import [book], on_duplicate_key_update: "author = values(author)"
336
+
337
+ # PostgreSQL version
338
+ Book.import [book], on_duplicate_key_update: {
339
+ conflict_target: [:id], columns: "author = excluded.author"
340
+ }
341
+
342
+ # PostgreSQL shorthand version (conflict target must be primary key)
343
+ Book.import [book], on_duplicate_key_update: "author = excluded.author"
344
+
345
+ book.reload.title # => "Book1" (stayed the same)
346
+ book.reload.author # => "Bob Barker" (changed)
347
+ ```
348
+
349
+ PostgreSQL Using constraints
350
+
351
+ ```ruby
352
+ book = Book.create! title: "Book1", author: "FooManChu", edition: 3, published_at: nil
353
+ book.published_at = Time.now
354
+
355
+ # in migration
356
+ execute <<-SQL
357
+ ALTER TABLE books
358
+ ADD CONSTRAINT for_upsert UNIQUE (title, author, edition);
359
+ SQL
360
+
361
+ # PostgreSQL version
362
+ Book.import [book], on_duplicate_key_update: {constraint_name: :for_upsert, columns: [:published_at]}
363
+
364
+
365
+ book.reload.title # => "Book1" (stayed the same)
366
+ book.reload.author # => "FooManChu" (stayed the same)
367
+ book.reload.edition # => 3 (stayed the same)
368
+ book.reload.published_at # => 2017-10-09 (changed)
369
+ ```
370
+
371
+ ```ruby
372
+ Book.import books, validate_uniqueness: true
373
+ ```
374
+
375
+ ### Return Info
376
+
377
+ The `import` method returns a `Result` object that responds to `failed_instances` and `num_inserts`. Additionally, for users of Postgres, there will be two arrays `ids` and `results` that can be accessed`.
378
+
379
+ ```ruby
380
+ articles = [
381
+ Article.new(author_id: 1, title: 'First Article', content: 'This is the first article'),
382
+ Article.new(author_id: 2, title: 'Second Article', content: ''),
383
+ Article.new(author_id: 3, content: '')
384
+ ]
385
+
386
+ demo = Article.import(articles), returning: :title # => #<struct ActiveRecord::Import::Result
387
+
388
+ demo.failed_instances
389
+ => [#<Article id: 3, author_id: 3, title: nil, content: "", created_at: nil, updated_at: nil>]
390
+
391
+ demo.num_inserts
392
+ => 1,
393
+
394
+ demo.ids
395
+ => ["1", "2"] # for Postgres
396
+ => [] # for other DBs
397
+
398
+ demo.results
399
+ => ["First Article", "Second Article"] # for Postgres
400
+ => [] for other DBs
401
+ ```
402
+
403
+ ### Counter Cache
404
+
405
+ When running `import`, `activerecord-import` does not automatically update counter cache columns. To update these columns, you will need to do one of the following:
406
+
407
+ * Provide values to the column as an argument on your object that is passed in.
408
+ * Manually update the column after the record has been imported.
409
+
410
+ ### ActiveRecord Timestamps
411
+
412
+ If you're familiar with ActiveRecord you're probably familiar with its timestamp columns: created_at, created_on, updated_at, updated_on, etc. When importing data the timestamp fields will continue to work as expected and each timestamp column will be set.
413
+
414
+ Should you wish to specify those columns, you may use the option `timestamps: false`.
415
+
416
+ However, it is also possible to set just `:created_at` in specific records. In this case despite using `timestamps: true`, `:created_at` will be updated only in records where that field is `nil`. Same rule applies for record associations when enabling the option `recursive: true`.
417
+
418
+ If you are using custom time zones, these will be respected when performing imports as well as long as `ActiveRecord::Base.default_timezone` is set, which for practically all Rails apps it is
30
419
 
31
420
  ### Callbacks
32
421
 
@@ -34,7 +423,7 @@ ActiveRecord callbacks related to [creating](http://guides.rubyonrails.org/activ
34
423
 
35
424
  If you do have a collection of in-memory ActiveRecord objects you can do something like this:
36
425
 
37
- ```
426
+ ```ruby
38
427
  books.each do |book|
39
428
  book.run_callbacks(:save) { false }
40
429
  book.run_callbacks(:create) { false }
@@ -46,7 +435,7 @@ This will run before_create and before_save callbacks on each item. The `false`
46
435
 
47
436
  If that is an issue, another possible approach is to loop through your models first to do validations and then only run callbacks on and import the valid models.
48
437
 
49
- ```
438
+ ```ruby
50
439
  valid_books = []
51
440
  invalid_books = []
52
441
 
@@ -66,6 +455,24 @@ end
66
455
  Book.import valid_books, validate: false
67
456
  ```
68
457
 
458
+ ### Supported Adapters
459
+
460
+ The following database adapters are currently supported:
461
+
462
+ * MySQL - supports core import functionality plus on duplicate key update support (included in activerecord-import 0.1.0 and higher)
463
+ * MySQL2 - supports core import functionality plus on duplicate key update support (included in activerecord-import 0.2.0 and higher)
464
+ * PostgreSQL - supports core import functionality (included in activerecord-import 0.1.0 and higher)
465
+ * SQLite3 - supports core import functionality (included in activerecord-import 0.1.0 and higher)
466
+ * Oracle - supports core import functionality through DML trigger (available as an external gem: [activerecord-import-oracle_enhanced](https://github.com/keeguon/activerecord-import-oracle_enhanced)
467
+ * SQL Server - supports core import functionality (available as an external gem: [activerecord-import-sqlserver](https://github.com/keeguon/activerecord-import-sqlserver)
468
+
469
+ If your adapter isn't listed here, please consider creating an external gem as described in the README to provide support. If you do, feel free to update this wiki to include a link to the new adapter's repository!
470
+
471
+ To test which features are supported by your adapter, use the following methods on a model class:
472
+ * `supports_import?(*args)`
473
+ * `supports_on_duplicate_key_update?`
474
+ * `supports_setting_primary_key_of_imported_objects?`
475
+
69
476
  ### Additional Adapters
70
477
 
71
478
  Additional adapters can be provided by gems external to activerecord-import by providing an adapter that matches the naming convention setup by activerecord-import (and subsequently activerecord) for dynamically loading adapters. This involves also providing a folder on the load path that follows the activerecord-import naming convention to allow activerecord-import to dynamically load the file.
@@ -73,17 +480,54 @@ Additional adapters can be provided by gems external to activerecord-import by p
73
480
  When `ActiveRecord::Import.require_adapter("fake_name")` is called the require will be:
74
481
 
75
482
  ```ruby
76
- require 'activerecord-import/active_record/adapters/fake_name_adapter'
483
+ require 'activerecord-import/active_record/adapters/fake_name_adapter'
77
484
  ```
78
485
 
79
486
  This allows an external gem to dynamically add an adapter without the need to add any file/code to the core activerecord-import gem.
80
487
 
488
+ ### Requiring
489
+
490
+ Note: These instructions will only work if you are using version 0.2.0 or higher.
491
+
492
+ #### Autoloading via Bundler
493
+
494
+ If you are using Rails or otherwise autoload your dependencies via Bundler, all you need to do add the gem to your `Gemfile` like so:
495
+
496
+ ```ruby
497
+ gem 'activerecord-import'
498
+ ```
499
+
500
+ #### Manually Loading
501
+
502
+ You may want to manually load activerecord-import for one reason or another. First, add the `require: false` argument like so:
503
+
504
+ ```ruby
505
+ gem 'activerecord-import', require: false
506
+ ```
507
+
508
+ This will allow you to load up activerecord-import in the file or files where you are using it and only load the parts you need.
509
+ If you are doing this within Rails and ActiveRecord has established a database connection (such as within a controller), you will need to do extra initialization work:
510
+
511
+ ```ruby
512
+ require 'activerecord-import/base'
513
+ # load the appropriate database adapter (postgresql, mysql2, sqlite3, etc)
514
+ require 'activerecord-import/active_record/adapters/postgresql_adapter'
515
+ ```
516
+
517
+ If your gem dependencies aren’t autoloaded, and your script will be establishing a database connection, then simply require activerecord-import after ActiveRecord has been loaded, i.e.:
518
+
519
+ ```ruby
520
+ require 'active_record'
521
+ require 'activerecord-import'
522
+ ```
523
+
81
524
  ### Load Path Setup
82
525
  To understand how rubygems loads code you can reference the following:
83
526
 
84
527
  http://guides.rubygems.org/patterns/#loading_code
85
528
 
86
529
  And an example of how active_record dynamically load adapters:
530
+
87
531
  https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/connection_specification.rb
88
532
 
89
533
  In summary, when a gem is loaded rubygems adds the `lib` folder of the gem to the global load path `$LOAD_PATH` so that all `require` lookups will not propagate through all of the folders on the load path. When a `require` is issued each folder on the `$LOAD_PATH` is checked for the file and/or folder referenced. This allows a gem (like activerecord-import) to define push the activerecord-import folder (or namespace) on the `$LOAD_PATH` and any adapters provided by activerecord-import will be found by rubygems when the require is issued.
@@ -105,12 +549,62 @@ activerecord-import-fake_name/
105
549
 
106
550
  When rubygems pushes the `lib` folder onto the load path a `require` will now find `activerecord-import/active_record/adapters/fake_name_adapter` as it runs through the lookup process for a ruby file under that path in `$LOAD_PATH`
107
551
 
552
+
553
+ ### Conflicts With Other Gems
554
+
555
+ `activerecord-import` adds the `.import` method onto `ActiveRecord::Base`. There are other gems, such as `elasticsearch-rails`, that do the same thing. In conflicts such as this, there is an aliased method named `.bulk_import` that can be used interchangeably.
556
+
557
+ If you are using the `apartment` gem, there is a weird triple interaction between that gem, `activerecord-import`, and `activerecord` involving caching of the `sequence_name` of a model. This can be worked around by explcitly setting this value within the model. For example:
558
+
559
+ ```ruby
560
+ class Post < ActiveRecord::Base
561
+ self.sequence_name = "posts_seq"
562
+ end
563
+ ```
564
+
565
+ Another way to work around the issue is to call `.reset_sequence_name` on the model. For example:
566
+
567
+ ```ruby
568
+ schemas.all.each do |schema|
569
+ Apartment::Tenant.switch! schema.name
570
+ ActiveRecord::Base.transaction do
571
+ Post.reset_sequence_name
572
+
573
+ Post.import posts
574
+ end
575
+ end
576
+ ```
577
+
578
+ See https://github.com/zdennis/activerecord-import/issues/233 for further discussion.
579
+
108
580
  ### More Information
109
581
 
110
582
  For more information on activerecord-import please see its wiki: https://github.com/zdennis/activerecord-import/wiki
111
583
 
112
584
  To document new information, please add to the README instead of the wiki. See https://github.com/zdennis/activerecord-import/issues/397 for discussion.
113
585
 
586
+ ### Contributing
587
+
588
+ #### Running Tests
589
+
590
+ The first thing you need to do is set up your database(s):
591
+
592
+ * copy `test/database.yml.sample` to `test/database.yml`
593
+ * modify `test/database.yml` for your database settings
594
+ * create databases as needed
595
+
596
+ After that, you can run the tests. They run against multiple tests and ActiveRecord versions.
597
+
598
+ This is one example of how to run the tests:
599
+
600
+ ```ruby
601
+ rm Gemfile.lock
602
+ AR_VERSION=4.2 bundle install
603
+ AR_VERSION=4.2 bundle exec rake test:postgresql test:sqlite3 test:mysql2
604
+ ```
605
+
606
+ Once you have pushed up your changes, you can find your CI results [here](https://travis-ci.org/zdennis/activerecord-import/).
607
+
114
608
  # License
115
609
 
116
610
  This is licensed under the ruby license.
@@ -131,3 +625,4 @@ Zach Dennis (zach.dennis@gmail.com)
131
625
  * Thibaud Guillaume-Gentil
132
626
  * Mark Van Holstyn
133
627
  * Victor Costan
628
+ * Dillon Welch
@@ -24,33 +24,57 @@ module ActiveRecord::Import #:nodoc:
24
24
  end
25
25
 
26
26
  class Validator
27
- def initialize(options = {})
27
+ def initialize(klass, options = {})
28
28
  @options = options
29
+ init_validations(klass)
30
+ end
31
+
32
+ def init_validations(klass)
33
+ @validate_callbacks = klass._validate_callbacks.dup
34
+
35
+ klass._validate_callbacks.each_with_index do |callback, i|
36
+ filter = callback.raw_filter
37
+
38
+ if filter.class.name =~ /Validations::PresenceValidator/
39
+ callback = callback.dup
40
+ filter = filter.dup
41
+ associations = klass.reflect_on_all_associations(:belongs_to)
42
+ attrs = filter.instance_variable_get(:@attributes).dup
43
+ associations.each do |assoc|
44
+ if (index = attrs.index(assoc.name))
45
+ key = assoc.foreign_key.to_sym
46
+ attrs[index] = key unless attrs.include?(key)
47
+ end
48
+ end
49
+ filter.instance_variable_set(:@attributes, attrs)
50
+ if @validate_callbacks.respond_to?(:chain, true)
51
+ @validate_callbacks.send(:chain).tap do |chain|
52
+ callback.instance_variable_set(:@filter, filter)
53
+ chain[i] = callback
54
+ end
55
+ else
56
+ callback.raw_filter = filter
57
+ callback.filter = callback.send(:_compile_filter, filter)
58
+ @validate_callbacks[i] = callback
59
+ end
60
+ elsif !@options[:validate_uniqueness] && filter.is_a?(ActiveRecord::Validations::UniquenessValidator)
61
+ @validate_callbacks.delete(callback)
62
+ end
63
+ end
29
64
  end
30
65
 
31
66
  def valid_model?(model)
32
67
  validation_context = @options[:validate_with_context]
33
68
  validation_context ||= (model.new_record? ? :create : :update)
34
-
35
69
  current_context = model.send(:validation_context)
70
+
36
71
  begin
37
72
  model.send(:validation_context=, validation_context)
38
73
  model.errors.clear
39
74
 
40
- validate_callbacks = model._validate_callbacks.dup
41
- associations = model.class.reflect_on_all_associations(:belongs_to).map(&:name)
42
-
43
- model._validate_callbacks.each do |callback|
44
- filter = callback.raw_filter
45
- if (!@options[:validate_uniqueness] && filter.is_a?(ActiveRecord::Validations::UniquenessValidator)) ||
46
- (defined?(ActiveRecord::Validations::PresenceValidator) && filter.is_a?(ActiveRecord::Validations::PresenceValidator) && associations.include?(filter.attributes.first))
47
- validate_callbacks.delete(callback)
48
- end
49
- end
50
-
51
75
  model.run_callbacks(:validation) do
52
76
  if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
53
- runner = validate_callbacks.compile
77
+ runner = @validate_callbacks.compile
54
78
  env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
55
79
  if runner.respond_to?(:call) # ActiveRecord < 5.1
56
80
  runner.call(env)
@@ -69,10 +93,10 @@ module ActiveRecord::Import #:nodoc:
69
93
  runner.invoke_before(env)
70
94
  runner.invoke_after(env)
71
95
  end
72
- elsif validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
73
- model.instance_eval validate_callbacks.compile
96
+ elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
97
+ model.instance_eval @validate_callbacks.compile
74
98
  else # ActiveRecord 3.x
75
- model.instance_eval validate_callbacks.compile(nil, model)
99
+ model.instance_eval @validate_callbacks.compile(nil, model)
76
100
  end
77
101
  end
78
102
 
@@ -520,7 +544,7 @@ class ActiveRecord::Base
520
544
  options[:locking_column] = locking_column if attribute_names.include?(locking_column)
521
545
 
522
546
  is_validating = options[:validate_with_context].present? ? true : options[:validate]
523
- validator = ActiveRecord::Import::Validator.new(options)
547
+ validator = ActiveRecord::Import::Validator.new(self, options)
524
548
 
525
549
  # assume array of model objects
526
550
  if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
@@ -553,6 +577,14 @@ class ActiveRecord::Base
553
577
  serialized_attributes
554
578
  end
555
579
 
580
+ update_attrs = if record_timestamps && options[:timestamps]
581
+ if respond_to?(:timestamp_attributes_for_update, true)
582
+ send(:timestamp_attributes_for_update).map(&:to_sym)
583
+ else
584
+ new.send(:timestamp_attributes_for_update_in_model)
585
+ end
586
+ end
587
+
556
588
  array_of_attributes = []
557
589
 
558
590
  models.each do |model|
@@ -566,9 +598,13 @@ class ActiveRecord::Base
566
598
  end
567
599
 
568
600
  array_of_attributes << column_names.map do |name|
569
- if stored_attrs.key?(name.to_sym) ||
570
- serialized_attrs.key?(name) ||
571
- default_values.key?(name.to_s)
601
+ if model.persisted? &&
602
+ update_attrs && update_attrs.include?(name.to_sym) &&
603
+ !model.send("#{name}_changed?")
604
+ nil
605
+ elsif stored_attrs.key?(name.to_sym) ||
606
+ serialized_attrs.key?(name.to_s) ||
607
+ default_values[name.to_s]
572
608
  model.read_attribute(name.to_s)
573
609
  else
574
610
  model.read_attribute_before_type_cast(name.to_s)
@@ -655,7 +691,7 @@ class ActiveRecord::Base
655
691
  timestamps = {}
656
692
 
657
693
  # record timestamps unless disabled in ActiveRecord::Base
658
- if record_timestamps && options.delete( :timestamps )
694
+ if record_timestamps && options[:timestamps]
659
695
  timestamps = add_special_rails_stamps column_names, array_of_attributes, options
660
696
  end
661
697
 
@@ -961,7 +997,7 @@ class ActiveRecord::Base
961
997
  index = column_names.index(column) || column_names.index(column.to_sym)
962
998
  if index
963
999
  # replace every instance of the array of attributes with our value
964
- array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? || action == :update }
1000
+ array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? }
965
1001
  else
966
1002
  column_names << column
967
1003
  array_of_attributes.each { |arr| arr << timestamp }
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Import
3
- VERSION = "0.27.0".freeze
3
+ VERSION = "0.28.1".freeze
4
4
  end
5
5
  end
@@ -303,6 +303,27 @@ describe "#import" do
303
303
  end
304
304
  end
305
305
  end
306
+
307
+ context "when validatoring presence of belongs_to association" do
308
+ it "should not import records without foreign key" do
309
+ assert_no_difference "UserToken.count" do
310
+ UserToken.import [:token], [['12345abcdef67890']]
311
+ end
312
+ end
313
+
314
+ it "should import records with foreign key" do
315
+ assert_difference "UserToken.count", +1 do
316
+ UserToken.import [:user_name, :token], [%w("Bob", "12345abcdef67890")]
317
+ end
318
+ end
319
+
320
+ it "should not mutate the defined validations" do
321
+ UserToken.import [:user_name, :token], [%w("Bob", "12345abcdef67890")]
322
+ ut = UserToken.new
323
+ ut.valid?
324
+ assert_includes ut.errors.messages, :user
325
+ end
326
+ end
306
327
  end
307
328
 
308
329
  context "without :validation option" do
@@ -499,11 +520,11 @@ describe "#import" do
499
520
 
500
521
  context "when the timestamps columns are present" do
501
522
  setup do
502
- @existing_book = Book.create(title: "Fell", author_name: "Curry", publisher: "Bayer", created_at: 2.years.ago.utc, created_on: 2.years.ago.utc)
523
+ @existing_book = Book.create(title: "Fell", author_name: "Curry", publisher: "Bayer", created_at: 2.years.ago.utc, created_on: 2.years.ago.utc, updated_at: 2.years.ago.utc, updated_on: 2.years.ago.utc)
503
524
  ActiveRecord::Base.default_timezone = :utc
504
525
  Timecop.freeze(time) do
505
526
  assert_difference "Book.count", +2 do
506
- Book.import %w(title author_name publisher created_at created_on), [["LDAP", "Big Bird", "Del Rey", nil, nil], [@existing_book.title, @existing_book.author_name, @existing_book.publisher, @existing_book.created_at, @existing_book.created_on]]
527
+ Book.import %w(title author_name publisher created_at created_on updated_at updated_on), [["LDAP", "Big Bird", "Del Rey", nil, nil, nil, nil], [@existing_book.title, @existing_book.author_name, @existing_book.publisher, @existing_book.created_at, @existing_book.created_on, @existing_book.updated_at, @existing_book.updated_on]]
507
528
  end
508
529
  end
509
530
  @new_book, @existing_book = Book.last 2
@@ -532,6 +553,23 @@ describe "#import" do
532
553
  it "should set the updated_on column for new records" do
533
554
  assert_in_delta time.to_i, @new_book.updated_on.to_i, 1.second
534
555
  end
556
+
557
+ it "should not set the updated_at column for existing records" do
558
+ assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.updated_at.strftime("%Y:%d")
559
+ end
560
+
561
+ it "should not set the updated_on column for existing records" do
562
+ assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.updated_on.strftime("%Y:%d")
563
+ end
564
+
565
+ it "should not set the updated_at column on models if changed" do
566
+ timestamp = Time.now.utc
567
+ books = [
568
+ Book.new(author_name: "Foo", title: "Baz", created_at: timestamp, updated_at: timestamp)
569
+ ]
570
+ Book.import books
571
+ assert_equal timestamp.strftime("%Y:%d"), Book.last.updated_at.strftime("%Y:%d")
572
+ end
535
573
  end
536
574
 
537
575
  context "when a custom time zone is set" do
@@ -1,3 +1,4 @@
1
1
  class UserToken < ActiveRecord::Base
2
2
  belongs_to :user, primary_key: :name, foreign_key: :user_name
3
+ validates :user, presence: true
3
4
  end
@@ -246,6 +246,26 @@ def should_support_basic_on_duplicate_key_update
246
246
  end
247
247
  end
248
248
 
249
+ context "with timestamps enabled" do
250
+ let(:time) { Chronic.parse("5 minutes from now") }
251
+
252
+ it 'should not overwrite changed updated_at with current timestamp' do
253
+ topic = Topic.create(author_name: "Jane Doe", title: "Book")
254
+ timestamp = Time.now.utc
255
+ topic.updated_at = timestamp
256
+ Topic.import [topic], on_duplicate_key_update: :all, validate: false
257
+ assert_equal timestamp.to_s, Topic.last.updated_at.to_s
258
+ end
259
+
260
+ it 'should update updated_at with current timestamp' do
261
+ topic = Topic.create(author_name: "Jane Doe", title: "Book")
262
+ Timecop.freeze(time) do
263
+ Topic.import [topic], on_duplicate_key_update: [:updated_at], validate: false
264
+ assert_in_delta time.to_i, topic.reload.updated_at.to_i, 1.second
265
+ end
266
+ end
267
+ end
268
+
249
269
  context "with validation checks turned off" do
250
270
  asssertion_group(:should_support_on_duplicate_key_update) do
251
271
  should_not_update_fields_not_mentioned
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.27.0
4
+ version: 0.28.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Dennis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-09 00:00:00.000000000 Z
11
+ date: 2019-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord