activerecord-import 1.0.3

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.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +32 -0
  3. data/.rubocop.yml +49 -0
  4. data/.rubocop_todo.yml +36 -0
  5. data/.travis.yml +74 -0
  6. data/Brewfile +3 -0
  7. data/CHANGELOG.md +430 -0
  8. data/Gemfile +59 -0
  9. data/LICENSE +56 -0
  10. data/README.markdown +619 -0
  11. data/Rakefile +68 -0
  12. data/activerecord-import.gemspec +23 -0
  13. data/benchmarks/README +32 -0
  14. data/benchmarks/benchmark.rb +68 -0
  15. data/benchmarks/lib/base.rb +138 -0
  16. data/benchmarks/lib/cli_parser.rb +107 -0
  17. data/benchmarks/lib/float.rb +15 -0
  18. data/benchmarks/lib/mysql2_benchmark.rb +19 -0
  19. data/benchmarks/lib/output_to_csv.rb +19 -0
  20. data/benchmarks/lib/output_to_html.rb +64 -0
  21. data/benchmarks/models/test_innodb.rb +3 -0
  22. data/benchmarks/models/test_memory.rb +3 -0
  23. data/benchmarks/models/test_myisam.rb +3 -0
  24. data/benchmarks/schema/mysql_schema.rb +16 -0
  25. data/gemfiles/3.2.gemfile +2 -0
  26. data/gemfiles/4.0.gemfile +2 -0
  27. data/gemfiles/4.1.gemfile +2 -0
  28. data/gemfiles/4.2.gemfile +2 -0
  29. data/gemfiles/5.0.gemfile +2 -0
  30. data/gemfiles/5.1.gemfile +2 -0
  31. data/gemfiles/5.2.gemfile +2 -0
  32. data/gemfiles/6.0.gemfile +1 -0
  33. data/gemfiles/6.1.gemfile +1 -0
  34. data/lib/activerecord-import.rb +6 -0
  35. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +9 -0
  36. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -0
  37. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +6 -0
  38. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +6 -0
  39. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +6 -0
  40. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +6 -0
  41. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +7 -0
  42. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +6 -0
  43. data/lib/activerecord-import/adapters/abstract_adapter.rb +66 -0
  44. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +5 -0
  45. data/lib/activerecord-import/adapters/mysql2_adapter.rb +5 -0
  46. data/lib/activerecord-import/adapters/mysql_adapter.rb +129 -0
  47. data/lib/activerecord-import/adapters/postgresql_adapter.rb +217 -0
  48. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +180 -0
  49. data/lib/activerecord-import/base.rb +43 -0
  50. data/lib/activerecord-import/import.rb +1059 -0
  51. data/lib/activerecord-import/mysql2.rb +7 -0
  52. data/lib/activerecord-import/postgresql.rb +7 -0
  53. data/lib/activerecord-import/sqlite3.rb +7 -0
  54. data/lib/activerecord-import/synchronize.rb +66 -0
  55. data/lib/activerecord-import/value_sets_parser.rb +77 -0
  56. data/lib/activerecord-import/version.rb +5 -0
  57. data/test/adapters/jdbcmysql.rb +1 -0
  58. data/test/adapters/jdbcpostgresql.rb +1 -0
  59. data/test/adapters/jdbcsqlite3.rb +1 -0
  60. data/test/adapters/makara_postgis.rb +1 -0
  61. data/test/adapters/mysql2.rb +1 -0
  62. data/test/adapters/mysql2_makara.rb +1 -0
  63. data/test/adapters/mysql2spatial.rb +1 -0
  64. data/test/adapters/postgis.rb +1 -0
  65. data/test/adapters/postgresql.rb +1 -0
  66. data/test/adapters/postgresql_makara.rb +1 -0
  67. data/test/adapters/seamless_database_pool.rb +1 -0
  68. data/test/adapters/spatialite.rb +1 -0
  69. data/test/adapters/sqlite3.rb +1 -0
  70. data/test/database.yml.sample +52 -0
  71. data/test/import_test.rb +903 -0
  72. data/test/jdbcmysql/import_test.rb +5 -0
  73. data/test/jdbcpostgresql/import_test.rb +4 -0
  74. data/test/jdbcsqlite3/import_test.rb +4 -0
  75. data/test/makara_postgis/import_test.rb +8 -0
  76. data/test/models/account.rb +3 -0
  77. data/test/models/alarm.rb +2 -0
  78. data/test/models/bike_maker.rb +7 -0
  79. data/test/models/book.rb +9 -0
  80. data/test/models/car.rb +3 -0
  81. data/test/models/chapter.rb +4 -0
  82. data/test/models/dictionary.rb +4 -0
  83. data/test/models/discount.rb +3 -0
  84. data/test/models/end_note.rb +4 -0
  85. data/test/models/group.rb +3 -0
  86. data/test/models/promotion.rb +3 -0
  87. data/test/models/question.rb +3 -0
  88. data/test/models/rule.rb +3 -0
  89. data/test/models/tag.rb +4 -0
  90. data/test/models/topic.rb +23 -0
  91. data/test/models/user.rb +3 -0
  92. data/test/models/user_token.rb +4 -0
  93. data/test/models/vendor.rb +7 -0
  94. data/test/models/widget.rb +24 -0
  95. data/test/mysql2/import_test.rb +5 -0
  96. data/test/mysql2_makara/import_test.rb +6 -0
  97. data/test/mysqlspatial2/import_test.rb +6 -0
  98. data/test/postgis/import_test.rb +8 -0
  99. data/test/postgresql/import_test.rb +4 -0
  100. data/test/schema/generic_schema.rb +194 -0
  101. data/test/schema/jdbcpostgresql_schema.rb +1 -0
  102. data/test/schema/mysql2_schema.rb +19 -0
  103. data/test/schema/postgis_schema.rb +1 -0
  104. data/test/schema/postgresql_schema.rb +47 -0
  105. data/test/schema/sqlite3_schema.rb +13 -0
  106. data/test/schema/version.rb +10 -0
  107. data/test/sqlite3/import_test.rb +4 -0
  108. data/test/support/active_support/test_case_extensions.rb +75 -0
  109. data/test/support/assertions.rb +73 -0
  110. data/test/support/factories.rb +64 -0
  111. data/test/support/generate.rb +29 -0
  112. data/test/support/mysql/import_examples.rb +98 -0
  113. data/test/support/postgresql/import_examples.rb +563 -0
  114. data/test/support/shared_examples/on_duplicate_key_ignore.rb +43 -0
  115. data/test/support/shared_examples/on_duplicate_key_update.rb +368 -0
  116. data/test/support/shared_examples/recursive_import.rb +216 -0
  117. data/test/support/sqlite3/import_examples.rb +231 -0
  118. data/test/synchronize_test.rb +41 -0
  119. data/test/test_helper.rb +75 -0
  120. data/test/travis/database.yml +66 -0
  121. data/test/value_sets_bytes_parser_test.rb +104 -0
  122. data/test/value_sets_records_parser_test.rb +32 -0
  123. metadata +259 -0
@@ -0,0 +1,13 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :alarms, force: true do |t|
3
+ t.column :device_id, :integer, null: false
4
+ t.column :alarm_type, :integer, null: false
5
+ t.column :status, :integer, null: false
6
+ t.column :metadata, :text
7
+ t.column :secret_key, :binary
8
+ t.datetime :created_at
9
+ t.datetime :updated_at
10
+ end
11
+
12
+ add_index :alarms, [:device_id, :alarm_type], unique: true, where: 'status <> 0'
13
+ end
@@ -0,0 +1,10 @@
1
+ class SchemaInfo < ActiveRecord::Base
2
+ if respond_to?(:table_name=)
3
+ self.table_name = 'schema_info'
4
+ else
5
+ # this is becoming deprecated in ActiveRecord but not all adapters supported it
6
+ # at this time
7
+ set_table_name 'schema_info'
8
+ end
9
+ VERSION = 12
10
+ end
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../support/sqlite3/import_examples')
3
+
4
+ should_support_sqlite3_import_functionality
@@ -0,0 +1,75 @@
1
+ class ActiveSupport::TestCase
2
+ include ActiveRecord::TestFixtures
3
+
4
+ if ENV['AR_VERSION'].to_f >= 5.0
5
+ self.use_transactional_tests = true
6
+ else
7
+ self.use_transactional_fixtures = true
8
+ end
9
+
10
+ class << self
11
+ def requires_active_record_version(version_string, &blk)
12
+ return unless Gem::Dependency.new('', version_string).match?('', ActiveRecord::VERSION::STRING)
13
+ instance_eval(&blk)
14
+ end
15
+
16
+ def assertion(name, &block)
17
+ mc = class << self; self; end
18
+ mc.class_eval do
19
+ define_method(name) do
20
+ it(name, &block)
21
+ end
22
+ end
23
+ end
24
+
25
+ def asssertion_group(name, &block)
26
+ mc = class << self; self; end
27
+ mc.class_eval do
28
+ define_method(name, &block)
29
+ end
30
+ end
31
+
32
+ def macro(name, &block)
33
+ class_eval do
34
+ define_method(name, &block)
35
+ end
36
+ end
37
+
38
+ def describe(description, toplevel = nil, &blk)
39
+ text = toplevel ? description : "#{name} #{description}"
40
+ klass = Class.new(self)
41
+
42
+ klass.class_eval <<-RUBY_EVAL
43
+ def self.name
44
+ "#{text}"
45
+ end
46
+ RUBY_EVAL
47
+
48
+ # do not inherit test methods from the superclass
49
+ klass.class_eval do
50
+ instance_methods.grep(/^test.+/) do |method|
51
+ undef_method method
52
+ end
53
+ end
54
+
55
+ klass.instance_eval(&blk)
56
+ end
57
+ alias context describe
58
+
59
+ def let(name, &blk)
60
+ define_method(name) do
61
+ instance_variable_name = "@__let_#{name}"
62
+ return instance_variable_get(instance_variable_name) if instance_variable_defined?(instance_variable_name)
63
+ instance_variable_set(instance_variable_name, instance_eval(&blk))
64
+ end
65
+ end
66
+
67
+ def it(description, &blk)
68
+ define_method("test_#{name}_#{description}", &blk)
69
+ end
70
+ end
71
+ end
72
+
73
+ def describe(description, &blk)
74
+ ActiveSupport::TestCase.describe(description, true, &blk)
75
+ end
@@ -0,0 +1,73 @@
1
+ class ActiveSupport::TestCase
2
+ module ImportAssertions
3
+ def self.extended(klass)
4
+ klass.instance_eval do
5
+ assertion(:should_not_update_created_at_on_timestamp_columns) do
6
+ Timecop.freeze Chronic.parse("5 minutes from now") do
7
+ perform_import
8
+ assert_in_delta @topic.created_at.to_i, updated_topic.created_at.to_i, 1
9
+ assert_in_delta @topic.created_on.to_i, updated_topic.created_on.to_i, 1
10
+ end
11
+ end
12
+
13
+ assertion(:should_update_updated_at_on_timestamp_columns) do
14
+ time = Chronic.parse("5 minutes from now")
15
+ Timecop.freeze time do
16
+ perform_import
17
+ assert_in_delta time.to_i, updated_topic.updated_at.to_i, 1
18
+ assert_in_delta time.to_i, updated_topic.updated_on.to_i, 1
19
+ end
20
+ end
21
+
22
+ assertion(:should_not_update_updated_at_on_timestamp_columns) do
23
+ time = Chronic.parse("5 minutes from now")
24
+ Timecop.freeze time do
25
+ perform_import
26
+ assert_in_delta @topic.updated_at.to_i, updated_topic.updated_at.to_i, 1
27
+ assert_in_delta @topic.updated_on.to_i, updated_topic.updated_on.to_i, 1
28
+ end
29
+ end
30
+
31
+ assertion(:should_not_update_timestamps) do
32
+ Timecop.freeze Chronic.parse("5 minutes from now") do
33
+ perform_import timestamps: false
34
+ assert_in_delta @topic.created_at.to_i, updated_topic.created_at.to_i, 1
35
+ assert_in_delta @topic.created_on.to_i, updated_topic.created_on.to_i, 1
36
+ assert_in_delta @topic.updated_at.to_i, updated_topic.updated_at.to_i, 1
37
+ assert_in_delta @topic.updated_on.to_i, updated_topic.updated_on.to_i, 1
38
+ end
39
+ end
40
+
41
+ assertion(:should_not_update_fields_not_mentioned) do
42
+ assert_equal "John Doe", updated_topic.author_name
43
+ end
44
+
45
+ assertion(:should_update_fields_mentioned) do
46
+ perform_import
47
+ assert_equal "Book - 2nd Edition", updated_topic.title
48
+ assert_equal "johndoe@example.com", updated_topic.author_email_address
49
+ end
50
+
51
+ assertion(:should_raise_update_fields_mentioned) do
52
+ assert_raise ActiveRecord::RecordNotUnique do
53
+ perform_import
54
+ end
55
+
56
+ assert_equal "Book", updated_topic.title
57
+ assert_equal "john@doe.com", updated_topic.author_email_address
58
+ end
59
+
60
+ assertion(:should_update_fields_mentioned_with_hash_mappings) do
61
+ perform_import
62
+ assert_equal "johndoe@example.com", updated_topic.title
63
+ assert_equal "Book - 2nd Edition", updated_topic.author_email_address
64
+ end
65
+
66
+ assertion(:should_update_foreign_keys) do
67
+ perform_import
68
+ assert_equal 57, updated_topic.parent_id
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,64 @@
1
+ FactoryBot.define do
2
+ sequence(:book_title) { |n| "Book #{n}" }
3
+ sequence(:chapter_title) { |n| "Chapter #{n}" }
4
+ sequence(:end_note) { |n| "Endnote #{n}" }
5
+
6
+ factory :group do
7
+ sequence(:order) { |n| "Order #{n}" }
8
+ end
9
+
10
+ factory :invalid_topic, class: "Topic" do
11
+ sequence(:title) { |n| "Title #{n}" }
12
+ author_name { nil }
13
+ end
14
+
15
+ factory :topic do
16
+ sequence(:title) { |n| "Title #{n}" }
17
+ sequence(:author_name) { |n| "Author #{n}" }
18
+ sequence(:content) { |n| "Content #{n}" }
19
+ end
20
+
21
+ factory :widget do
22
+ sequence(:w_id) { |n| n }
23
+ end
24
+
25
+ factory :question do
26
+ sequence(:body) { |n| "Text #{n}" }
27
+
28
+ trait :with_rule do
29
+ after(:build) do |question|
30
+ question.build_rule(FactoryBot.attributes_for(:rule))
31
+ end
32
+ end
33
+ end
34
+
35
+ factory :rule do
36
+ sequence(:id) { |n| n }
37
+ sequence(:condition_text) { |n| "q_#{n}_#{n}" }
38
+ end
39
+
40
+ factory :topic_with_book, parent: :topic do
41
+ after(:build) do |topic|
42
+ 2.times do
43
+ book = topic.books.build(title: FactoryBot.generate(:book_title), author_name: 'Stephen King')
44
+ 3.times do
45
+ book.chapters.build(title: FactoryBot.generate(:chapter_title))
46
+ end
47
+
48
+ 4.times do
49
+ book.end_notes.build(note: FactoryBot.generate(:end_note))
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ factory :book do
56
+ title { 'Tortilla Flat' }
57
+ author_name { 'John Steinbeck' }
58
+ end
59
+
60
+ factory :car do
61
+ sequence(:Name) { |n| n }
62
+ sequence(:Features) { |n| "Feature #{n}" }
63
+ end
64
+ end
@@ -0,0 +1,29 @@
1
+ class ActiveSupport::TestCase
2
+ def Build(*args) # rubocop:disable Style/MethodName
3
+ n = args.shift if args.first.is_a?(Numeric)
4
+ factory = args.shift
5
+ factory_bot_args = args.shift || {}
6
+
7
+ if n
8
+ [].tap do |collection|
9
+ n.times.each { collection << FactoryBot.build(factory.to_s.singularize.to_sym, factory_bot_args) }
10
+ end
11
+ else
12
+ FactoryBot.build(factory.to_s.singularize.to_sym, factory_bot_args)
13
+ end
14
+ end
15
+
16
+ def Generate(*args) # rubocop:disable Style/MethodName
17
+ n = args.shift if args.first.is_a?(Numeric)
18
+ factory = args.shift
19
+ factory_bot_args = args.shift || {}
20
+
21
+ if n
22
+ [].tap do |collection|
23
+ n.times.each { collection << FactoryBot.create(factory.to_s.singularize.to_sym, factory_bot_args) }
24
+ end
25
+ else
26
+ FactoryBot.create(factory.to_s.singularize.to_sym, factory_bot_args)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: UTF-8
2
+ def should_support_mysql_import_functionality
3
+ # Forcefully disable strict mode for this session.
4
+ ActiveRecord::Base.connection.execute "set sql_mode='STRICT_ALL_TABLES'"
5
+
6
+ should_support_basic_on_duplicate_key_update
7
+ should_support_on_duplicate_key_ignore
8
+
9
+ describe "#import" do
10
+ context "with :on_duplicate_key_update and validation checks turned off" do
11
+ extend ActiveSupport::TestCase::ImportAssertions
12
+
13
+ asssertion_group(:should_support_on_duplicate_key_update) do
14
+ should_not_update_fields_not_mentioned
15
+ should_update_foreign_keys
16
+ should_not_update_created_at_on_timestamp_columns
17
+ should_update_updated_at_on_timestamp_columns
18
+ end
19
+
20
+ macro(:perform_import) { raise "supply your own #perform_import in a context below" }
21
+ macro(:updated_topic) { Topic.find(@topic.id) }
22
+
23
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
24
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
25
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
26
+
27
+ macro(:perform_import) do |*opts|
28
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: update_columns, validate: false)
29
+ end
30
+
31
+ setup do
32
+ Topic.import columns, values, validate: false
33
+ @topic = Topic.find 99
34
+ end
35
+
36
+ context "using string hash map" do
37
+ let(:update_columns) { { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
38
+ should_support_on_duplicate_key_update
39
+ should_update_fields_mentioned
40
+ end
41
+
42
+ context "using string hash map, but specifying column mismatches" do
43
+ let(:update_columns) { { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } }
44
+ should_support_on_duplicate_key_update
45
+ should_update_fields_mentioned_with_hash_mappings
46
+ end
47
+
48
+ context "using symbol hash map" do
49
+ let(:update_columns) { { title: :title, author_email_address: :author_email_address, parent_id: :parent_id } }
50
+ should_support_on_duplicate_key_update
51
+ should_update_fields_mentioned
52
+ end
53
+
54
+ context "using symbol hash map, but specifying column mismatches" do
55
+ let(:update_columns) { { title: :author_email_address, author_email_address: :title, parent_id: :parent_id } }
56
+ should_support_on_duplicate_key_update
57
+ should_update_fields_mentioned_with_hash_mappings
58
+ end
59
+ end
60
+
61
+ context "with :synchronization option" do
62
+ let(:topics) { [] }
63
+ let(:values) { [[topics.first.id, "Jerry Carter", "title1"], [topics.last.id, "Chad Fowler", "title2"]] }
64
+ let(:columns) { %w(id author_name title) }
65
+
66
+ setup do
67
+ topics << Topic.create!(title: "LDAP", author_name: "Big Bird", content: "Putting Directories to Work.")
68
+ topics << Topic.create!(title: "Rails Recipes", author_name: "Elmo", content: "A trusted collection of solutions.")
69
+ end
70
+
71
+ it "synchronizes passed in ActiveRecord model instances with the data just imported" do
72
+ columns2update = ['author_name']
73
+
74
+ expected_count = Topic.count
75
+ Topic.import( columns, values,
76
+ validate: false,
77
+ on_duplicate_key_update: columns2update,
78
+ synchronize: topics )
79
+
80
+ assert_equal expected_count, Topic.count, "no new records should have been created!"
81
+ assert_equal "Jerry Carter", topics.first.author_name, "wrong author!"
82
+ assert_equal "Chad Fowler", topics.last.author_name, "wrong author!"
83
+ end
84
+ end
85
+
86
+ if ENV['AR_VERSION'].to_f >= 5.1
87
+ context "with virtual columns" do
88
+ let(:books) { [Book.new(author_name: "foo", title: "bar")] }
89
+
90
+ it "ignores virtual columns and creates record" do
91
+ assert_difference "Book.count", +1 do
92
+ Book.import books
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,563 @@
1
+ # encoding: UTF-8
2
+ def should_support_postgresql_import_functionality
3
+ should_support_recursive_import
4
+
5
+ if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
6
+ should_support_postgresql_upsert_functionality
7
+ end
8
+
9
+ describe "#supports_imports?" do
10
+ it "should support import" do
11
+ assert ActiveRecord::Base.supports_import?
12
+ end
13
+ end
14
+
15
+ describe "#import" do
16
+ it "should import with a single insert" do
17
+ # see ActiveRecord::ConnectionAdapters::AbstractAdapter test for more specifics
18
+ assert_difference "Topic.count", +10 do
19
+ result = Topic.import Build(3, :topics)
20
+ assert_equal 1, result.num_inserts
21
+
22
+ result = Topic.import Build(7, :topics)
23
+ assert_equal 1, result.num_inserts
24
+ end
25
+ end
26
+
27
+ context "setting attributes and marking clean" do
28
+ let(:topic) { Build(:topics) }
29
+
30
+ setup { Topic.import([topic]) }
31
+
32
+ it "assigns ids" do
33
+ assert topic.id.present?
34
+ end
35
+
36
+ it "marks models as clean" do
37
+ assert !topic.changed?
38
+ end
39
+
40
+ if ENV['AR_VERSION'].to_f > 4.1
41
+ it "moves the dirty changes to previous_changes" do
42
+ assert topic.previous_changes.present?
43
+ end
44
+ end
45
+
46
+ it "marks models as persisted" do
47
+ assert !topic.new_record?
48
+ assert topic.persisted?
49
+ end
50
+
51
+ it "assigns timestamps" do
52
+ assert topic.created_at.present?
53
+ assert topic.updated_at.present?
54
+ end
55
+ end
56
+
57
+ describe "with query cache enabled" do
58
+ setup do
59
+ unless ActiveRecord::Base.connection.query_cache_enabled
60
+ ActiveRecord::Base.connection.enable_query_cache!
61
+ @disable_cache_on_teardown = true
62
+ end
63
+ end
64
+
65
+ it "clears cache on insert" do
66
+ before_import = Topic.all.to_a
67
+
68
+ Topic.import(Build(2, :topics), validate: false)
69
+
70
+ after_import = Topic.all.to_a
71
+ assert_equal 2, after_import.size - before_import.size
72
+ end
73
+
74
+ teardown do
75
+ if @disable_cache_on_teardown
76
+ ActiveRecord::Base.connection.disable_query_cache!
77
+ end
78
+ end
79
+ end
80
+
81
+ describe "no_returning" do
82
+ let(:books) { [Book.new(author_name: "foo", title: "bar")] }
83
+
84
+ it "creates records" do
85
+ assert_difference "Book.count", +1 do
86
+ Book.import books, no_returning: true
87
+ end
88
+ end
89
+
90
+ it "returns no ids" do
91
+ assert_equal [], Book.import(books, no_returning: true).ids
92
+ end
93
+ end
94
+
95
+ describe "returning" do
96
+ let(:books) { [Book.new(author_name: "King", title: "It")] }
97
+ let(:result) { Book.import(books, returning: %w(author_name title)) }
98
+ let(:book_id) do
99
+ if RUBY_PLATFORM == 'java' || ENV['AR_VERSION'].to_i >= 5.0
100
+ books.first.id
101
+ else
102
+ books.first.id.to_s
103
+ end
104
+ end
105
+
106
+ it "creates records" do
107
+ assert_difference("Book.count", +1) { result }
108
+ end
109
+
110
+ it "returns ids" do
111
+ result
112
+ assert_equal [book_id], result.ids
113
+ end
114
+
115
+ it "returns specified columns" do
116
+ assert_equal [%w(King It)], result.results
117
+ end
118
+
119
+ context "when given an empty array" do
120
+ let(:result) { Book.import([], returning: %w(title)) }
121
+
122
+ setup { result }
123
+
124
+ it "returns empty arrays for ids and results" do
125
+ assert_equal [], result.ids
126
+ assert_equal [], result.results
127
+ end
128
+ end
129
+
130
+ context "when primary key and returning overlap" do
131
+ let(:result) { Book.import(books, returning: %w(id title)) }
132
+
133
+ setup { result }
134
+
135
+ it "returns ids" do
136
+ assert_equal [book_id], result.ids
137
+ end
138
+
139
+ it "returns specified columns" do
140
+ assert_equal [[book_id, 'It']], result.results
141
+ end
142
+ end
143
+
144
+ context "setting model attributes" do
145
+ let(:code) { 'abc' }
146
+ let(:discount) { 0.10 }
147
+ let(:original_promotion) do
148
+ Promotion.new(code: code, discount: discount)
149
+ end
150
+ let(:updated_promotion) do
151
+ Promotion.new(code: code, description: 'ABC discount')
152
+ end
153
+ let(:returning_columns) { %w(discount) }
154
+
155
+ setup do
156
+ Promotion.import([original_promotion])
157
+ Promotion.import([updated_promotion],
158
+ on_duplicate_key_update: { conflict_target: %i(code), columns: %i(description) },
159
+ returning: returning_columns)
160
+ end
161
+
162
+ it "sets model attributes" do
163
+ assert_equal updated_promotion.discount, discount
164
+ end
165
+
166
+ context "returning multiple columns" do
167
+ let(:returning_columns) { %w(discount description) }
168
+
169
+ it "sets model attributes" do
170
+ assert_equal updated_promotion.discount, discount
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ if ENV['AR_VERSION'].to_f >= 4.0
178
+ describe "with a uuid primary key" do
179
+ let(:vendor) { Vendor.new(name: "foo") }
180
+ let(:vendors) { [vendor] }
181
+
182
+ it "creates records" do
183
+ assert_difference "Vendor.count", +1 do
184
+ Vendor.import vendors
185
+ end
186
+ end
187
+
188
+ it "assigns an id to the model objects" do
189
+ Vendor.import vendors
190
+ assert_not_nil vendor.id
191
+ end
192
+ end
193
+
194
+ describe "with an assigned uuid primary key" do
195
+ let(:id) { SecureRandom.uuid }
196
+ let(:vendor) { Vendor.new(id: id, name: "foo") }
197
+ let(:vendors) { [vendor] }
198
+
199
+ it "creates records with correct id" do
200
+ assert_difference "Vendor.count", +1 do
201
+ Vendor.import vendors
202
+ end
203
+ assert_equal id, vendor.id
204
+ end
205
+ end
206
+ end
207
+
208
+ describe "with store accessor fields" do
209
+ if ENV['AR_VERSION'].to_f >= 4.0
210
+ it "imports values for json fields" do
211
+ vendors = [Vendor.new(name: 'Vendor 1', size: 100)]
212
+ assert_difference "Vendor.count", +1 do
213
+ Vendor.import vendors
214
+ end
215
+ assert_equal(100, Vendor.first.size)
216
+ end
217
+
218
+ it "imports values for hstore fields" do
219
+ vendors = [Vendor.new(name: 'Vendor 1', contact: 'John Smith')]
220
+ assert_difference "Vendor.count", +1 do
221
+ Vendor.import vendors
222
+ end
223
+ assert_equal('John Smith', Vendor.first.contact)
224
+ end
225
+ end
226
+
227
+ if ENV['AR_VERSION'].to_f >= 4.2
228
+ it "imports values for jsonb fields" do
229
+ vendors = [Vendor.new(name: 'Vendor 1', charge_code: '12345')]
230
+ assert_difference "Vendor.count", +1 do
231
+ Vendor.import vendors
232
+ end
233
+ assert_equal('12345', Vendor.first.charge_code)
234
+ end
235
+ end
236
+ end
237
+
238
+ if ENV['AR_VERSION'].to_f >= 4.2
239
+ describe "with serializable fields" do
240
+ it "imports default values as correct data type" do
241
+ vendors = [Vendor.new(name: 'Vendor 1')]
242
+ assert_difference "Vendor.count", +1 do
243
+ Vendor.import vendors
244
+ end
245
+ assert_equal({}, Vendor.first.json_data)
246
+ end
247
+ end
248
+
249
+ %w(json jsonb).each do |json_type|
250
+ describe "with pure #{json_type} fields" do
251
+ let(:data) { { a: :b } }
252
+ let(:json_field_name) { "pure_#{json_type}_data" }
253
+ it "imports the values from saved records" do
254
+ vendor = Vendor.create!(name: 'Vendor 1', json_field_name => data)
255
+
256
+ Vendor.import [vendor], on_duplicate_key_update: [json_field_name]
257
+ assert_equal(data.as_json, vendor.reload[json_field_name])
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ describe "with binary field" do
264
+ let(:binary_value) { "\xE0'c\xB2\xB0\xB3Bh\\\xC2M\xB1m\\I\xC4r".force_encoding('ASCII-8BIT') }
265
+ it "imports the correct values for binary fields" do
266
+ alarms = [Alarm.new(device_id: 1, alarm_type: 1, status: 1, secret_key: binary_value)]
267
+ assert_difference "Alarm.count", +1 do
268
+ Alarm.import alarms
269
+ end
270
+ assert_equal(binary_value, Alarm.first.secret_key)
271
+ end
272
+ end
273
+ end
274
+
275
+ def should_support_postgresql_upsert_functionality
276
+ should_support_basic_on_duplicate_key_update
277
+ should_support_on_duplicate_key_ignore
278
+
279
+ describe "#import" do
280
+ extend ActiveSupport::TestCase::ImportAssertions
281
+
282
+ macro(:perform_import) { raise "supply your own #perform_import in a context below" }
283
+ macro(:updated_topic) { Topic.find(@topic.id) }
284
+
285
+ context "with :on_duplicate_key_ignore and validation checks turned off" do
286
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
287
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
288
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
289
+
290
+ setup do
291
+ Topic.import columns, values, validate: false
292
+ end
293
+
294
+ it "should not update any records" do
295
+ result = Topic.import columns, updated_values, on_duplicate_key_ignore: true, validate: false
296
+ assert_equal [], result.ids
297
+ end
298
+ end
299
+
300
+ context "with :on_duplicate_key_ignore and :recursive enabled" do
301
+ let(:new_topic) { Build(1, :topic_with_book) }
302
+ let(:mixed_topics) { Build(1, :topic_with_book) + new_topic + Build(1, :topic_with_book) }
303
+
304
+ setup do
305
+ Topic.import new_topic, recursive: true
306
+ end
307
+
308
+ # Recursive import depends on the primary keys of the parent model being returned
309
+ # on insert. With on_duplicate_key_ignore enabled, not all ids will be returned
310
+ # and it is possible that a model will be assigned the wrong id and then its children
311
+ # would be associated with the wrong parent.
312
+ it ":on_duplicate_key_ignore is ignored" do
313
+ assert_raise ActiveRecord::RecordNotUnique do
314
+ Topic.import mixed_topics, recursive: true, on_duplicate_key_ignore: true, validate: false
315
+ end
316
+ end
317
+ end
318
+
319
+ context "with :on_duplicate_key_update and validation checks turned off" do
320
+ asssertion_group(:should_support_on_duplicate_key_update) do
321
+ should_not_update_fields_not_mentioned
322
+ should_update_foreign_keys
323
+ should_not_update_created_at_on_timestamp_columns
324
+ should_update_updated_at_on_timestamp_columns
325
+ end
326
+
327
+ context "using a hash" do
328
+ context "with :columns :all" do
329
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
330
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
331
+
332
+ macro(:perform_import) do |*opts|
333
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: :all }, validate: false)
334
+ end
335
+
336
+ setup do
337
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
338
+ Topic.import columns + ['replies_count'], values, validate: false
339
+ end
340
+
341
+ it "should update all specified columns" do
342
+ perform_import
343
+ updated_topic = Topic.find(99)
344
+ assert_equal 'Book - 2nd Edition', updated_topic.title
345
+ assert_equal 'Jane Doe', updated_topic.author_name
346
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
347
+ assert_equal 57, updated_topic.parent_id
348
+ assert_equal 3, updated_topic.replies_count
349
+ end
350
+ end
351
+
352
+ context "with :columns a hash" do
353
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
354
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
355
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
356
+
357
+ macro(:perform_import) do |*opts|
358
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: update_columns }, validate: false)
359
+ end
360
+
361
+ setup do
362
+ Topic.import columns, values, validate: false
363
+ @topic = Topic.find 99
364
+ end
365
+
366
+ it "should not modify the passed in :on_duplicate_key_update columns array" do
367
+ assert_nothing_raised do
368
+ columns = %w(title author_name).freeze
369
+ Topic.import columns, [%w(foo, bar)], { on_duplicate_key_update: { columns: columns }.freeze }.freeze
370
+ end
371
+ end
372
+
373
+ context "using string hash map" do
374
+ let(:update_columns) { { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
375
+ should_support_on_duplicate_key_update
376
+ should_update_fields_mentioned
377
+ end
378
+
379
+ context "using string hash map, but specifying column mismatches" do
380
+ let(:update_columns) { { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } }
381
+ should_support_on_duplicate_key_update
382
+ should_update_fields_mentioned_with_hash_mappings
383
+ end
384
+
385
+ context "using symbol hash map" do
386
+ let(:update_columns) { { title: :title, author_email_address: :author_email_address, parent_id: :parent_id } }
387
+ should_support_on_duplicate_key_update
388
+ should_update_fields_mentioned
389
+ end
390
+
391
+ context "using symbol hash map, but specifying column mismatches" do
392
+ let(:update_columns) { { title: :author_email_address, author_email_address: :title, parent_id: :parent_id } }
393
+ should_support_on_duplicate_key_update
394
+ should_update_fields_mentioned_with_hash_mappings
395
+ end
396
+ end
397
+
398
+ context 'with :index_predicate' do
399
+ let(:columns) { %w( id device_id alarm_type status metadata ) }
400
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
401
+ let(:updated_values) { [[99, 17, 1, 2, 'bar']] }
402
+
403
+ macro(:perform_import) do |*opts|
404
+ 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)
405
+ end
406
+
407
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
408
+
409
+ setup do
410
+ Alarm.import columns, values, validate: false
411
+ @alarm = Alarm.find 99
412
+ end
413
+
414
+ context 'supports on duplicate key update for partial indexes' do
415
+ it 'should not update created_at timestamp columns' do
416
+ Timecop.freeze Chronic.parse("5 minutes from now") do
417
+ perform_import
418
+ assert_in_delta @alarm.created_at.to_i, updated_alarm.created_at.to_i, 1
419
+ end
420
+ end
421
+
422
+ it 'should update updated_at timestamp columns' do
423
+ time = Chronic.parse("5 minutes from now")
424
+ Timecop.freeze time do
425
+ perform_import
426
+ assert_in_delta time.to_i, updated_alarm.updated_at.to_i, 1
427
+ end
428
+ end
429
+
430
+ it 'should not update fields not mentioned' do
431
+ perform_import
432
+ assert_equal 'foo', updated_alarm.metadata
433
+ end
434
+
435
+ it 'should update fields mentioned with hash mappings' do
436
+ perform_import
437
+ assert_equal 2, updated_alarm.status
438
+ end
439
+ end
440
+ end
441
+
442
+ context 'with :condition' do
443
+ let(:columns) { %w( id device_id alarm_type status metadata) }
444
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
445
+ let(:updated_values) { [[99, 17, 1, 1, 'bar']] }
446
+
447
+ macro(:perform_import) do |*opts|
448
+ Alarm.import(
449
+ columns,
450
+ updated_values,
451
+ opts.extract_options!.merge(
452
+ on_duplicate_key_update: {
453
+ conflict_target: [:id],
454
+ condition: "alarms.metadata NOT LIKE '%foo%'",
455
+ columns: [:metadata]
456
+ },
457
+ validate: false
458
+ )
459
+ )
460
+ end
461
+
462
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
463
+
464
+ setup do
465
+ Alarm.import columns, values, validate: false
466
+ @alarm = Alarm.find 99
467
+ end
468
+
469
+ it 'should not update fields not matched' do
470
+ perform_import
471
+ assert_equal 'foo', updated_alarm.metadata
472
+ end
473
+ end
474
+
475
+ context "with :constraint_name" do
476
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
477
+ let(:values) { [[100, "Book", "John Doe", "john@doe.com", 17]] }
478
+ let(:updated_values) { [[100, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
479
+
480
+ macro(:perform_import) do |*opts|
481
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { constraint_name: :topics_pkey, columns: update_columns }, validate: false)
482
+ end
483
+
484
+ setup do
485
+ Topic.import columns, values, validate: false
486
+ @topic = Topic.find 100
487
+ end
488
+
489
+ let(:update_columns) { [:title, :author_email_address, :parent_id] }
490
+ should_support_on_duplicate_key_update
491
+ should_update_fields_mentioned
492
+ end
493
+
494
+ context "default to the primary key" do
495
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
496
+ let(:values) { [[100, "Book", "John Doe", "john@doe.com", 17]] }
497
+ let(:updated_values) { [[100, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
498
+ let(:update_columns) { [:title, :author_email_address, :parent_id] }
499
+
500
+ setup do
501
+ Topic.import columns, values, validate: false
502
+ @topic = Topic.find 100
503
+ end
504
+
505
+ context "with no :conflict_target or :constraint_name" do
506
+ macro(:perform_import) do |*opts|
507
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { columns: update_columns }, validate: false)
508
+ end
509
+
510
+ should_support_on_duplicate_key_update
511
+ should_update_fields_mentioned
512
+ end
513
+
514
+ context "with empty value for :conflict_target" do
515
+ macro(:perform_import) do |*opts|
516
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: [], columns: update_columns }, validate: false)
517
+ end
518
+
519
+ should_support_on_duplicate_key_update
520
+ should_update_fields_mentioned
521
+ end
522
+
523
+ context "with empty value for :constraint_name" do
524
+ macro(:perform_import) do |*opts|
525
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { constraint_name: '', columns: update_columns }, validate: false)
526
+ end
527
+
528
+ should_support_on_duplicate_key_update
529
+ should_update_fields_mentioned
530
+ end
531
+ end
532
+
533
+ context "with no :conflict_target or :constraint_name" do
534
+ context "with no primary key" do
535
+ it "raises ArgumentError" do
536
+ error = assert_raises ArgumentError do
537
+ Rule.import Build(3, :rules), on_duplicate_key_update: [:condition_text], validate: false
538
+ end
539
+ assert_match(/Expected :conflict_target or :constraint_name to be specified/, error.message)
540
+ end
541
+ end
542
+ end
543
+
544
+ context "with no :columns" do
545
+ let(:columns) { %w( id title author_name author_email_address ) }
546
+ let(:values) { [[100, "Book", "John Doe", "john@doe.com"]] }
547
+ let(:updated_values) { [[100, "Title Should Not Change", "Author Should Not Change", "john@nogo.com"]] }
548
+
549
+ macro(:perform_import) do |*opts|
550
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id }, validate: false)
551
+ end
552
+
553
+ setup do
554
+ Topic.import columns, values, validate: false
555
+ @topic = Topic.find 100
556
+ end
557
+
558
+ should_update_updated_at_on_timestamp_columns
559
+ end
560
+ end
561
+ end
562
+ end
563
+ end