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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 71fdf968e3955c0ad63d10e6bab1f97ca4554aea
4
- data.tar.gz: 891b854bfd76d3ea41c2596a03af4a4494e2c470
2
+ SHA256:
3
+ metadata.gz: a9432ca865104e5e0298a6cc01cbdedd8d5e5512ab8418f54301731aad44994e
4
+ data.tar.gz: 84c9f8bd766fb6d81194b7c0ab44b65447f0fd18562bfc9b91630e4af6f861c4
5
5
  SHA512:
6
- metadata.gz: bbd93f935866c4ba427e942ca90fe0d2012df09cf7d064ad44642cfb981cb22f9c03cc541a0a16b71a0c0d64ecef3705e61c056e34e501a963689ce07e25a123
7
- data.tar.gz: 8235e2a1afb32b3d4cf689c08afbeb478b88034dcd14a90cff9ecd437451493b40a38f6c672ad266a141f6f922d55b78377ddf0374a1a486e31df14587c676e0
6
+ metadata.gz: '09eb496352a2b2b29f3a79f033689167b6bf9f743e0b14da5364211e60e4176db44d821ea94d23250028eac4ada508171fbcfcfc45cc1a3f93b39c000e4c3608'
7
+ data.tar.gz: c0450b85a0cb842663939cf69f65dd1ac857f351bd761b4f9d1ef84eac39647edb177a644ccb03564a53622525bdb2d872b370b28e681836ecfbeb11dce58359
data/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ ### Added
6
+ - Rows are persisted in bulk per CSV chunk through
7
+ [activerecord-import](https://github.com/zdennis/activerecord-import):
8
+ one bulk `INSERT` for new records and one bulk upsert (primary key conflict)
9
+ for existing records, inside a single transaction per chunk.
10
+ - Existing records matched by `find_options` are loaded with a single query per chunk.
11
+ - Validations are kept: every row is validated (`valid?`) before persistence and invalid
12
+ rows are written to the failed file with their errors, exactly as before.
13
+ - When the database rejects a bulk statement (e.g. a unique index without a model
14
+ validation), the group is retried row by row in savepoints so only the offending rows fail.
15
+ - `acts_as_importable bulk_insert: false` saves rows one by one (still one transaction
16
+ per chunk) so the model's own ActiveRecord save callbacks keep running.
17
+ - Spec suite running against PostgreSQL (default) and MySQL (`DB=mysql`).
18
+
19
+ ### Changed
20
+ - **Bulk mode skips the model's own ActiveRecord save callbacks** (`before_save`,
21
+ `after_create`, ...). Use `bulk_insert: false` if your model depends on them during import.
22
+ Importer hooks (`before_save`, `after_save`, `state_machine_attr`) still run; when
23
+ `after_save` or `state_machine_attr` are configured the chunk is saved row by row.
24
+ - Default `chunk_size` is now 500 (was `nil`, which processed one row at a time).
25
+ - Requires Ruby >= 2.7 and ActiveRecord >= 6.0 (was ActiveRecord >= 4.0).
26
+ - `activerecord-import` is a new runtime dependency.
27
+ - RSpec 3 for development.
28
+
29
+ ### Fixed
30
+ - Importer options were stored in a class variable shared by every importable model;
31
+ each model now keeps its own options.
32
+ - `default_attributes` given as `Proc` or `Symbol` were never evaluated, and
33
+ `importing: true` was not set on imported instances.
34
+ - `before_save`/`after_save` given as a single `Symbol` or `Proc` raised `NameError`.
35
+ - The failed file header row was not written as CSV.
36
+ - `File.exists?` (removed in Ruby 3.2) replaced with `File.exist?`.
37
+
38
+ ## 0.4.0 and earlier
39
+
40
+ See the git history.
data/Gemfile CHANGED
@@ -2,3 +2,10 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in active_record_importer.gemspec
4
4
  gemspec
5
+
6
+ # Database adapters used by the spec suite (see README "Development").
7
+ # Select the database with DB=postgresql (default) or DB=mysql.
8
+ group :test do
9
+ gem 'pg'
10
+ gem 'trilogy'
11
+ end
data/README.md CHANGED
@@ -1,11 +1,21 @@
1
1
  # ActiveRecordImporter
2
2
 
3
- Supports only Rails 4 and 5
4
-
5
3
  This gem helps you insert/update records easily. For now, it only accepts CSV file.
6
4
  This also helps you monitor how many rows are imported, and how many rows failed.
7
5
  This gem also allows you to easily import to any model with few configurations.
8
6
 
7
+ Rows are validated one by one but persisted **in bulk** (one `INSERT` per chunk,
8
+ one upsert per chunk) through [activerecord-import](https://github.com/zdennis/activerecord-import).
9
+ See [How rows are persisted](#how-rows-are-persisted).
10
+
11
+ The spec suite runs against PostgreSQL and MySQL (ActiveRecord 7.2); see [Development](#development).
12
+
13
+ ## Requirements
14
+
15
+ * Ruby >= 2.7
16
+ * ActiveRecord >= 6.0
17
+ * PostgreSQL (>= 9.5) or MySQL for bulk upserts; other databases fall back to row-by-row saving
18
+
9
19
  I'll release an update to enable this on background job.
10
20
 
11
21
  ## Installation
@@ -25,6 +35,13 @@ Or install it yourself as:
25
35
  $ gem install active_record_importer
26
36
 
27
37
  ## Usage
38
+ ### For version 1.0.0
39
+
40
+ Rows are now validated one by one but persisted in bulk per chunk (see
41
+ [How rows are persisted](#how-rows-are-persisted)). If your model relies on its own
42
+ ActiveRecord save callbacks during import, add `bulk_insert: false` to `acts_as_importable`.
43
+ See the [CHANGELOG](CHANGELOG.md) for the full list of changes.
44
+
28
45
  ### For version 0.4.0
29
46
 
30
47
  For the newest version (0.4.0), you don't have to create Import table/model and controller.
@@ -232,6 +249,38 @@ end
232
249
 
233
250
  `I'll add more options SOON!`
234
251
 
252
+ ### How rows are persisted
253
+
254
+ Every CSV chunk (`chunk_size` / `batch_size` rows, 500 by default) goes through these steps:
255
+
256
+ 1. The attributes of every row are built (`default_attributes`, `store_accessors`, timestamps...).
257
+ 2. For `upsert` and `error_duplicate`, the existing records matching the `find_options`
258
+ of the whole chunk are loaded with **a single query**.
259
+ 3. An instance is built for every row, the `before_save` callback runs, and the instance is
260
+ **validated** (`valid?`). Invalid rows are kept aside with their errors
261
+ (e.g. `Validation failed: Email can't be blank`).
262
+ 4. Valid new records are inserted with **one bulk `INSERT`**, and valid existing records are
263
+ updated with **one bulk upsert** on the primary key, inside one transaction per chunk.
264
+ If the database rejects a bulk statement (e.g. a unique index without a model validation),
265
+ that group is retried row by row in savepoints, so only the offending rows fail.
266
+ 5. Failed rows are written to the failed file with an `import_errors` column, and
267
+ `imported_rows` / `failed_rows` are updated on the `Import` record.
268
+
269
+ Bulk persistence skips the model's own ActiveRecord save callbacks (like `activerecord-import` does).
270
+ Rows are saved one by one (still within one transaction per chunk) when:
271
+
272
+ * the importer has `after_save` or `state_machine_attr` options, since they need a persisted instance, or
273
+ * bulk insert is disabled for the model:
274
+
275
+ ```ruby
276
+ class User < ActiveRecord::Base
277
+ acts_as_importable bulk_insert: false
278
+ end
279
+ ```
280
+
281
+ Duplicates inside the same chunk follow the `insert_method`: with `upsert` they are merged into
282
+ a single record, with `error_duplicate` the second occurrence fails with `Duplicate record found!`.
283
+
235
284
 
236
285
  ### Create Imports Controller:
237
286
  ```ruby
@@ -269,7 +318,24 @@ Headers of your csv file should be formatted/transformed to column names of your
269
318
 
270
319
  ## Development
271
320
 
272
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
321
+ After checking out the repo, run `bin/setup` to install dependencies.
322
+
323
+ The specs run against a real database. By default they use PostgreSQL
324
+ (database `active_record_importer_test`, created automatically if missing):
325
+
326
+ $ bundle exec rspec
327
+ $ DB=postgresql PG_HOST=localhost PG_USER=postgres PG_PASSWORD=secret bundle exec rspec
328
+
329
+ To run them against MySQL (uses the `trilogy` adapter, or `MYSQL_ADAPTER=mysql2`):
330
+
331
+ $ docker run -d --name ari_mysql -e MYSQL_ROOT_PASSWORD=root \
332
+ -e MYSQL_DATABASE=active_record_importer_test -p 3307:3306 mysql:8.0
333
+ $ DB=mysql MYSQL_PORT=3307 MYSQL_PASSWORD=root bundle exec rspec
334
+
335
+ Supported variables: `DB` (`postgresql`|`mysql`), `DB_NAME`, `PG_HOST`, `PG_PORT`, `PG_USER`,
336
+ `PG_PASSWORD`, `MYSQL_ADAPTER`, `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`.
337
+
338
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
273
339
 
274
340
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
275
341
 
@@ -19,10 +19,14 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ['lib']
21
21
 
22
- spec.add_development_dependency 'rspec', '~> 2.99'
22
+ spec.required_ruby_version = '>= 2.7'
23
23
 
24
- spec.add_runtime_dependency 'activerecord', '>= 4.0'
25
- spec.add_runtime_dependency 'activesupport', '>= 4.0'
24
+ spec.add_development_dependency 'rake', '>= 12.0'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
26
+
27
+ spec.add_runtime_dependency 'activerecord', '>= 6.0'
28
+ spec.add_runtime_dependency 'activerecord-import', '>= 1.0'
29
+ spec.add_runtime_dependency 'activesupport', '>= 6.0'
26
30
  spec.add_runtime_dependency 'enumerize', '>= 2.0', '~> 2.0.1'
27
31
  spec.add_runtime_dependency 'virtus', '>= 1.0', '~> 1.0'
28
32
  spec.add_runtime_dependency 'smarter_csv', '>= 1.0', '~> 1.0'
@@ -30,6 +30,7 @@ module ActiveRecordImporter
30
30
  default_attributes.each do |key, value|
31
31
  def_attrs[key] = fetch_value(value)
32
32
  end
33
+ def_attrs
33
34
  end
34
35
 
35
36
  def force_encode_attributes
@@ -1,4 +1,13 @@
1
1
  module ActiveRecordImporter
2
+ ##
3
+ # Imports one chunk of csv rows:
4
+ #
5
+ # 1. builds the attributes of every row
6
+ # 2. preloads, in one query, the records matched by the find options
7
+ # 3. builds and validates an instance for every row
8
+ # 4. persists the valid instances in bulk (see BatchPersister)
9
+ # 5. writes the failed rows (with their errors) and updates the counters
10
+ ##
2
11
  class BatchImporter
3
12
  include Virtus.model
4
13
 
@@ -9,38 +18,69 @@ module ActiveRecordImporter
9
18
  def process!
10
19
  @imported_count, @failed_count = 0, 0
11
20
 
12
- data.each do |row_attrs|
13
- next if row_attrs.blank?
14
- process_row(row_attrs.symbolize_keys!)
15
- end
16
-
21
+ processors = build_processors
22
+ persist(processors)
23
+ record_results(processors)
17
24
  set_import_count
18
25
  end
19
26
 
20
27
  private
21
28
 
22
- def process_row(row_attrs)
23
- processor =
24
- DataProcessor.new(
25
- import: import,
26
- importable: importable,
27
- row_attrs: row_attrs
28
- )
29
- return @imported_count += 1 if processor.process
29
+ def build_processors
30
+ processors = rows.map { |row_attrs| build_processor(row_attrs) }
31
+ processors.each(&:prepare)
32
+ lookup.preload(processors.map(&:find_attributes_for_lookup))
33
+ processors.each(&:build)
34
+ processors
35
+ end
36
+
37
+ def rows
38
+ data.reject(&:blank?).map { |row_attrs| row_attrs.to_hash.symbolize_keys }
39
+ end
40
+
41
+ def build_processor(row_attrs)
42
+ DataProcessor.new(
43
+ import: import,
44
+ importable: importable,
45
+ row_attrs: row_attrs,
46
+ lookup: lookup
47
+ )
48
+ end
49
+
50
+ def lookup
51
+ @lookup ||= RecordPreloader.new(importable)
52
+ end
53
+
54
+ def persist(processors)
55
+ BatchPersister.new(
56
+ importable: importable,
57
+ processors: processors.select(&:ready?)
58
+ ).persist!
59
+ end
30
60
 
31
- write_failed_row(row_attrs, processor.row_errors)
32
- @failed_count += 1
61
+ def record_results(processors)
62
+ processors.each do |processor|
63
+ if processor.failed?
64
+ write_failed_row(processor.row_attrs, processor.row_errors)
65
+ @failed_count += 1
66
+ else
67
+ @imported_count += 1
68
+ end
69
+ end
33
70
  end
34
71
 
35
72
  def set_import_count
36
73
  return unless import
37
74
 
38
- Import.update_counters(import.id, imported_rows: @imported_count)
39
- Import.update_counters(import.id, failed_rows: @failed_count)
75
+ import.class.update_counters(
76
+ import.id,
77
+ imported_rows: @imported_count,
78
+ failed_rows: @failed_count
79
+ )
40
80
  end
41
81
 
42
82
  def failed_file
43
- return unless import.present? || import.respond_to?(:failed_file)
83
+ return unless import
44
84
  @failed_file ||= FailedFileBuilder.new(import)
45
85
  end
46
86
 
@@ -48,11 +88,5 @@ module ActiveRecordImporter
48
88
  return puts errors.inspect unless failed_file
49
89
  failed_file.write(row_attrs.merge(import_errors: errors))
50
90
  end
51
-
52
- delegate :importer_options, to: :importable
53
-
54
- def csv_options
55
- importer_options.csv_opts.to_hash
56
- end
57
91
  end
58
92
  end
@@ -0,0 +1,103 @@
1
+ module ActiveRecordImporter
2
+ ##
3
+ # Persists the already validated instances of a chunk.
4
+ #
5
+ # When possible, new records are inserted with a single bulk INSERT and
6
+ # existing records are updated with a single bulk upsert on the primary
7
+ # key (through activerecord-import). If a bulk statement fails because of
8
+ # a database level error (e.g. a unique index), the affected group is
9
+ # retried row by row inside savepoints so only the offending rows fail.
10
+ #
11
+ # When importer callbacks need a persisted instance (after_save or
12
+ # state_machine_attr), or bulk insert is disabled for the model, every
13
+ # row is saved individually inside one transaction per chunk.
14
+ ##
15
+ class BatchPersister
16
+ include Virtus.model
17
+
18
+ attribute :importable, Class
19
+ attribute :processors, Array, default: []
20
+
21
+ def persist!
22
+ return if processors.empty?
23
+
24
+ importable.transaction do
25
+ bulk? ? persist_in_bulk : persist_each(processors)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ delegate :importer_options, to: :importable
32
+ delegate :after_save, :state_machine_attr, :bulk_insert, to: :importer_options
33
+
34
+ def bulk?
35
+ bulk_insert != false && !callbacks_required? && bulk_import_supported?
36
+ end
37
+
38
+ def callbacks_required?
39
+ after_save.present? || state_machine_attr.present?
40
+ end
41
+
42
+ def bulk_import_supported?
43
+ importable.respond_to?(:bulk_import)
44
+ end
45
+
46
+ def bulk_update_supported?
47
+ connection = importable.connection
48
+ connection.respond_to?(:supports_on_duplicate_key_update?) &&
49
+ connection.supports_on_duplicate_key_update?
50
+ end
51
+
52
+ def persist_in_bulk
53
+ new_processors, existing_processors =
54
+ processors.partition { |processor| processor.instance.new_record? }
55
+
56
+ bulk_insert_records(new_processors)
57
+ bulk_update_records(existing_processors)
58
+ end
59
+
60
+ def bulk_insert_records(group)
61
+ return if group.empty?
62
+ bulk_import(group, validate: false)
63
+ end
64
+
65
+ def bulk_update_records(group)
66
+ return if group.empty?
67
+ return persist_each(group) unless bulk_update_supported?
68
+
69
+ columns = changed_columns(group)
70
+ return if columns.empty?
71
+
72
+ bulk_import(group, validate: false, on_duplicate_key_update: columns)
73
+ end
74
+
75
+ def bulk_import(group, options)
76
+ instances = group.map(&:instance).uniq
77
+
78
+ importable.transaction(requires_new: true) do
79
+ importable.bulk_import(instances, options)
80
+ end
81
+ rescue ActiveRecord::ActiveRecordError
82
+ persist_each(group)
83
+ end
84
+
85
+ def changed_columns(group)
86
+ columns = group.flat_map { |processor| processor.instance.changed }.uniq
87
+ (columns - Array(importable.primary_key).map(&:to_s)).map(&:to_sym)
88
+ end
89
+
90
+ def persist_each(group)
91
+ group.each do |processor|
92
+ begin
93
+ importable.transaction(requires_new: true) do
94
+ processor.instance.save!
95
+ processor.after_persist
96
+ end
97
+ rescue => exception
98
+ processor.mark_failed(exception)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -1,4 +1,9 @@
1
1
  module ActiveRecordImporter
2
+ ##
3
+ # Prepares a single csv row: builds its attributes and find attributes,
4
+ # builds and validates the instance, and keeps the row errors.
5
+ # Persistence happens later, for the whole chunk, in BatchPersister.
6
+ ##
2
7
  class DataProcessor
3
8
  include Virtus.model
4
9
 
@@ -9,36 +14,73 @@ module ActiveRecordImporter
9
14
  attribute :instance_attrs, Hash
10
15
  attribute :find_options, String, default: :set_find_options
11
16
  attribute :find_attributes, Hash
12
- attribute :row_errors, Array
17
+ attribute :row_errors, String
13
18
  attribute :instance
19
+ attribute :lookup, Object, default: :default_lookup
14
20
 
15
21
  def process
22
+ prepare
23
+ build
24
+ end
25
+
26
+ ##
27
+ # Step 1: build the attributes used for the instance and the lookup
28
+ ##
29
+ def prepare
16
30
  fetch_instance_attributes
17
31
  fetch_find_attributes
18
- create_instance
32
+ !failed?
19
33
  end
20
34
 
21
- private
35
+ ##
36
+ # Step 2: build and validate the instance
37
+ ##
38
+ def build
39
+ return false if failed?
40
+
41
+ @instance =
42
+ InstanceBuilder.new(
43
+ importable: importable,
44
+ insert_method: insert_method,
45
+ find_attributes: find_attributes,
46
+ instance_attrs: attributes_without_state_machine_attrs,
47
+ lookup: lookup
48
+ ).build
49
+ true
50
+ rescue => exception
51
+ append_errors(exception)
52
+ false
53
+ end
22
54
 
23
- def create_instance
24
- ActiveRecord::Base.transaction do
25
- begin
26
- @instance =
27
- InstanceBuilder.new(
28
- importable: importable,
29
- insert_method: insert_method,
30
- find_attributes: find_attributes,
31
- instance_attrs: attributes_without_state_machine_attrs
32
- ).build
33
-
34
- methods_after_upsert
35
- true
36
- rescue => exception
37
- append_errors(exception, true)
38
- end
39
- end
55
+ ##
56
+ # Find attributes that need to be preloaded for this row
57
+ ##
58
+ def find_attributes_for_lookup
59
+ return if failed? || insert_method.inquiry.insert?
60
+ find_attributes
61
+ end
62
+
63
+ ##
64
+ # Called once the instance is persisted (per-row mode only)
65
+ ##
66
+ def after_persist
67
+ methods_after_upsert
68
+ end
69
+
70
+ def mark_failed(error)
71
+ append_errors(error)
72
+ end
73
+
74
+ def failed?
75
+ row_errors.present?
76
+ end
77
+
78
+ def ready?
79
+ !failed? && instance.present?
40
80
  end
41
81
 
82
+ private
83
+
42
84
  def fetch_instance_attributes
43
85
  @instance_attrs = Attribute::AttributesBuilder.new(
44
86
  importable, row_attrs
@@ -48,6 +90,8 @@ module ActiveRecordImporter
48
90
  end
49
91
 
50
92
  def fetch_find_attributes
93
+ return if failed?
94
+
51
95
  @find_attributes = Attribute::FindOptionsBuilder.new(
52
96
  importable: importable,
53
97
  find_options: find_options,
@@ -81,7 +125,7 @@ module ActiveRecordImporter
81
125
  end
82
126
 
83
127
  def attributes_without_state_machine_attrs
84
- instance_attrs.except(*state_machine_attr)
128
+ instance_attrs.except(*Array(state_machine_attr))
85
129
  end
86
130
 
87
131
  def skip_callbacks?
@@ -94,12 +138,10 @@ module ActiveRecordImporter
94
138
  ImportCallbacker.new(instance, after_save).call
95
139
  end
96
140
 
97
- def append_errors(error, rollback = false)
141
+ def append_errors(error)
98
142
  return if error.blank?
99
143
 
100
- message = error.is_a?(Exception) ? error.message : error
101
- @row_errors = message
102
- fail ActiveRecord::Rollback if rollback
144
+ @row_errors = error.is_a?(Exception) ? error.message : error.to_s
103
145
  end
104
146
 
105
147
  def set_insert_method
@@ -111,5 +153,9 @@ module ActiveRecordImporter
111
153
  def set_find_options
112
154
  import.try(:find_options) || importer_options.find_options.join(',')
113
155
  end
156
+
157
+ def default_lookup
158
+ RecordPreloader.new(importable)
159
+ end
114
160
  end
115
161
  end
@@ -26,8 +26,8 @@ module ActiveRecordImporter
26
26
 
27
27
  def csv_options
28
28
  opts = klass_csv_opts
29
- return opts if import.nil? || import.batch_size.blank?
30
- opts.merge(chunk_size: import.batch_size)
29
+ return opts if import.nil? || import.batch_size.to_i <= 0
30
+ opts.merge(chunk_size: import.batch_size.to_i)
31
31
  end
32
32
 
33
33
  def klass_csv_opts
@@ -52,7 +52,7 @@ module ActiveRecordImporter
52
52
  end
53
53
 
54
54
  def create_import_failed_file
55
- return if import.nil? || !File.exists?(temp_failed_file_path)
55
+ return if import.nil? || !File.exist?(temp_failed_file_path)
56
56
  File.open(temp_failed_file_path) do |file|
57
57
  import.failed_file = file
58
58
 
@@ -10,13 +10,13 @@ module ActiveRecordImporter
10
10
  def write(failed_row = {})
11
11
  return if failed_row.blank?
12
12
 
13
- if File.exists?(temp_failed_file_path)
13
+ if File.exist?(temp_failed_file_path)
14
14
  File.open(temp_failed_file_path, 'a') do |file|
15
15
  file.write failed_row.values.to_csv
16
16
  end
17
17
  else
18
18
  File.open(temp_failed_file_path, 'w') do |file|
19
- file.write failed_row.keys
19
+ file.write failed_row.keys.to_csv
20
20
  file.write failed_row.values.to_csv
21
21
  end
22
22
  end
@@ -12,9 +12,9 @@ module ActiveRecordImporter
12
12
  when Array
13
13
  run_each_callbacks
14
14
  when Symbol
15
- object.send(callback)
15
+ object.send(callback_methods)
16
16
  when Proc
17
- callback.call(object)
17
+ callback_methods.call(object)
18
18
  end
19
19
  end
20
20
 
@@ -21,14 +21,15 @@ module ActiveRecordImporter
21
21
  # end
22
22
  ##
23
23
  def acts_as_importable(options = {})
24
- @@importer_options = OptionsBuilder.build(options.merge(allowed_columns_hash(options)))
24
+ @importer_options = OptionsBuilder.build(options.merge(allowed_columns_hash(options)))
25
25
 
26
26
  include ActiveRecordImporter::Importable::InstanceMethods
27
27
  extend ActiveRecordImporter::Importable::SingletonMethods
28
28
  end
29
29
 
30
30
  def importer_options
31
- @@importer_options
31
+ return @importer_options if defined?(@importer_options) && @importer_options
32
+ superclass.importer_options if superclass.respond_to?(:importer_options)
32
33
  end
33
34
 
34
35
  def importable?