activerecord-import 1.6.0 → 1.7.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/.github/workflows/test.yaml +11 -2
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +4 -0
- data/CHANGELOG.md +7 -0
- data/README.markdown +2 -1
- data/lib/activerecord-import/import.rb +19 -3
- data/lib/activerecord-import/version.rb +1 -1
- data/lib/activerecord-import.rb +0 -1
- data/test/models/author.rb +7 -0
- data/test/models/book.rb +5 -2
- data/test/models/composite_book.rb +19 -0
- data/test/models/composite_chapter.rb +9 -0
- data/test/models/customer.rb +12 -4
- data/test/models/order.rb +11 -4
- data/test/models/tag.rb +6 -1
- data/test/models/tag_alias.rb +5 -1
- data/test/schema/postgresql_schema.rb +34 -0
- data/test/support/postgresql/import_examples.rb +12 -0
- data/test/support/shared_examples/recursive_import.rb +66 -0
- data/test/test_helper.rb +3 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5aeed63fcbc9ad647ab901931265d2765baf5d0e5a1e9d2cf2c68af32dd5e1be
|
4
|
+
data.tar.gz: 47480cc7244aada4bc69ba28043e8ab85ae1cece4420db4453400425a5773126
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f6b5749dd6ce81784f7016c6227a6d0f5047c59a45beb13616853a89599ac507241012a92c3e4f9996c7a465d4d926e0e8d1183af1854f7a0c8a05e2e59219f
|
7
|
+
data.tar.gz: 72b00e7a5c61af3923b618ebe0d96f945a7af145f38ec5312962220b925e1dfc918da6028bb48d8ff7842c32584b34b06cec8fd8288dbb83212ca035f7a16c08
|
data/.github/workflows/test.yaml
CHANGED
@@ -34,7 +34,7 @@ jobs:
|
|
34
34
|
fail-fast: false
|
35
35
|
matrix:
|
36
36
|
ruby:
|
37
|
-
- 3.
|
37
|
+
- 3.3
|
38
38
|
env:
|
39
39
|
- AR_VERSION: '7.1'
|
40
40
|
RUBYOPT: --enable-frozen-string-literal
|
@@ -43,6 +43,15 @@ jobs:
|
|
43
43
|
- AR_VERSION: 6.1
|
44
44
|
RUBYOPT: --enable-frozen-string-literal
|
45
45
|
include:
|
46
|
+
- ruby: 3.2
|
47
|
+
env:
|
48
|
+
AR_VERSION: '7.1'
|
49
|
+
- ruby: 3.2
|
50
|
+
env:
|
51
|
+
AR_VERSION: '7.0'
|
52
|
+
- ruby: 3.2
|
53
|
+
env:
|
54
|
+
AR_VERSION: 6.1
|
46
55
|
- ruby: 3.1
|
47
56
|
env:
|
48
57
|
AR_VERSION: '7.1'
|
@@ -126,7 +135,7 @@ jobs:
|
|
126
135
|
bundle exec rake test:spatialite
|
127
136
|
bundle exec rake test:sqlite3
|
128
137
|
- name: Run trilogy tests
|
129
|
-
if: ${{ matrix.env.AR_VERSION >= '7.0' && matrix.ruby
|
138
|
+
if: ${{ matrix.env.AR_VERSION >= '7.0' && !startsWith(matrix.ruby, 'jruby') }}
|
130
139
|
run: bundle exec rake test:trilogy
|
131
140
|
lint:
|
132
141
|
runs-on: ubuntu-latest
|
data/.gitignore
CHANGED
data/.rubocop_todo.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
## Changes in 1.7.0
|
2
|
+
|
3
|
+
### New Features
|
4
|
+
|
5
|
+
* Add support for ActiveRecord 7.1 composite primary keys. Thanks to @fragkakis via \##837.
|
6
|
+
* Add support for upserting associations when doing recursive imports. Thanks to @ramblex via \##778.
|
7
|
+
|
1
8
|
## Changes in 1.6.0
|
2
9
|
|
3
10
|
### New Features
|
data/README.markdown
CHANGED
@@ -265,7 +265,7 @@ Book.import books, recursive: true
|
|
265
265
|
Key | Options | Default | Description
|
266
266
|
------------------------- | --------------------- | ------------------ | -----------
|
267
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
|
268
|
+
:validate_uniqueness | `true`/`false` | `false` | Whether or not to run ActiveRecord uniqueness validations. Beware this will incur an sql query per-record (N+1 queries). (requires `>= v0.27.0`).
|
269
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
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
271
|
:on_duplicate_key_ignore | `true`/`false` | `false` | Allows skipping records with duplicate keys. See [here](#duplicate-key-ignore) for more details.
|
@@ -274,6 +274,7 @@ Key | Options | Default | Descrip
|
|
274
274
|
:synchronize | `Array` | N/A | An array of ActiveRecord instances. This synchronizes existing instances in memory with updates from the import.
|
275
275
|
:timestamps | `true`/`false` | `true` | Enables/disables timestamps on imported records.
|
276
276
|
:recursive | `true`/`false` | `false` | Imports has_many/has_one associations (PostgreSQL only).
|
277
|
+
:recursive_on_duplicate_key_update | `Hash` | N/A | Allows upsert logic to be used for recursive associations. The hash key is the association name and the value has the same options as `:on_duplicate_key_update`. See [here](#duplicate-key-update) for more details.
|
277
278
|
:batch_size | `Integer` | total # of records | Max number of records to insert per import
|
278
279
|
: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
280
|
:all_or_none | `true`/`false` | `false` | Will not import any records if there is a record with validation errors.
|
@@ -857,6 +857,15 @@ class ActiveRecord::Base
|
|
857
857
|
|
858
858
|
private
|
859
859
|
|
860
|
+
def associated_options(options, associated_class)
|
861
|
+
return options unless options.key?(:recursive_on_duplicate_key_update)
|
862
|
+
|
863
|
+
table_name = associated_class.arel_table.name.to_sym
|
864
|
+
options.merge(
|
865
|
+
on_duplicate_key_update: options[:recursive_on_duplicate_key_update][table_name]
|
866
|
+
)
|
867
|
+
end
|
868
|
+
|
860
869
|
def set_attributes_and_mark_clean(models, import_result, timestamps, options)
|
861
870
|
return if models.nil?
|
862
871
|
models -= import_result.failed_instances
|
@@ -941,7 +950,7 @@ class ActiveRecord::Base
|
|
941
950
|
association = association.target
|
942
951
|
next if association.blank? || model.public_send(column_name).present?
|
943
952
|
|
944
|
-
association_primary_key = Array(association_reflection.association_primary_key)[column_index]
|
953
|
+
association_primary_key = Array(association_reflection.association_primary_key.tr("[]:", "").split(", "))[column_index]
|
945
954
|
model.public_send("#{column_name}=", association.send(association_primary_key))
|
946
955
|
end
|
947
956
|
end
|
@@ -963,7 +972,11 @@ class ActiveRecord::Base
|
|
963
972
|
|
964
973
|
associated_objects_by_class.each_value do |associations|
|
965
974
|
associations.each_value do |associated_records|
|
966
|
-
|
975
|
+
next if associated_records.empty?
|
976
|
+
|
977
|
+
associated_class = associated_records.first.class
|
978
|
+
associated_class.bulk_import(associated_records,
|
979
|
+
associated_options(options, associated_class))
|
967
980
|
end
|
968
981
|
end
|
969
982
|
end
|
@@ -996,7 +1009,10 @@ class ActiveRecord::Base
|
|
996
1009
|
|
997
1010
|
changed_objects = association.select { |a| a.new_record? || a.changed? }
|
998
1011
|
changed_objects.each do |child|
|
999
|
-
|
1012
|
+
Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key).each_with_index do |column, index|
|
1013
|
+
child.public_send("#{column}=", Array(model.id)[index])
|
1014
|
+
end
|
1015
|
+
|
1000
1016
|
# For polymorphic associations
|
1001
1017
|
association_name = if model.class.respond_to?(:polymorphic_name)
|
1002
1018
|
model.class.polymorphic_name
|
data/lib/activerecord-import.rb
CHANGED
data/test/models/book.rb
CHANGED
@@ -2,8 +2,11 @@
|
|
2
2
|
|
3
3
|
class Book < ActiveRecord::Base
|
4
4
|
belongs_to :topic, inverse_of: :books
|
5
|
-
|
6
|
-
|
5
|
+
if ENV['AR_VERSION'].to_f <= 7.0
|
6
|
+
belongs_to :tag, foreign_key: [:tag_id, :parent_id] unless ENV["SKIP_COMPOSITE_PK"]
|
7
|
+
else
|
8
|
+
belongs_to :tag, query_constraints: [:tag_id, :parent_id] unless ENV["SKIP_COMPOSITE_PK"]
|
9
|
+
end
|
7
10
|
has_many :chapters, inverse_of: :book
|
8
11
|
has_many :discounts, as: :discountable
|
9
12
|
has_many :end_notes, inverse_of: :book
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CompositeBook < ActiveRecord::Base
|
4
|
+
self.primary_key = %i[id author_id]
|
5
|
+
belongs_to :author
|
6
|
+
if ENV['AR_VERSION'].to_f <= 7.0
|
7
|
+
unless ENV["SKIP_COMPOSITE_PK"]
|
8
|
+
has_many :composite_chapters, inverse_of: :composite_book,
|
9
|
+
foreign_key: [:id, :author_id]
|
10
|
+
end
|
11
|
+
else
|
12
|
+
has_many :composite_chapters, inverse_of: :composite_book,
|
13
|
+
query_constraints: [:id, :author_id]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.sequence_name
|
17
|
+
"composite_book_id_seq"
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CompositeChapter < ActiveRecord::Base
|
4
|
+
if ENV['AR_VERSION'].to_f >= 7.1
|
5
|
+
belongs_to :composite_book, inverse_of: :composite_chapters,
|
6
|
+
query_constraints: [:composite_book_id, :author_id]
|
7
|
+
end
|
8
|
+
validates :title, presence: true
|
9
|
+
end
|
data/test/models/customer.rb
CHANGED
@@ -2,9 +2,17 @@
|
|
2
2
|
|
3
3
|
class Customer < ActiveRecord::Base
|
4
4
|
unless ENV["SKIP_COMPOSITE_PK"]
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
if ENV['AR_VERSION'].to_f <= 7.0
|
6
|
+
has_many :orders,
|
7
|
+
inverse_of: :customer,
|
8
|
+
primary_key: %i(account_id id),
|
9
|
+
foreign_key: %i(account_id customer_id)
|
10
|
+
else
|
11
|
+
has_many :orders,
|
12
|
+
inverse_of: :customer,
|
13
|
+
primary_key: %i(account_id id),
|
14
|
+
query_constraints: %i(account_id customer_id)
|
15
|
+
end
|
16
|
+
|
9
17
|
end
|
10
18
|
end
|
data/test/models/order.rb
CHANGED
@@ -2,9 +2,16 @@
|
|
2
2
|
|
3
3
|
class Order < ActiveRecord::Base
|
4
4
|
unless ENV["SKIP_COMPOSITE_PK"]
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
if ENV['AR_VERSION'].to_f <= 7.0
|
6
|
+
belongs_to :customer,
|
7
|
+
inverse_of: :orders,
|
8
|
+
primary_key: %i(account_id id),
|
9
|
+
foreign_key: %i(account_id customer_id)
|
10
|
+
else
|
11
|
+
belongs_to :customer,
|
12
|
+
inverse_of: :orders,
|
13
|
+
primary_key: %i(account_id id),
|
14
|
+
query_constraints: %i(account_id customer_id)
|
15
|
+
end
|
9
16
|
end
|
10
17
|
end
|
data/test/models/tag.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Tag < ActiveRecord::Base
|
4
|
-
|
4
|
+
if ENV['AR_VERSION'].to_f <= 7.0
|
5
|
+
self.primary_keys = :tag_id, :publisher_id unless ENV["SKIP_COMPOSITE_PK"]
|
6
|
+
else
|
7
|
+
self.primary_key = [:tag_id, :publisher_id] unless ENV["SKIP_COMPOSITE_PK"]
|
8
|
+
end
|
9
|
+
self.primary_key = [:tag_id, :publisher_id] unless ENV["SKIP_COMPOSITE_PK"]
|
5
10
|
has_many :books, inverse_of: :tag
|
6
11
|
has_many :tag_aliases, inverse_of: :tag
|
7
12
|
end
|
data/test/models/tag_alias.rb
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
class TagAlias < ActiveRecord::Base
|
4
4
|
unless ENV["SKIP_COMPOSITE_PK"]
|
5
|
-
|
5
|
+
if ENV['AR_VERSION'].to_f <= 7.0
|
6
|
+
belongs_to :tag, foreign_key: [:tag_id, :parent_id], required: true
|
7
|
+
else
|
8
|
+
belongs_to :tag, query_constraints: [:tag_id, :parent_id], required: true
|
9
|
+
end
|
6
10
|
end
|
7
11
|
end
|
@@ -57,4 +57,38 @@ ActiveRecord::Schema.define do
|
|
57
57
|
end
|
58
58
|
|
59
59
|
add_index :alarms, [:device_id, :alarm_type], unique: true, where: 'status <> 0'
|
60
|
+
|
61
|
+
unless ENV["SKIP_COMPOSITE_PK"]
|
62
|
+
create_table :authors, force: :cascade do |t|
|
63
|
+
t.string :name
|
64
|
+
end
|
65
|
+
|
66
|
+
execute %(
|
67
|
+
DROP SEQUENCE IF EXISTS composite_book_id_seq CASCADE;
|
68
|
+
CREATE SEQUENCE composite_book_id_seq
|
69
|
+
AS integer
|
70
|
+
START WITH 1
|
71
|
+
INCREMENT BY 1
|
72
|
+
NO MINVALUE
|
73
|
+
NO MAXVALUE
|
74
|
+
CACHE 1;
|
75
|
+
|
76
|
+
DROP TABLE IF EXISTS composite_books;
|
77
|
+
CREATE TABLE composite_books (
|
78
|
+
id bigint DEFAULT nextval('composite_book_id_seq'::regclass) NOT NULL,
|
79
|
+
title character varying,
|
80
|
+
author_id bigint
|
81
|
+
);
|
82
|
+
|
83
|
+
ALTER TABLE ONLY composite_books ADD CONSTRAINT fk_rails_040a418131 FOREIGN KEY (author_id) REFERENCES authors(id);
|
84
|
+
).split.join(' ').strip
|
85
|
+
end
|
86
|
+
|
87
|
+
create_table :composite_chapters, force: :cascade do |t|
|
88
|
+
t.string :title
|
89
|
+
t.integer :composite_book_id, null: false
|
90
|
+
t.integer :author_id, null: false
|
91
|
+
t.datetime :created_at
|
92
|
+
t.datetime :updated_at
|
93
|
+
end
|
60
94
|
end
|
@@ -351,6 +351,18 @@ def should_support_postgresql_import_functionality
|
|
351
351
|
assert_equal db_customer.orders.last, db_order
|
352
352
|
assert_not_equal db_order.customer_id, nil
|
353
353
|
end
|
354
|
+
|
355
|
+
it "should import models with auto-incrementing ID successfully" do
|
356
|
+
author = Author.create!(name: "Foo Barson")
|
357
|
+
|
358
|
+
books = []
|
359
|
+
2.times do |i|
|
360
|
+
books << CompositeBook.new(author_id: author.id, title: "book #{i}")
|
361
|
+
end
|
362
|
+
assert_difference "CompositeBook.count", +2 do
|
363
|
+
CompositeBook.import books
|
364
|
+
end
|
365
|
+
end
|
354
366
|
end
|
355
367
|
end
|
356
368
|
end
|
@@ -165,6 +165,24 @@ def should_support_recursive_import
|
|
165
165
|
assert_equal 1, tags[0].tag_id
|
166
166
|
assert_equal 2, tags[1].tag_id
|
167
167
|
end
|
168
|
+
|
169
|
+
if ENV['AR_VERSION'].to_f >= 7.1
|
170
|
+
it "should import models with auto-incrementing ID successfully with recursive set to true" do
|
171
|
+
author = Author.create!(name: "Foo Barson")
|
172
|
+
books = []
|
173
|
+
2.times do |i|
|
174
|
+
books << CompositeBook.new(author_id: author.id, title: "Book #{i}", composite_chapters: [
|
175
|
+
CompositeChapter.new(title: "Book #{i} composite chapter 1"),
|
176
|
+
CompositeChapter.new(title: "Book #{i} composite chapter 2"),
|
177
|
+
])
|
178
|
+
end
|
179
|
+
assert_difference "CompositeBook.count", +2 do
|
180
|
+
assert_difference "CompositeChapter.count", +4 do
|
181
|
+
CompositeBook.import books, recursive: true
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
168
186
|
end
|
169
187
|
end
|
170
188
|
|
@@ -213,6 +231,54 @@ def should_support_recursive_import
|
|
213
231
|
end
|
214
232
|
end
|
215
233
|
end
|
234
|
+
|
235
|
+
describe "recursive_on_duplicate_key_update" do
|
236
|
+
let(:new_topics) { Build(1, :topic_with_book) }
|
237
|
+
|
238
|
+
setup do
|
239
|
+
Topic.import new_topics, recursive: true
|
240
|
+
end
|
241
|
+
|
242
|
+
it "updates associated objects" do
|
243
|
+
new_author_name = 'Richard Bachman'
|
244
|
+
topic = new_topics.first
|
245
|
+
topic.books.each do |book|
|
246
|
+
book.author_name = new_author_name
|
247
|
+
end
|
248
|
+
|
249
|
+
assert_nothing_raised do
|
250
|
+
Topic.import new_topics,
|
251
|
+
recursive: true,
|
252
|
+
on_duplicate_key_update: [:id],
|
253
|
+
recursive_on_duplicate_key_update: {
|
254
|
+
books: { conflict_target: [:id], columns: [:author_name] }
|
255
|
+
}
|
256
|
+
end
|
257
|
+
Topic.find(topic.id).books.each do |book|
|
258
|
+
assert_equal new_author_name, book.author_name
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
it "updates nested associated objects" do
|
263
|
+
new_chapter_title = 'The Final Chapter'
|
264
|
+
book = new_topics.first.books.first
|
265
|
+
book.author_name = 'Richard Bachman'
|
266
|
+
|
267
|
+
example_chapter = book.chapters.first
|
268
|
+
example_chapter.title = new_chapter_title
|
269
|
+
|
270
|
+
assert_nothing_raised do
|
271
|
+
Topic.import new_topics,
|
272
|
+
recursive: true,
|
273
|
+
on_duplicate_key_update: [:id],
|
274
|
+
recursive_on_duplicate_key_update: {
|
275
|
+
books: { conflict_target: [:id], columns: [:author_name] },
|
276
|
+
chapters: { conflict_target: [:id], columns: [:title] }
|
277
|
+
}
|
278
|
+
end
|
279
|
+
assert_equal new_chapter_title, Chapter.find(example_chapter.id).title
|
280
|
+
end
|
281
|
+
end
|
216
282
|
end
|
217
283
|
|
218
284
|
# If returning option is provided, it is only applied to top level models so that SQL with invalid
|
data/test/test_helper.rb
CHANGED
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.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zach Dennis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -127,11 +127,14 @@ files:
|
|
127
127
|
- test/models/account.rb
|
128
128
|
- test/models/alarm.rb
|
129
129
|
- test/models/animal.rb
|
130
|
+
- test/models/author.rb
|
130
131
|
- test/models/bike_maker.rb
|
131
132
|
- test/models/book.rb
|
132
133
|
- test/models/car.rb
|
133
134
|
- test/models/card.rb
|
134
135
|
- test/models/chapter.rb
|
136
|
+
- test/models/composite_book.rb
|
137
|
+
- test/models/composite_chapter.rb
|
135
138
|
- test/models/customer.rb
|
136
139
|
- test/models/deck.rb
|
137
140
|
- test/models/dictionary.rb
|
@@ -226,11 +229,14 @@ test_files:
|
|
226
229
|
- test/models/account.rb
|
227
230
|
- test/models/alarm.rb
|
228
231
|
- test/models/animal.rb
|
232
|
+
- test/models/author.rb
|
229
233
|
- test/models/bike_maker.rb
|
230
234
|
- test/models/book.rb
|
231
235
|
- test/models/car.rb
|
232
236
|
- test/models/card.rb
|
233
237
|
- test/models/chapter.rb
|
238
|
+
- test/models/composite_book.rb
|
239
|
+
- test/models/composite_chapter.rb
|
234
240
|
- test/models/customer.rb
|
235
241
|
- test/models/deck.rb
|
236
242
|
- test/models/dictionary.rb
|