File: import_examples.rb

package info (click to toggle)
ruby-activerecord-import 1.4.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 840 kB
  • sloc: ruby: 4,698; makefile: 7
file content (646 lines) | stat: -rw-r--r-- 22,596 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
# frozen_string_literal: true

def should_support_postgresql_import_functionality
  should_support_recursive_import

  if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
    should_support_postgresql_upsert_functionality
  end

  describe "#supports_imports?" do
    it "should support import" do
      assert ActiveRecord::Base.supports_import?
    end
  end

  describe "#import" do
    it "should import with a single insert" do
      # see ActiveRecord::ConnectionAdapters::AbstractAdapter test for more specifics
      assert_difference "Topic.count", +10 do
        result = Topic.import Build(3, :topics)
        assert_equal 1, result.num_inserts

        result = Topic.import Build(7, :topics)
        assert_equal 1, result.num_inserts
      end
    end

    context "setting attributes and marking clean" do
      let(:topic) { Build(:topics) }

      setup { Topic.import([topic]) }

      it "assigns ids" do
        assert topic.id.present?
      end

      it "marks models as clean" do
        assert !topic.changed?
      end

      if ENV['AR_VERSION'].to_f > 4.1
        it "moves the dirty changes to previous_changes" do
          assert topic.previous_changes.present?
        end
      end

      it "marks models as persisted" do
        assert !topic.new_record?
        assert topic.persisted?
      end

      it "assigns timestamps" do
        assert topic.created_at.present?
        assert topic.updated_at.present?
      end
    end

    describe "with query cache enabled" do
      setup do
        unless ActiveRecord::Base.connection.query_cache_enabled
          ActiveRecord::Base.connection.enable_query_cache!
          @disable_cache_on_teardown = true
        end
      end

      it "clears cache on insert" do
        before_import = Topic.all.to_a

        Topic.import(Build(2, :topics), validate: false)

        after_import = Topic.all.to_a
        assert_equal 2, after_import.size - before_import.size
      end

      teardown do
        if @disable_cache_on_teardown
          ActiveRecord::Base.connection.disable_query_cache!
        end
      end
    end

    describe "no_returning" do
      let(:books) { [Book.new(author_name: "foo", title: "bar")] }

      it "creates records" do
        assert_difference "Book.count", +1 do
          Book.import books, no_returning: true
        end
      end

      it "returns no ids" do
        assert_equal [], Book.import(books, no_returning: true).ids
      end
    end

    describe "returning" do
      let(:books) { [Book.new(author_name: "King", title: "It")] }
      let(:result) { Book.import(books, returning: %w(author_name title)) }
      let(:book_id) do
        if RUBY_PLATFORM == 'java' || ENV['AR_VERSION'].to_i >= 5.0
          books.first.id
        else
          books.first.id.to_s
        end
      end
      let(:true_returning_value) { ENV['AR_VERSION'].to_f >= 5.0 ? true : 't' }
      let(:false_returning_value) { ENV['AR_VERSION'].to_f >= 5.0 ? false : 'f' }

      it "creates records" do
        assert_difference("Book.count", +1) { result }
      end

      it "returns ids" do
        result
        assert_equal [book_id], result.ids
      end

      it "returns specified columns" do
        assert_equal [%w(King It)], result.results
      end

      context "when given an empty array" do
        let(:result) { Book.import([], returning: %w(title)) }

        setup { result }

        it "returns empty arrays for ids and results" do
          assert_equal [], result.ids
          assert_equal [], result.results
        end
      end

      context "when a returning column is a serialized attribute" do
        let(:vendor) { Vendor.new(hours: { monday: '8-5' }) }
        let(:result) { Vendor.import([vendor], returning: %w(hours)) }

        it "creates records" do
          assert_difference("Vendor.count", +1) { result }
        end
      end

      context "when primary key and returning overlap" do
        let(:result) { Book.import(books, returning: %w(id title)) }

        setup { result }

        it "returns ids" do
          assert_equal [book_id], result.ids
        end

        it "returns specified columns" do
          assert_equal [[book_id, 'It']], result.results
        end
      end

      context "when returning is raw sql" do
        let(:result) { Book.import(books, returning: "title, (xmax = '0') AS inserted") }

        setup { result }

        it "returns ids" do
          assert_equal [book_id], result.ids
        end

        it "returns specified columns" do
          assert_equal [['It', true_returning_value]], result.results
        end
      end

      context "when returning contains raw sql" do
        let(:result) { Book.import(books, returning: [:title, "id, (xmax = '0') AS inserted"]) }

        setup { result }

        it "returns ids" do
          assert_equal [book_id], result.ids
        end

        it "returns specified columns" do
          assert_equal [['It', book_id, true_returning_value]], result.results
        end
      end

      context "setting model attributes" do
        let(:code) { 'abc' }
        let(:discount) { 0.10 }
        let(:original_promotion) do
          Promotion.new(code: code, discount: discount)
        end
        let(:updated_promotion) do
          Promotion.new(code: code, description: 'ABC discount')
        end
        let(:returning_columns) { %w(discount) }

        setup do
          Promotion.import([original_promotion])
          Promotion.import([updated_promotion],
            on_duplicate_key_update: { conflict_target: %i(code), columns: %i(description) },
            returning: returning_columns)
        end

        it "sets model attributes" do
          assert_equal updated_promotion.discount, discount
        end

        context "returning multiple columns" do
          let(:returning_columns) { %w(discount description) }

          it "sets model attributes" do
            assert_equal updated_promotion.discount, discount
          end
        end

        context 'returning raw sql' do
          let(:returning_columns) { [:discount, "(xmax = '0') AS inserted"] }

          it "sets custom model attributes" do
            assert_equal updated_promotion.inserted, false_returning_value
          end
        end
      end
    end
  end

  if ENV['AR_VERSION'].to_f >= 4.0
    describe "with a uuid primary key" do
      let(:vendor) { Vendor.new(name: "foo") }
      let(:vendors) { [vendor] }

      it "creates records" do
        assert_difference "Vendor.count", +1 do
          Vendor.import vendors
        end
      end

      it "assigns an id to the model objects" do
        Vendor.import vendors
        assert_not_nil vendor.id
      end
    end

    describe "with an assigned uuid primary key" do
      let(:id) { SecureRandom.uuid }
      let(:vendor) { Vendor.new(id: id, name: "foo") }
      let(:vendors) { [vendor] }

      it "creates records with correct id" do
        assert_difference "Vendor.count", +1 do
          Vendor.import vendors
        end
        assert_equal id, vendor.id
      end
    end
  end

  describe "with store accessor fields" do
    if ENV['AR_VERSION'].to_f >= 4.0
      it "imports values for json fields" do
        vendors = [Vendor.new(name: 'Vendor 1', size: 100)]
        assert_difference "Vendor.count", +1 do
          Vendor.import vendors
        end
        assert_equal(100, Vendor.first.size)
      end

      it "imports values for hstore fields" do
        vendors = [Vendor.new(name: 'Vendor 1', contact: 'John Smith')]
        assert_difference "Vendor.count", +1 do
          Vendor.import vendors
        end
        assert_equal('John Smith', Vendor.first.contact)
      end
    end

    if ENV['AR_VERSION'].to_f >= 4.2
      it "imports values for jsonb fields" do
        vendors = [Vendor.new(name: 'Vendor 1', charge_code: '12345')]
        assert_difference "Vendor.count", +1 do
          Vendor.import vendors
        end
        assert_equal('12345', Vendor.first.charge_code)
      end
    end
  end

  if ENV['AR_VERSION'].to_f >= 4.2
    describe "with serializable fields" do
      it "imports default values as correct data type" do
        vendors = [Vendor.new(name: 'Vendor 1')]
        assert_difference "Vendor.count", +1 do
          Vendor.import vendors
        end
        assert_equal({}, Vendor.first.json_data)
      end
    end

    %w(json jsonb).each do |json_type|
      describe "with pure #{json_type} fields" do
        let(:data) { { a: :b } }
        let(:json_field_name) { "pure_#{json_type}_data" }
        it "imports the values from saved records" do
          vendor = Vendor.create!(name: 'Vendor 1', json_field_name => data)

          Vendor.import [vendor], on_duplicate_key_update: [json_field_name]
          assert_equal(data.as_json, vendor.reload[json_field_name])
        end
      end
    end
  end

  describe "with enum field" do
    let(:vendor_type) { "retailer" }
    it "imports the correct values for enum fields" do
      vendor = Vendor.new(name: 'Vendor 1', vendor_type: vendor_type)
      assert_difference "Vendor.count", +1 do
        Vendor.import [vendor]
      end
      assert_equal(vendor_type, Vendor.first.vendor_type)
    end
  end

  describe "with binary field" do
    let(:binary_value) { "\xE0'c\xB2\xB0\xB3Bh\\\xC2M\xB1m\\I\xC4r".dup.force_encoding('ASCII-8BIT') }
    it "imports the correct values for binary fields" do
      alarms = [Alarm.new(device_id: 1, alarm_type: 1, status: 1, secret_key: binary_value)]
      assert_difference "Alarm.count", +1 do
        Alarm.import alarms
      end
      assert_equal(binary_value, Alarm.first.secret_key)
    end
  end

  unless ENV["SKIP_COMPOSITE_PK"]
    describe "with composite foreign keys" do
      let(:account_id) { 555 }
      let(:customer) { Customer.new(account_id: account_id, name: "foo") }
      let(:order) { Order.new(account_id: account_id, amount: 100, customer: customer) }

      it "imports and correctly maps foreign keys" do
        assert_difference "Customer.count", +1 do
          Customer.import [customer]
        end

        assert_difference "Order.count", +1 do
          Order.import [order]
        end

        db_customer = Customer.last
        db_order = Order.last

        assert_equal db_customer.orders.last, db_order
        assert_not_equal db_order.customer_id, nil
      end
    end
  end
end

def should_support_postgresql_upsert_functionality
  should_support_basic_on_duplicate_key_update
  should_support_on_duplicate_key_ignore

  describe "#import" do
    extend ActiveSupport::TestCase::ImportAssertions

    macro(:perform_import) { raise "supply your own #perform_import in a context below" }
    macro(:updated_topic) { Topic.find(@topic.id) }

    context "with :on_duplicate_key_ignore and validation checks turned off" do
      let(:columns) { %w( id title author_name author_email_address parent_id ) }
      let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
      let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }

      setup do
        Topic.import columns, values, validate: false
      end

      it "should not update any records" do
        result = Topic.import columns, updated_values, on_duplicate_key_ignore: true, validate: false
        assert_equal [], result.ids
      end
    end

    context "with :on_duplicate_key_ignore and :recursive enabled" do
      let(:new_topic) { Build(1, :topic_with_book) }
      let(:mixed_topics) { Build(1, :topic_with_book) + new_topic + Build(1, :topic_with_book) }

      setup do
        Topic.import new_topic, recursive: true
      end

      # Recursive import depends on the primary keys of the parent model being returned
      # on insert. With on_duplicate_key_ignore enabled, not all ids will be returned
      # and it is possible that a model will be assigned the wrong id and then its children
      # would be associated with the wrong parent.
      it ":on_duplicate_key_ignore is ignored" do
        assert_raise ActiveRecord::RecordNotUnique do
          Topic.import mixed_topics, recursive: true, on_duplicate_key_ignore: true, validate: false
        end
      end
    end

    context "with :on_duplicate_key_update and validation checks turned off" do
      asssertion_group(:should_support_on_duplicate_key_update) do
        should_not_update_fields_not_mentioned
        should_update_foreign_keys
        should_not_update_created_at_on_timestamp_columns
        should_update_updated_at_on_timestamp_columns
      end

      context "using a hash" do
        context "with :columns :all" do
          let(:columns) { %w( id title author_name author_email_address parent_id ) }
          let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }

          macro(:perform_import) do |*opts|
            Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: :all }, validate: false)
          end

          setup do
            values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
            Topic.import columns + ['replies_count'], values, validate: false
          end

          it "should update all specified columns" do
            perform_import
            updated_topic = Topic.find(99)
            assert_equal 'Book - 2nd Edition', updated_topic.title
            assert_equal 'Jane Doe', updated_topic.author_name
            assert_equal 'janedoe@example.com', updated_topic.author_email_address
            assert_equal 57, updated_topic.parent_id
            assert_equal 3, updated_topic.replies_count
          end
        end

        context "with :columns a hash" do
          let(:columns) { %w( id title author_name author_email_address parent_id ) }
          let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
          let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }

          macro(:perform_import) do |*opts|
            Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: update_columns }, validate: false)
          end

          setup do
            Topic.import columns, values, validate: false
            @topic = Topic.find 99
          end

          it "should not modify the passed in :on_duplicate_key_update columns array" do
            assert_nothing_raised do
              columns = %w(title author_name).freeze
              Topic.import columns, [%w(foo, bar)], { on_duplicate_key_update: { columns: columns }.freeze }.freeze
            end
          end

          context "using string hash map" do
            let(:update_columns) { { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
            should_support_on_duplicate_key_update
            should_update_fields_mentioned
          end

          context "using string hash map, but specifying column mismatches" do
            let(:update_columns) { { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } }
            should_support_on_duplicate_key_update
            should_update_fields_mentioned_with_hash_mappings
          end

          context "using symbol hash map" do
            let(:update_columns) { { title: :title, author_email_address: :author_email_address, parent_id: :parent_id } }
            should_support_on_duplicate_key_update
            should_update_fields_mentioned
          end

          context "using symbol hash map, but specifying column mismatches" do
            let(:update_columns) { { title: :author_email_address, author_email_address: :title, parent_id: :parent_id } }
            should_support_on_duplicate_key_update
            should_update_fields_mentioned_with_hash_mappings
          end
        end

        context 'with :index_predicate' do
          let(:columns) { %w( id device_id alarm_type status metadata ) }
          let(:values) { [[99, 17, 1, 1, 'foo']] }
          let(:updated_values) { [[99, 17, 1, 2, 'bar']] }

          macro(:perform_import) do |*opts|
            Alarm.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: [:device_id, :alarm_type], index_predicate: 'status <> 0', columns: [:status] }, validate: false)
          end

          macro(:updated_alarm) { Alarm.find(@alarm.id) }

          setup do
            Alarm.import columns, values, validate: false
            @alarm = Alarm.find 99
          end

          context 'supports on duplicate key update for partial indexes' do
            it 'should not update created_at timestamp columns' do
              Timecop.freeze Chronic.parse("5 minutes from now") do
                perform_import
                assert_in_delta @alarm.created_at.to_i, updated_alarm.created_at.to_i, 1
              end
            end

            it 'should update updated_at timestamp columns' do
              time = Chronic.parse("5 minutes from now")
              Timecop.freeze time do
                perform_import
                assert_in_delta time.to_i, updated_alarm.updated_at.to_i, 1
              end
            end

            it 'should not update fields not mentioned' do
              perform_import
              assert_equal 'foo', updated_alarm.metadata
            end

            it 'should update fields mentioned with hash mappings' do
              perform_import
              assert_equal 2, updated_alarm.status
            end
          end
        end

        context 'with :condition' do
          let(:columns) { %w( id device_id alarm_type status metadata) }
          let(:values) { [[99, 17, 1, 1, 'foo']] }
          let(:updated_values) { [[99, 17, 1, 1, 'bar']] }

          macro(:perform_import) do |*opts|
            Alarm.import(
              columns,
              updated_values,
              opts.extract_options!.merge(
                on_duplicate_key_update: {
                  conflict_target: [:id],
                  condition: "alarms.metadata NOT LIKE '%foo%'",
                  columns: [:metadata]
                },
                validate: false
              )
            )
          end

          macro(:updated_alarm) { Alarm.find(@alarm.id) }

          setup do
            Alarm.import columns, values, validate: false
            @alarm = Alarm.find 99
          end

          it 'should not update fields not matched' do
            perform_import
            assert_equal 'foo', updated_alarm.metadata
          end
        end

        context "with :constraint_name" do
          let(:columns) { %w( id title author_name author_email_address parent_id ) }
          let(:values) { [[100, "Book", "John Doe", "john@doe.com", 17]] }
          let(:updated_values) { [[100, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }

          macro(:perform_import) do |*opts|
            Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { constraint_name: :topics_pkey, columns: update_columns }, validate: false)
          end

          setup do
            Topic.import columns, values, validate: false
            @topic = Topic.find 100
          end

          let(:update_columns) { [:title, :author_email_address, :parent_id] }
          should_support_on_duplicate_key_update
          should_update_fields_mentioned
        end

        context "default to the primary key" do
          let(:columns) { %w( id title author_name author_email_address parent_id ) }
          let(:values) { [[100, "Book", "John Doe", "john@doe.com", 17]] }
          let(:updated_values) { [[100, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
          let(:update_columns) { [:title, :author_email_address, :parent_id] }

          setup do
            Topic.import columns, values, validate: false
            @topic = Topic.find 100
          end

          context "with no :conflict_target or :constraint_name" do
            macro(:perform_import) do |*opts|
              Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { columns: update_columns }, validate: false)
            end

            should_support_on_duplicate_key_update
            should_update_fields_mentioned
          end

          context "with empty value for :conflict_target" do
            macro(:perform_import) do |*opts|
              Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: [], columns: update_columns }, validate: false)
            end

            should_support_on_duplicate_key_update
            should_update_fields_mentioned
          end

          context "with empty value for :constraint_name" do
            macro(:perform_import) do |*opts|
              Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { constraint_name: '', columns: update_columns }, validate: false)
            end

            should_support_on_duplicate_key_update
            should_update_fields_mentioned
          end
        end

        context "with no :conflict_target or :constraint_name" do
          context "with no primary key" do
            it "raises ArgumentError" do
              error = assert_raises ArgumentError do
                Rule.import Build(3, :rules), on_duplicate_key_update: [:condition_text], validate: false
              end
              assert_match(/Expected :conflict_target or :constraint_name to be specified/, error.message)
            end
          end
        end

        context "with no :columns" do
          let(:columns) { %w( id title author_name author_email_address ) }
          let(:values) { [[100, "Book", "John Doe", "john@doe.com"]] }
          let(:updated_values) { [[100, "Title Should Not Change", "Author Should Not Change", "john@nogo.com"]] }

          macro(:perform_import) do |*opts|
            Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id }, validate: false)
          end

          setup do
            Topic.import columns, values, validate: false
            @topic = Topic.find 100
          end

          should_update_updated_at_on_timestamp_columns
        end
      end
    end
  end
end