activerecord-import 0.19.0 → 1.0.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.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +22 -12
  3. data/CHANGELOG.md +166 -0
  4. data/Gemfile +13 -10
  5. data/README.markdown +548 -5
  6. data/Rakefile +2 -1
  7. data/benchmarks/lib/cli_parser.rb +2 -1
  8. data/gemfiles/5.1.gemfile +1 -0
  9. data/gemfiles/5.2.gemfile +2 -0
  10. data/lib/activerecord-import/adapters/abstract_adapter.rb +2 -2
  11. data/lib/activerecord-import/adapters/mysql_adapter.rb +16 -10
  12. data/lib/activerecord-import/adapters/postgresql_adapter.rb +59 -15
  13. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +126 -3
  14. data/lib/activerecord-import/base.rb +4 -6
  15. data/lib/activerecord-import/import.rb +384 -126
  16. data/lib/activerecord-import/synchronize.rb +1 -1
  17. data/lib/activerecord-import/value_sets_parser.rb +14 -0
  18. data/lib/activerecord-import/version.rb +1 -1
  19. data/lib/activerecord-import.rb +2 -15
  20. data/test/adapters/makara_postgis.rb +1 -0
  21. data/test/import_test.rb +148 -14
  22. data/test/makara_postgis/import_test.rb +8 -0
  23. data/test/models/account.rb +3 -0
  24. data/test/models/bike_maker.rb +7 -0
  25. data/test/models/topic.rb +10 -0
  26. data/test/models/user.rb +3 -0
  27. data/test/models/user_token.rb +4 -0
  28. data/test/schema/generic_schema.rb +20 -0
  29. data/test/schema/mysql2_schema.rb +19 -0
  30. data/test/schema/postgresql_schema.rb +1 -0
  31. data/test/schema/sqlite3_schema.rb +13 -0
  32. data/test/support/factories.rb +9 -8
  33. data/test/support/generate.rb +6 -6
  34. data/test/support/mysql/import_examples.rb +14 -2
  35. data/test/support/postgresql/import_examples.rb +142 -0
  36. data/test/support/shared_examples/on_duplicate_key_update.rb +252 -1
  37. data/test/support/shared_examples/recursive_import.rb +41 -11
  38. data/test/support/sqlite3/import_examples.rb +187 -10
  39. data/test/synchronize_test.rb +8 -0
  40. data/test/test_helper.rb +9 -1
  41. data/test/value_sets_bytes_parser_test.rb +13 -2
  42. metadata +20 -5
  43. data/test/schema/mysql_schema.rb +0 -16
@@ -1,6 +1,8 @@
1
1
  # encoding: UTF-8
2
2
  def should_support_sqlite3_import_functionality
3
- should_support_on_duplicate_key_ignore
3
+ if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
4
+ should_support_sqlite_upsert_functionality
5
+ end
4
6
 
5
7
  describe "#supports_imports?" do
6
8
  context "and SQLite is 3.7.11 or higher" do
@@ -49,18 +51,193 @@ def should_support_sqlite3_import_functionality
49
51
  assert_equal 2500, Topic.count, "Failed to insert all records. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed"
50
52
  end
51
53
  end
54
+ end
55
+ end
56
+
57
+ def should_support_sqlite_upsert_functionality
58
+ should_support_basic_on_duplicate_key_update
59
+ should_support_on_duplicate_key_ignore
60
+
61
+ describe "#import" do
62
+ extend ActiveSupport::TestCase::ImportAssertions
63
+
64
+ macro(:perform_import) { raise "supply your own #perform_import in a context below" }
65
+ macro(:updated_topic) { Topic.find(@topic.id) }
66
+
67
+ context "with :on_duplicate_key_ignore and validation checks turned off" do
68
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
69
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
70
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
71
+
72
+ setup do
73
+ Topic.import columns, values, validate: false
74
+ end
75
+
76
+ it "should not update any records" do
77
+ result = Topic.import columns, updated_values, on_duplicate_key_ignore: true, validate: false
78
+ assert_equal [], result.ids
79
+ end
80
+ end
81
+
82
+ context "with :on_duplicate_key_update and validation checks turned off" do
83
+ asssertion_group(:should_support_on_duplicate_key_update) do
84
+ should_not_update_fields_not_mentioned
85
+ should_update_foreign_keys
86
+ should_not_update_created_at_on_timestamp_columns
87
+ should_update_updated_at_on_timestamp_columns
88
+ end
89
+
90
+ context "using a hash" do
91
+ context "with :columns a hash" do
92
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
93
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
94
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
95
+
96
+ macro(:perform_import) do |*opts|
97
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: update_columns }, validate: false)
98
+ end
99
+
100
+ setup do
101
+ Topic.import columns, values, validate: false
102
+ @topic = Topic.find 99
103
+ end
104
+
105
+ it "should not modify the passed in :on_duplicate_key_update columns array" do
106
+ assert_nothing_raised do
107
+ columns = %w(title author_name).freeze
108
+ Topic.import columns, [%w(foo, bar)], on_duplicate_key_update: { columns: columns }
109
+ end
110
+ end
111
+
112
+ context "using string hash map" do
113
+ let(:update_columns) { { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
114
+ should_support_on_duplicate_key_update
115
+ should_update_fields_mentioned
116
+ end
117
+
118
+ context "using string hash map, but specifying column mismatches" do
119
+ let(:update_columns) { { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } }
120
+ should_support_on_duplicate_key_update
121
+ should_update_fields_mentioned_with_hash_mappings
122
+ end
123
+
124
+ context "using symbol hash map" do
125
+ let(:update_columns) { { title: :title, author_email_address: :author_email_address, parent_id: :parent_id } }
126
+ should_support_on_duplicate_key_update
127
+ should_update_fields_mentioned
128
+ end
129
+
130
+ context "using symbol hash map, but specifying column mismatches" do
131
+ let(:update_columns) { { title: :author_email_address, author_email_address: :title, parent_id: :parent_id } }
132
+ should_support_on_duplicate_key_update
133
+ should_update_fields_mentioned_with_hash_mappings
134
+ end
135
+ end
136
+
137
+ context 'with :index_predicate' do
138
+ let(:columns) { %w( id device_id alarm_type status metadata ) }
139
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
140
+ let(:updated_values) { [[99, 17, 1, 2, 'bar']] }
141
+
142
+ macro(:perform_import) do |*opts|
143
+ 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)
144
+ end
145
+
146
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
147
+
148
+ setup do
149
+ Alarm.import columns, values, validate: false
150
+ @alarm = Alarm.find 99
151
+ end
152
+
153
+ context 'supports on duplicate key update for partial indexes' do
154
+ it 'should not update created_at timestamp columns' do
155
+ Timecop.freeze Chronic.parse("5 minutes from now") do
156
+ perform_import
157
+ assert_in_delta @alarm.created_at.to_i, updated_alarm.created_at.to_i, 1
158
+ end
159
+ end
160
+
161
+ it 'should update updated_at timestamp columns' do
162
+ time = Chronic.parse("5 minutes from now")
163
+ Timecop.freeze time do
164
+ perform_import
165
+ assert_in_delta time.to_i, updated_alarm.updated_at.to_i, 1
166
+ end
167
+ end
168
+
169
+ it 'should not update fields not mentioned' do
170
+ perform_import
171
+ assert_equal 'foo', updated_alarm.metadata
172
+ end
173
+
174
+ it 'should update fields mentioned with hash mappings' do
175
+ perform_import
176
+ assert_equal 2, updated_alarm.status
177
+ end
178
+ end
179
+ end
180
+
181
+ context 'with :condition' do
182
+ let(:columns) { %w( id device_id alarm_type status metadata) }
183
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
184
+ let(:updated_values) { [[99, 17, 1, 1, 'bar']] }
185
+
186
+ macro(:perform_import) do |*opts|
187
+ Alarm.import(
188
+ columns,
189
+ updated_values,
190
+ opts.extract_options!.merge(
191
+ on_duplicate_key_update: {
192
+ conflict_target: [:id],
193
+ condition: "alarms.metadata NOT LIKE '%foo%'",
194
+ columns: [:metadata]
195
+ },
196
+ validate: false
197
+ )
198
+ )
199
+ end
200
+
201
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
202
+
203
+ setup do
204
+ Alarm.import columns, values, validate: false
205
+ @alarm = Alarm.find 99
206
+ end
207
+
208
+ it 'should not update fields not matched' do
209
+ perform_import
210
+ assert_equal 'foo', updated_alarm.metadata
211
+ end
212
+ end
213
+
214
+ context "with no :conflict_target" do
215
+ context "with no primary key" do
216
+ it "raises ArgumentError" do
217
+ error = assert_raises ArgumentError do
218
+ Rule.import Build(3, :rules), on_duplicate_key_update: [:condition_text], validate: false
219
+ end
220
+ assert_match(/Expected :conflict_target to be specified/, error.message)
221
+ end
222
+ end
223
+ end
224
+
225
+ context "with no :columns" do
226
+ let(:columns) { %w( id title author_name author_email_address ) }
227
+ let(:values) { [[100, "Book", "John Doe", "john@doe.com"]] }
228
+ let(:updated_values) { [[100, "Title Should Not Change", "Author Should Not Change", "john@nogo.com"]] }
52
229
 
53
- context "with :on_duplicate_key_update" do
54
- let(:topics) { Build(1, :topics) }
230
+ macro(:perform_import) do |*opts|
231
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id }, validate: false)
232
+ end
55
233
 
56
- it "should log a warning message" do
57
- log = StringIO.new
58
- logger = Logger.new(log)
59
- logger.level = Logger::WARN
60
- ActiveRecord::Base.connection.stubs(:logger).returns(logger)
234
+ setup do
235
+ Topic.import columns, values, validate: false
236
+ @topic = Topic.find 100
237
+ end
61
238
 
62
- Topic.import topics, on_duplicate_key_update: true
63
- assert_match(/Ignoring on_duplicate_key_update/, log.string)
239
+ should_update_updated_at_on_timestamp_columns
240
+ end
64
241
  end
65
242
  end
66
243
  end
@@ -30,4 +30,12 @@ describe ".synchronize" do
30
30
  assert_equal false, topics[1].changed?, "the second record was dirty"
31
31
  assert_equal false, topics[2].changed?, "the third record was dirty"
32
32
  end
33
+
34
+ it "ignores default scope" do
35
+ # update records outside of ActiveRecord knowing about it
36
+ Topic.connection.execute( "UPDATE #{Topic.table_name} SET approved='0' WHERE id=#{topics[0].id}", "Updating record 1 without ActiveRecord" )
37
+
38
+ Topic.synchronize topics
39
+ assert_equal false, topics[0].approved
40
+ end
33
41
  end
data/test/test_helper.rb CHANGED
@@ -33,6 +33,14 @@ rescue LoadError
33
33
  ENV["SKIP_COMPOSITE_PK"] = "true"
34
34
  end
35
35
 
36
+ # Support MySQL 5.7
37
+ if ActiveSupport::VERSION::STRING < "4.1"
38
+ require "active_record/connection_adapters/mysql2_adapter"
39
+ class ActiveRecord::ConnectionAdapters::Mysql2Adapter
40
+ NATIVE_DATABASE_TYPES[:primary_key] = "int(11) auto_increment PRIMARY KEY"
41
+ end
42
+ end
43
+
36
44
  require "ruby-debug" if RUBY_VERSION.to_f < 1.9
37
45
 
38
46
  adapter = ENV["ARE_DB"] || "sqlite3"
@@ -50,7 +58,7 @@ ActiveSupport::Notifications.subscribe(/active_record.sql/) do |_, _, _, _, hsh|
50
58
  ActiveRecord::Base.logger.info hsh[:sql]
51
59
  end
52
60
 
53
- require "factory_girl"
61
+ require "factory_bot"
54
62
  Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each { |file| require file }
55
63
 
56
64
  # Load base/generic schema
@@ -8,6 +8,15 @@ describe ActiveRecord::Import::ValueSetsBytesParser do
8
8
  let(:base_sql) { "INSERT INTO atable (a,b,c)" }
9
9
  let(:values) { ["(1,2,3)", "(2,3,4)", "(3,4,5)"] }
10
10
 
11
+ context "when the max allowed bytes is 30 and the base SQL is 26 bytes" do
12
+ it "should raise ActiveRecord::Import::ValueSetTooLargeError" do
13
+ error = assert_raises ActiveRecord::Import::ValueSetTooLargeError do
14
+ parser.parse values, reserved_bytes: base_sql.size, max_bytes: 30
15
+ end
16
+ assert_match(/33 bytes exceeds the max allowed for an insert \[30\]/, error.message)
17
+ end
18
+ end
19
+
11
20
  context "when the max allowed bytes is 33 and the base SQL is 26 bytes" do
12
21
  it "should return 3 value sets when given 3 value sets of 7 bytes a piece" do
13
22
  value_sets = parser.parse values, reserved_bytes: base_sql.size, max_bytes: 33
@@ -54,7 +63,8 @@ describe ActiveRecord::Import::ValueSetsBytesParser do
54
63
  values = [
55
64
  "('1','2','3')",
56
65
  "('4','5','6')",
57
- "('7','8','9')"]
66
+ "('7','8','9')"
67
+ ]
58
68
 
59
69
  base_sql_size_in_bytes = 15
60
70
  max_bytes = 30
@@ -79,7 +89,8 @@ describe ActiveRecord::Import::ValueSetsBytesParser do
79
89
  # each accented e should be 2 bytes, so each entry is 6 bytes instead of 5
80
90
  values = [
81
91
  "('é')",
82
- "('é')"]
92
+ "('é')"
93
+ ]
83
94
 
84
95
  base_sql_size_in_bytes = 15
85
96
  max_bytes = 26
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.19.0
4
+ version: 1.0.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: 2017-06-13 00:00:00.000000000 Z
11
+ date: 2019-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -74,6 +74,7 @@ files:
74
74
  - gemfiles/4.2.gemfile
75
75
  - gemfiles/5.0.gemfile
76
76
  - gemfiles/5.1.gemfile
77
+ - gemfiles/5.2.gemfile
77
78
  - lib/activerecord-import.rb
78
79
  - lib/activerecord-import/active_record/adapters/abstract_adapter.rb
79
80
  - lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb
@@ -100,6 +101,7 @@ files:
100
101
  - test/adapters/jdbcmysql.rb
101
102
  - test/adapters/jdbcpostgresql.rb
102
103
  - test/adapters/jdbcsqlite3.rb
104
+ - test/adapters/makara_postgis.rb
103
105
  - test/adapters/mysql2.rb
104
106
  - test/adapters/mysql2_makara.rb
105
107
  - test/adapters/mysql2spatial.rb
@@ -114,7 +116,10 @@ files:
114
116
  - test/jdbcmysql/import_test.rb
115
117
  - test/jdbcpostgresql/import_test.rb
116
118
  - test/jdbcsqlite3/import_test.rb
119
+ - test/makara_postgis/import_test.rb
120
+ - test/models/account.rb
117
121
  - test/models/alarm.rb
122
+ - test/models/bike_maker.rb
118
123
  - test/models/book.rb
119
124
  - test/models/car.rb
120
125
  - test/models/chapter.rb
@@ -127,6 +132,8 @@ files:
127
132
  - test/models/rule.rb
128
133
  - test/models/tag.rb
129
134
  - test/models/topic.rb
135
+ - test/models/user.rb
136
+ - test/models/user_token.rb
130
137
  - test/models/vendor.rb
131
138
  - test/models/widget.rb
132
139
  - test/mysql2/import_test.rb
@@ -136,9 +143,10 @@ files:
136
143
  - test/postgresql/import_test.rb
137
144
  - test/schema/generic_schema.rb
138
145
  - test/schema/jdbcpostgresql_schema.rb
139
- - test/schema/mysql_schema.rb
146
+ - test/schema/mysql2_schema.rb
140
147
  - test/schema/postgis_schema.rb
141
148
  - test/schema/postgresql_schema.rb
149
+ - test/schema/sqlite3_schema.rb
142
150
  - test/schema/version.rb
143
151
  - test/sqlite3/import_test.rb
144
152
  - test/support/active_support/test_case_extensions.rb
@@ -176,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
184
  version: '0'
177
185
  requirements: []
178
186
  rubyforge_project:
179
- rubygems_version: 2.6.11
187
+ rubygems_version: 2.7.7
180
188
  signing_key:
181
189
  specification_version: 4
182
190
  summary: Bulk insert extension for ActiveRecord
@@ -184,6 +192,7 @@ test_files:
184
192
  - test/adapters/jdbcmysql.rb
185
193
  - test/adapters/jdbcpostgresql.rb
186
194
  - test/adapters/jdbcsqlite3.rb
195
+ - test/adapters/makara_postgis.rb
187
196
  - test/adapters/mysql2.rb
188
197
  - test/adapters/mysql2_makara.rb
189
198
  - test/adapters/mysql2spatial.rb
@@ -198,7 +207,10 @@ test_files:
198
207
  - test/jdbcmysql/import_test.rb
199
208
  - test/jdbcpostgresql/import_test.rb
200
209
  - test/jdbcsqlite3/import_test.rb
210
+ - test/makara_postgis/import_test.rb
211
+ - test/models/account.rb
201
212
  - test/models/alarm.rb
213
+ - test/models/bike_maker.rb
202
214
  - test/models/book.rb
203
215
  - test/models/car.rb
204
216
  - test/models/chapter.rb
@@ -211,6 +223,8 @@ test_files:
211
223
  - test/models/rule.rb
212
224
  - test/models/tag.rb
213
225
  - test/models/topic.rb
226
+ - test/models/user.rb
227
+ - test/models/user_token.rb
214
228
  - test/models/vendor.rb
215
229
  - test/models/widget.rb
216
230
  - test/mysql2/import_test.rb
@@ -220,9 +234,10 @@ test_files:
220
234
  - test/postgresql/import_test.rb
221
235
  - test/schema/generic_schema.rb
222
236
  - test/schema/jdbcpostgresql_schema.rb
223
- - test/schema/mysql_schema.rb
237
+ - test/schema/mysql2_schema.rb
224
238
  - test/schema/postgis_schema.rb
225
239
  - test/schema/postgresql_schema.rb
240
+ - test/schema/sqlite3_schema.rb
226
241
  - test/schema/version.rb
227
242
  - test/sqlite3/import_test.rb
228
243
  - test/support/active_support/test_case_extensions.rb
@@ -1,16 +0,0 @@
1
- ActiveRecord::Schema.define do
2
- create_table :books, options: 'ENGINE=MyISAM', force: true do |t|
3
- t.column :title, :string, null: false
4
- t.column :publisher, :string, null: false, default: 'Default Publisher'
5
- t.column :author_name, :string, null: false
6
- t.column :created_at, :datetime
7
- t.column :created_on, :datetime
8
- t.column :updated_at, :datetime
9
- t.column :updated_on, :datetime
10
- t.column :publish_date, :date
11
- t.column :topic_id, :integer
12
- t.column :for_sale, :boolean, default: true
13
- t.column :status, :integer
14
- end
15
- execute "ALTER TABLE books ADD FULLTEXT( `title`, `publisher`, `author_name` )"
16
- end