active_record_importer 0.4.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 (34) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +40 -0
  3. data/Gemfile +7 -0
  4. data/README.md +69 -3
  5. data/active_record_importer.gemspec +7 -3
  6. data/lib/active_record_importer/attribute/attributes_builder.rb +1 -0
  7. data/lib/active_record_importer/batch_importer.rb +58 -24
  8. data/lib/active_record_importer/batch_persister.rb +103 -0
  9. data/lib/active_record_importer/data_processor.rb +71 -25
  10. data/lib/active_record_importer/dispatcher.rb +3 -3
  11. data/lib/active_record_importer/failed_file_builder.rb +2 -2
  12. data/lib/active_record_importer/import_callbacker.rb +2 -2
  13. data/lib/active_record_importer/importable.rb +3 -2
  14. data/lib/active_record_importer/instance_builder.rb +17 -9
  15. data/lib/active_record_importer/options_builder.rb +5 -1
  16. data/lib/active_record_importer/record_preloader.rb +72 -0
  17. data/lib/active_record_importer/version.rb +1 -1
  18. data/lib/active_record_importer.rb +6 -1
  19. data/spec/active_record_importer/attribute/builders_spec.rb +51 -0
  20. data/spec/active_record_importer/batch_persister_spec.rb +87 -0
  21. data/spec/active_record_importer/data_processor_spec.rb +89 -0
  22. data/spec/active_record_importer/failed_file_builder_spec.rb +26 -0
  23. data/spec/active_record_importer/import_callbacker_spec.rb +25 -0
  24. data/spec/active_record_importer/instance_builder_spec.rb +77 -0
  25. data/spec/active_record_importer/options_builder_spec.rb +19 -0
  26. data/spec/active_record_importer/record_preloader_spec.rb +59 -0
  27. data/spec/active_record_importer_spec.rb +1 -5
  28. data/spec/integration/import_spec.rb +457 -0
  29. data/spec/spec_helper.rb +41 -0
  30. data/spec/support/csv_helper.rb +42 -0
  31. data/spec/support/database.rb +100 -0
  32. data/spec/support/models.rb +112 -0
  33. data/spec/support/query_counter.rb +22 -0
  34. metadata +57 -14
@@ -0,0 +1,457 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Importing a CSV file', type: :integration do
4
+ let(:import) { Import.new(resource: 'User') }
5
+
6
+ describe 'insert' do
7
+ before { User.acts_as_importable }
8
+
9
+ it 'inserts every valid row and records the counts' do
10
+ run_import(User, user_rows(5), import: import)
11
+
12
+ expect(User.count).to eq(5)
13
+ expect(User.pluck(:email)).to match_array((1..5).map { |i| "user#{i}@example.com" })
14
+ expect(import.imported_rows).to eq(5)
15
+ expect(import.failed_rows).to eq(0)
16
+ expect(import.failed_file_content).to be_nil
17
+ end
18
+
19
+ it 'persists a chunk with a single bulk INSERT' do
20
+ counts = count_queries { run_import(User, user_rows(20)) }
21
+
22
+ expect(User.count).to eq(20)
23
+ expect(counts['INSERT']).to eq(1)
24
+ end
25
+
26
+ it 'works without an Import object and prints the errors of failed rows' do
27
+ rows = user_rows(3)
28
+ rows[1][:first_name] = ''
29
+
30
+ expect { run_import(User, rows) }.to output(/First name can't be blank/).to_stdout
31
+ expect(User.count).to eq(2)
32
+ end
33
+
34
+ it 'sets timestamps on the inserted records' do
35
+ run_import(User, user_rows(2))
36
+
37
+ expect(User.where(created_at: nil).count).to eq(0)
38
+ expect(User.where(updated_at: nil).count).to eq(0)
39
+ end
40
+
41
+ it 'uses created_at/updated_at from the csv when present' do
42
+ rows = user_rows(1, created_at: '2020-01-02 03:04:05', updated_at: '2020-02-03 04:05:06')
43
+ run_import(User, rows)
44
+
45
+ user = User.first
46
+ expect(user.created_at.year).to eq(2020)
47
+ expect(user.created_at.month).to eq(1)
48
+ expect(user.updated_at.month).to eq(2)
49
+ end
50
+
51
+ it 'ignores csv columns that are not importable columns' do
52
+ run_import(User, user_rows(1, id: 999, unknown_column: 'x'))
53
+
54
+ expect(User.count).to eq(1)
55
+ expect(User.first.id).not_to eq(999)
56
+ end
57
+
58
+ it 'skips the model save callbacks in bulk mode' do
59
+ run_import(User, user_rows(2))
60
+
61
+ expect(User.pluck(:imports_count)).to eq([0, 0])
62
+ end
63
+ end
64
+
65
+ describe 'validations' do
66
+ before { User.acts_as_importable }
67
+
68
+ let(:rows) do
69
+ [
70
+ { first_name: 'Valid', email: 'valid@example.com', employee_no: 'A1', age: 30 },
71
+ { first_name: '', email: 'noname@example.com', employee_no: 'A2', age: 30 },
72
+ { first_name: 'Bad Email', email: 'not-an-email', employee_no: 'A3', age: 30 },
73
+ { first_name: 'Bad Age', email: 'badage@example.com', employee_no: 'A4', age: -1 },
74
+ { first_name: 'Also Valid', email: 'also@example.com', employee_no: 'A5', age: 31 }
75
+ ]
76
+ end
77
+
78
+ it 'imports the valid rows and rejects the invalid ones' do
79
+ run_import(User, rows, import: import)
80
+
81
+ expect(User.pluck(:email)).to match_array(%w(valid@example.com also@example.com))
82
+ expect(import.imported_rows).to eq(2)
83
+ expect(import.failed_rows).to eq(3)
84
+ end
85
+
86
+ it 'writes the failed rows with their validation errors to the failed file' do
87
+ run_import(User, rows, import: import)
88
+
89
+ failed = import.failed_csv
90
+ expect(import.failed_file_content_type).to eq('text/csv')
91
+ expect(failed.headers).to include('first_name', 'email', 'import_errors')
92
+ expect(failed.size).to eq(3)
93
+
94
+ by_email = failed.map { |r| [r['email'], r['import_errors']] }.to_h
95
+ expect(by_email['noname@example.com']).to eq("Validation failed: First name can't be blank")
96
+ expect(by_email['not-an-email']).to eq('Validation failed: Email is invalid')
97
+ expect(by_email['badage@example.com']).to eq('Validation failed: Age must be greater than 0')
98
+ end
99
+
100
+ it 'keeps the original row order in the failed file' do
101
+ run_import(User, rows, import: import)
102
+
103
+ expect(import.failed_csv.map { |r| r['email'] })
104
+ .to eq(%w(noname@example.com not-an-email badage@example.com))
105
+ end
106
+
107
+ it 'still bulk inserts the valid rows' do
108
+ counts = count_queries { run_import(User, rows) }
109
+
110
+ expect(counts['INSERT']).to eq(1)
111
+ end
112
+ end
113
+
114
+ describe 'database level errors' do
115
+ before { User.acts_as_importable }
116
+
117
+ it 'falls back to per-row saving so only the offending row fails' do
118
+ User.create!(first_name: 'Existing', email: 'existing@example.com', employee_no: 'DUP')
119
+
120
+ rows = user_rows(3)
121
+ rows[1][:employee_no] = 'DUP' # violates the unique index, no model validation
122
+
123
+ run_import(User, rows, import: import)
124
+
125
+ expect(User.count).to eq(3)
126
+ expect(User.pluck(:email)).to match_array(
127
+ %w(existing@example.com user1@example.com user3@example.com)
128
+ )
129
+ expect(import.imported_rows).to eq(2)
130
+ expect(import.failed_rows).to eq(1)
131
+
132
+ failed = import.failed_csv
133
+ expect(failed.size).to eq(1)
134
+ expect(failed.first['email']).to eq('user2@example.com')
135
+ expect(failed.first['import_errors']).to match(/DUP|unique|duplicate/i)
136
+ end
137
+ end
138
+
139
+ describe 'upsert' do
140
+ before do
141
+ User.acts_as_importable insert_method: 'upsert', find_options: %i(email)
142
+ end
143
+
144
+ let!(:existing) do
145
+ User.create!(first_name: 'Old', last_name: 'Name', email: 'user1@example.com',
146
+ employee_no: 'OLD-1', age: 50)
147
+ end
148
+
149
+ it 'updates existing records and inserts the new ones' do
150
+ run_import(User, user_rows(3), import: import)
151
+
152
+ expect(User.count).to eq(3)
153
+ existing.reload
154
+ expect(existing.first_name).to eq('User1')
155
+ expect(existing.employee_no).to eq('EMP-1')
156
+ expect(existing.age).to eq(21)
157
+ expect(import.imported_rows).to eq(3)
158
+ expect(import.failed_rows).to eq(0)
159
+ end
160
+
161
+ it 'does not touch columns missing from the csv' do
162
+ run_import(User, [{ email: 'user1@example.com', first_name: 'Renamed' }])
163
+
164
+ existing.reload
165
+ expect(existing.first_name).to eq('Renamed')
166
+ expect(existing.last_name).to eq('Name')
167
+ expect(existing.age).to eq(50)
168
+ end
169
+
170
+ it 'looks records up once per chunk and persists with one INSERT and one upsert' do
171
+ User.create!(first_name: 'Another', email: 'user2@example.com', employee_no: 'OLD-2')
172
+
173
+ counts = count_queries { run_import(User, user_rows(10)) }
174
+
175
+ expect(counts['SELECT']).to eq(1)
176
+ expect(counts['INSERT']).to eq(2)
177
+ expect(counts['UPDATE'].to_i).to eq(0)
178
+ end
179
+
180
+ it 'validates updated records too' do
181
+ run_import(User, [{ email: 'user1@example.com', age: -5 }], import: import)
182
+
183
+ expect(existing.reload.age).to eq(50)
184
+ expect(import.failed_rows).to eq(1)
185
+ expect(import.failed_csv.first['import_errors'])
186
+ .to eq('Validation failed: Age must be greater than 0')
187
+ end
188
+
189
+ it 'merges duplicate rows of the same chunk into one record' do
190
+ rows = [
191
+ { first_name: 'First', email: 'new@example.com', employee_no: 'N1' },
192
+ { first_name: 'Second', email: 'new@example.com', employee_no: 'N1' }
193
+ ]
194
+
195
+ run_import(User, rows, import: import)
196
+
197
+ created = User.where(email: 'new@example.com')
198
+ expect(created.count).to eq(1)
199
+ expect(created.first.first_name).to eq('Second')
200
+ expect(import.imported_rows).to eq(2)
201
+ end
202
+
203
+ it 'supports multiple find options' do
204
+ User.acts_as_importable insert_method: 'upsert', find_options: %i(first_name last_name)
205
+ doe = User.create!(first_name: 'User2', last_name: 'Doe', email: 'doe@example.com', employee_no: 'D')
206
+ User.create!(first_name: 'User2', last_name: 'Smith', email: 'other@example.com', employee_no: 'X')
207
+
208
+ run_import(User, user_rows(2))
209
+
210
+ expect(User.count).to eq(4) # existing (Old Name), User1 Doe (new), User2 Doe (updated), User2 Smith
211
+ expect(existing.reload.employee_no).to eq('OLD-1') # email is not part of the find options
212
+ expect(doe.reload.email).to eq('user2@example.com')
213
+ expect(User.find_by(first_name: 'User2', last_name: 'Smith').email).to eq('other@example.com')
214
+ end
215
+
216
+ it 'fails rows without find attributes' do
217
+ run_import(User, [{ first_name: 'No Email', email: nil, employee_no: 'Z' }], import: import)
218
+
219
+ expect(import.failed_rows).to eq(1)
220
+ expect(import.failed_csv.first['import_errors'])
221
+ .to eq('Find by option for your csv is missing or incorrect.')
222
+ end
223
+
224
+ it 'takes insert_method and find_options from the Import object' do
225
+ User.acts_as_importable
226
+ import.insert_method = 'upsert'
227
+ import.find_options = 'email'
228
+
229
+ run_import(User, user_rows(1), import: import)
230
+
231
+ expect(User.count).to eq(1)
232
+ expect(existing.reload.first_name).to eq('User1')
233
+ end
234
+ end
235
+
236
+ describe 'error_duplicate' do
237
+ before do
238
+ User.acts_as_importable insert_method: 'error_duplicate', find_options: %i(email)
239
+ User.create!(first_name: 'Old', email: 'user1@example.com', employee_no: 'OLD-1')
240
+ end
241
+
242
+ it 'rejects rows matching an existing record' do
243
+ run_import(User, user_rows(3), import: import)
244
+
245
+ expect(User.count).to eq(3)
246
+ expect(User.find_by(email: 'user1@example.com').first_name).to eq('Old')
247
+ expect(import.imported_rows).to eq(2)
248
+ expect(import.failed_rows).to eq(1)
249
+ expect(import.failed_csv.first['import_errors']).to eq('Duplicate record found!')
250
+ end
251
+
252
+ it 'rejects duplicates within the same chunk' do
253
+ rows = user_rows(2, email: 'same@example.com')
254
+ rows[1][:employee_no] = 'OTHER'
255
+
256
+ run_import(User, rows, import: import)
257
+
258
+ expect(User.where(email: 'same@example.com').count).to eq(1)
259
+ expect(import.imported_rows).to eq(1)
260
+ expect(import.failed_rows).to eq(1)
261
+ end
262
+ end
263
+
264
+ describe 'importer options' do
265
+ it 'applies default_attributes (values, procs and symbols)' do
266
+ User.acts_as_importable default_attributes: {
267
+ last_name: 'Default',
268
+ password: proc { |row| "pw-#{row[:first_name]}" },
269
+ role: :member_role
270
+ }
271
+
272
+ run_import(User, [{ first_name: 'Ann', email: 'ann@example.com', employee_no: 'A' }])
273
+
274
+ user = User.first
275
+ expect(user.last_name).to eq('Default')
276
+ expect(user.password).to eq('pw-Ann')
277
+ expect(user.role).to eq('member')
278
+ end
279
+
280
+ it 'lets csv values override default_attributes' do
281
+ User.acts_as_importable default_attributes: { last_name: 'Default' }
282
+
283
+ run_import(User, user_rows(1))
284
+
285
+ expect(User.first.last_name).to eq('Doe')
286
+ end
287
+
288
+ it 'imports store accessors' do
289
+ User.acts_as_importable store_accessors: %i(nickname department)
290
+
291
+ run_import(User, user_rows(1, nickname: 'annie', department: 'R&D'))
292
+
293
+ expect(User.first.nickname).to eq('annie')
294
+ expect(User.first.department).to eq('R&D')
295
+ end
296
+
297
+ it 'excludes columns listed in exclude_columns' do
298
+ User.acts_as_importable exclude_columns: %i(age)
299
+
300
+ run_import(User, user_rows(1))
301
+
302
+ expect(User.first.age).to be_nil
303
+ end
304
+
305
+ it 'runs before_save procs before validating' do
306
+ User.acts_as_importable before_save: proc { |user| user.password = 'temporary123' }
307
+
308
+ run_import(User, user_rows(2))
309
+
310
+ expect(User.pluck(:password).uniq).to eq(['temporary123'])
311
+ end
312
+
313
+ it 'keeps options separate per model' do
314
+ User.acts_as_importable
315
+
316
+ expect(User.importer_options.insert_method).to be_nil
317
+ expect(Account.importer_options.insert_method).to eq('upsert')
318
+ expect(Account.importer_options.find_options).to eq([:email])
319
+ end
320
+
321
+ it 'honours csv_opts' do
322
+ User.acts_as_importable csv_opts: { col_sep: '|' }
323
+ path = csv_file(user_rows(1))
324
+ File.write(path, "first_name|email|employee_no\nPipe|pipe@example.com|P1\n")
325
+
326
+ User.import!(file: path)
327
+
328
+ expect(User.first.first_name).to eq('Pipe')
329
+ end
330
+ end
331
+
332
+ describe 'per-row fallback' do
333
+ it 'runs after_save hooks on persisted instances' do
334
+ User.acts_as_importable after_save: proc { |user| User.after_save_log << user.id }
335
+
336
+ run_import(User, user_rows(3), import: import)
337
+
338
+ expect(User.after_save_log.sort).to eq(User.pluck(:id).sort)
339
+ expect(import.imported_rows).to eq(3)
340
+ end
341
+
342
+ it 'accepts symbol and array after_save hooks' do
343
+ User.acts_as_importable after_save: [:activate]
344
+
345
+ run_import(User, user_rows(2))
346
+
347
+ expect(User.pluck(:state).uniq).to eq(['active'])
348
+ end
349
+
350
+ it 'applies state machine transitions after saving' do
351
+ User.acts_as_importable insert_method: 'upsert', find_options: %i(email),
352
+ state_machine_attr: %i(state)
353
+ pending = User.create!(first_name: 'P', email: 'user1@example.com', employee_no: 'P1', state: 'pending')
354
+
355
+ run_import(User, user_rows(1, state: 'active'), import: import)
356
+
357
+ expect(pending.reload.state).to eq('active')
358
+ expect(User.state_log).to eq([[pending.id, 'active']])
359
+ expect(import.imported_rows).to eq(1)
360
+ end
361
+
362
+ it 'fails the row when the transition is invalid' do
363
+ User.acts_as_importable insert_method: 'upsert', find_options: %i(email),
364
+ state_machine_attr: %i(state)
365
+ pending = User.create!(first_name: 'P', email: 'user1@example.com', employee_no: 'P1', state: 'pending')
366
+
367
+ run_import(User, user_rows(1, state: 'archived'), import: import)
368
+
369
+ expect(pending.reload.state).to eq('pending')
370
+ expect(pending.first_name).to eq('P') # rolled back
371
+ expect(import.failed_rows).to eq(1)
372
+ expect(import.failed_csv.first['import_errors']).to eq('Transition is invalid')
373
+ end
374
+
375
+ it 'runs the model callbacks when bulk_insert is disabled' do
376
+ User.acts_as_importable bulk_insert: false
377
+
378
+ counts = count_queries { run_import(User, user_rows(3), import: import) }
379
+
380
+ expect(User.pluck(:imports_count)).to eq([1, 1, 1])
381
+ expect(counts['INSERT']).to eq(3)
382
+ expect(import.imported_rows).to eq(3)
383
+ end
384
+
385
+ it 'still reports validation errors' do
386
+ User.acts_as_importable bulk_insert: false
387
+ rows = user_rows(2)
388
+ rows[0][:email] = 'broken'
389
+
390
+ run_import(User, rows, import: import)
391
+
392
+ expect(User.count).to eq(1)
393
+ expect(import.failed_rows).to eq(1)
394
+ expect(import.failed_csv.first['import_errors']).to eq('Validation failed: Email is invalid')
395
+ end
396
+ end
397
+
398
+ describe 'batching' do
399
+ before { User.acts_as_importable }
400
+
401
+ it 'splits the file in chunks of batch_size' do
402
+ import.batch_size = 4
403
+
404
+ counts = count_queries { run_import(User, user_rows(10), import: import) }
405
+
406
+ expect(User.count).to eq(10)
407
+ expect(counts['INSERT']).to eq(3)
408
+ expect(import.imported_rows).to eq(10)
409
+ end
410
+
411
+ it 'accumulates counters across chunks' do
412
+ import.batch_size = 2
413
+ rows = user_rows(5)
414
+ rows[0][:first_name] = ''
415
+ rows[4][:first_name] = ''
416
+
417
+ run_import(User, rows, import: import)
418
+
419
+ expect(import.imported_rows).to eq(3)
420
+ expect(import.failed_rows).to eq(2)
421
+ expect(import.failed_csv.size).to eq(2)
422
+ end
423
+
424
+ it 'uses the chunk_size from csv_opts when the import has no batch_size' do
425
+ User.acts_as_importable csv_opts: { chunk_size: 5 }
426
+
427
+ counts = count_queries { run_import(User, user_rows(10)) }
428
+
429
+ expect(counts['INSERT']).to eq(2)
430
+ end
431
+ end
432
+
433
+ describe 'Import#execute!' do
434
+ before { User.acts_as_importable }
435
+
436
+ it 'runs the import for the resource' do
437
+ import.file = File.new(csv_file(user_rows(2)))
438
+ import.save!
439
+
440
+ import.execute!
441
+
442
+ expect(User.count).to eq(2)
443
+ expect(import.reload.imported_rows).to eq(2)
444
+ end
445
+ end
446
+
447
+ describe 'errors' do
448
+ it 'refuses to import a model without acts_as_importable' do
449
+ expect { Plain.import!(file: 'whatever.csv') }.to raise_error(/Plain is not importable/)
450
+ end
451
+
452
+ it 'requires a file' do
453
+ User.acts_as_importable
454
+ expect { User.import! }.to raise_error(ActiveRecordImporter::Errors::MissingImportFile)
455
+ end
456
+ end
457
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,2 +1,43 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'active_record'
2
4
  require 'active_record_importer'
5
+
6
+ require_relative 'support/database'
7
+ require_relative 'support/csv_helper'
8
+ require_relative 'support/query_counter'
9
+
10
+ RSpec.configure do |config|
11
+ config.expect_with :rspec do |expectations|
12
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
13
+ end
14
+
15
+ config.mock_with :rspec do |mocks|
16
+ mocks.verify_partial_doubles = true
17
+ end
18
+
19
+ config.filter_run_when_matching :focus
20
+ config.disable_monkey_patching!
21
+ config.order = :random
22
+ Kernel.srand config.seed
23
+
24
+ config.include CsvHelper
25
+ config.include QueryCounter
26
+
27
+ config.before(:suite) do
28
+ Database.connect!
29
+ Database.load_schema!
30
+ # Models call acts_as_importable, which reads the table columns,
31
+ # so they can only be loaded once the schema exists
32
+ require_relative 'support/models'
33
+ end
34
+
35
+ config.before(:each) do
36
+ Database.clean!
37
+ User.reset_logs!
38
+ end
39
+
40
+ config.after(:each) do
41
+ Array(@csv_files).each(&:close!)
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ require 'tempfile'
2
+ require 'csv'
3
+
4
+ module CsvHelper
5
+ ##
6
+ # Writes rows (array of hashes sharing the same keys) to a temp csv
7
+ # and returns its path. The file lives for the duration of the example.
8
+ ##
9
+ def csv_file(rows, headers: nil)
10
+ headers ||= rows.first.keys
11
+ file = Tempfile.new(['import', '.csv'])
12
+ file.write(CSV.generate_line(headers))
13
+ rows.each { |row| file.write(CSV.generate_line(headers.map { |h| row[h] })) }
14
+ file.flush
15
+ (@csv_files ||= []) << file
16
+ file.path
17
+ end
18
+
19
+ def user_rows(count, overrides = {})
20
+ (1..count).map do |i|
21
+ {
22
+ first_name: "User#{i}",
23
+ last_name: 'Doe',
24
+ email: "user#{i}@example.com",
25
+ employee_no: "EMP-#{i}",
26
+ age: 20 + i
27
+ }.merge(overrides)
28
+ end
29
+ end
30
+
31
+ def run_import(klass, rows, import: nil, headers: nil)
32
+ path = csv_file(rows, headers: headers)
33
+ if import
34
+ import.file = File.new(path)
35
+ import.save!
36
+ klass.import!(object: import)
37
+ import.reload
38
+ else
39
+ klass.import!(file: path)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,100 @@
1
+ ##
2
+ # Database used by the spec suite.
3
+ #
4
+ # DB=postgresql bundle exec rspec (default)
5
+ # DB=mysql bundle exec rspec
6
+ #
7
+ # Connection settings can be overridden with the usual environment
8
+ # variables (see .configuration below).
9
+ ##
10
+ module Database
11
+ DATABASE = ENV.fetch('DB_NAME', 'active_record_importer_test')
12
+
13
+ module_function
14
+
15
+ def adapter
16
+ ENV.fetch('DB', 'postgresql').to_s
17
+ end
18
+
19
+ def mysql?
20
+ adapter == 'mysql'
21
+ end
22
+
23
+ def postgresql?
24
+ adapter == 'postgresql'
25
+ end
26
+
27
+ def configuration
28
+ if mysql?
29
+ {
30
+ adapter: ENV.fetch('MYSQL_ADAPTER', 'trilogy'),
31
+ host: ENV.fetch('MYSQL_HOST', '127.0.0.1'),
32
+ port: ENV.fetch('MYSQL_PORT', '3306').to_i,
33
+ username: ENV.fetch('MYSQL_USER', 'root'),
34
+ password: ENV.fetch('MYSQL_PASSWORD', ''),
35
+ database: DATABASE
36
+ }
37
+ elsif postgresql?
38
+ {
39
+ adapter: 'postgresql',
40
+ host: ENV['PG_HOST'],
41
+ port: ENV['PG_PORT'],
42
+ username: ENV['PG_USER'],
43
+ password: ENV['PG_PASSWORD'],
44
+ database: DATABASE
45
+ }.compact
46
+ else
47
+ fail "Unsupported DB=#{adapter.inspect}, use postgresql or mysql"
48
+ end
49
+ end
50
+
51
+ def connect!
52
+ ActiveRecord::Base.establish_connection(configuration)
53
+ ActiveRecord::Base.connection.execute('SELECT 1')
54
+ rescue ActiveRecord::NoDatabaseError
55
+ create_database!
56
+ ActiveRecord::Base.establish_connection(configuration)
57
+ end
58
+
59
+ def create_database!
60
+ admin_config = configuration.merge(database: postgresql? ? 'postgres' : nil).compact
61
+ ActiveRecord::Base.establish_connection(admin_config)
62
+ ActiveRecord::Base.connection.create_database(DATABASE)
63
+ end
64
+
65
+ def load_schema!
66
+ ActiveRecord::Schema.verbose = false
67
+ ActiveRecord::Schema.define do
68
+ create_table :users, force: true do |t|
69
+ t.string :first_name
70
+ t.string :last_name
71
+ t.string :email
72
+ t.string :employee_no
73
+ t.integer :age
74
+ t.string :role
75
+ t.string :state
76
+ t.string :password
77
+ t.text :properties
78
+ t.integer :imports_count, default: 0
79
+ t.timestamps null: true
80
+ end
81
+ add_index :users, :employee_no, unique: true
82
+
83
+ create_table :imports, force: true do |t|
84
+ t.text :properties
85
+ t.string :resource, null: false
86
+ t.integer :imported_rows, default: 0
87
+ t.integer :failed_rows, default: 0
88
+ t.text :failed_file_content
89
+ t.string :failed_file_content_type
90
+ t.timestamps null: true
91
+ end
92
+ end
93
+ end
94
+
95
+ def clean!
96
+ %w(users imports).each do |table|
97
+ ActiveRecord::Base.connection.execute("DELETE FROM #{table}")
98
+ end
99
+ end
100
+ end