activerecord-import 0.19.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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