active_record_importer 0.3.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 +72 -5
  5. data/active_record_importer.gemspec +8 -4
  6. data/lib/active_record_importer/attribute/attributes_builder.rb +1 -0
  7. data/lib/active_record_importer/batch_importer.rb +61 -34
  8. data/lib/active_record_importer/batch_persister.rb +103 -0
  9. data/lib/active_record_importer/data_processor.rb +72 -26
  10. data/lib/active_record_importer/dispatcher.rb +34 -4
  11. data/lib/active_record_importer/failed_file_builder.rb +12 -55
  12. data/lib/active_record_importer/import_callbacker.rb +2 -2
  13. data/lib/active_record_importer/importable.rb +11 -4
  14. data/lib/active_record_importer/instance_builder.rb +17 -9
  15. data/lib/active_record_importer/options_builder.rb +4 -0
  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 +58 -15
@@ -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
@@ -0,0 +1,112 @@
1
+ ##
2
+ # Minimal stand-in for the host application's Import model
3
+ # (the one described in the README), without Paperclip.
4
+ ##
5
+ class Import < ActiveRecord::Base
6
+ store :properties, accessors: %i(insert_method find_options batch_size)
7
+
8
+ attr_accessor :execute_on_create
9
+
10
+ validates :resource, presence: true
11
+
12
+ def execute!
13
+ resource_class.import!(object: self, execute: true)
14
+ end
15
+
16
+ def resource_class
17
+ resource.safe_constantize
18
+ end
19
+
20
+ def import_file
21
+ file.path
22
+ end
23
+
24
+ attr_accessor :file
25
+
26
+ # Mimics Paperclip: receives the generated failed csv and keeps its content
27
+ def failed_file=(io)
28
+ self.failed_file_content = io.read
29
+ end
30
+
31
+ def failed_csv
32
+ CSV.parse(failed_file_content.to_s, headers: true)
33
+ end
34
+ end
35
+
36
+ ##
37
+ # Importable model with validations, a unique DB index that is NOT
38
+ # backed by a model validation (employee_no), and ActiveRecord callbacks.
39
+ ##
40
+ class User < ActiveRecord::Base
41
+ include ActiveRecordImporter::Importable
42
+
43
+ store :properties, accessors: %i(nickname department)
44
+
45
+ validates :first_name, presence: true
46
+ validates :email, presence: true, format: { with: /\A[^@\s]+@[^@\s]+\z/ }
47
+ validates :age, numericality: { greater_than: 0 }, allow_nil: true
48
+
49
+ after_save :bump_imports_count
50
+
51
+ class << self
52
+ attr_accessor :after_save_log, :state_log
53
+
54
+ def reset_logs!
55
+ self.after_save_log = []
56
+ self.state_log = []
57
+ end
58
+ end
59
+ reset_logs!
60
+
61
+ # Fake state machine API, enough for TransitionProcessor
62
+ Transition = Struct.new(:from, :to, :event)
63
+
64
+ def state_transitions
65
+ [
66
+ Transition.new('pending', 'active', :activate),
67
+ Transition.new('active', 'archived', :archive)
68
+ ]
69
+ end
70
+
71
+ def state_was
72
+ attribute_was('state')
73
+ end
74
+
75
+ def activate
76
+ update_column(:state, 'active')
77
+ self.class.state_log << [id, 'active']
78
+ end
79
+
80
+ def archive
81
+ update_column(:state, 'archived')
82
+ self.class.state_log << [id, 'archived']
83
+ end
84
+
85
+ def self.member_role(_row)
86
+ 'member'
87
+ end
88
+
89
+ private
90
+
91
+ def bump_imports_count
92
+ update_column(:imports_count, imports_count.to_i + 1)
93
+ end
94
+ end
95
+
96
+ ##
97
+ # Second importable model: makes sure options are not shared between models
98
+ ##
99
+ class Account < ActiveRecord::Base
100
+ include ActiveRecordImporter::Importable
101
+ self.table_name = 'users'
102
+
103
+ acts_as_importable insert_method: 'upsert', find_options: %i(email)
104
+ end
105
+
106
+ ##
107
+ # Model that never called acts_as_importable
108
+ ##
109
+ class Plain < ActiveRecord::Base
110
+ include ActiveRecordImporter::Importable
111
+ self.table_name = 'users'
112
+ end
@@ -0,0 +1,22 @@
1
+ ##
2
+ # Counts the SQL statements issued inside a block, grouped by verb,
3
+ # so specs can assert that a chunk is persisted in bulk.
4
+ ##
5
+ module QueryCounter
6
+ IGNORED = /\A\s*(BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE|ROLLBACK TO)/i
7
+ # Statements on the Import record itself are bookkeeping, not row persistence
8
+ IMPORTS_TABLE = /["`]imports["`]/
9
+
10
+ def count_queries
11
+ counts = Hash.new(0)
12
+ subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |*, payload|
13
+ sql = payload[:sql].to_s
14
+ next if payload[:name] == 'SCHEMA' || sql =~ IGNORED || sql =~ IMPORTS_TABLE
15
+ counts[sql[/\A\s*(\w+)/, 1].to_s.upcase] += 1
16
+ end
17
+ yield
18
+ counts
19
+ ensure
20
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
21
+ end
22
+ end
metadata CHANGED
@@ -1,57 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_importer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Nera
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-02-27 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '12.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '12.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rspec
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: '2.99'
33
+ version: '3.0'
20
34
  type: :development
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
- version: '2.99'
40
+ version: '3.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: activerecord
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
32
46
  - !ruby/object:Gem::Version
33
- version: '4.0'
47
+ version: '6.0'
34
48
  type: :runtime
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - ">="
39
53
  - !ruby/object:Gem::Version
40
- version: '4.0'
54
+ version: '6.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord-import
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: activesupport
43
71
  requirement: !ruby/object:Gem::Requirement
44
72
  requirements:
45
73
  - - ">="
46
74
  - !ruby/object:Gem::Version
47
- version: '4.0'
75
+ version: '6.0'
48
76
  type: :runtime
49
77
  prerelease: false
50
78
  version_requirements: !ruby/object:Gem::Requirement
51
79
  requirements:
52
80
  - - ">="
53
81
  - !ruby/object:Gem::Version
54
- version: '4.0'
82
+ version: '6.0'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: enumerize
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -132,7 +160,7 @@ dependencies:
132
160
  - - "~>"
133
161
  - !ruby/object:Gem::Version
134
162
  version: '4.0'
135
- description: Smart gem for importing rails models
163
+ description: Smart gem for importing CSV files to ActiveRecord models
136
164
  email:
137
165
  - kapitan_03@yahoo.com
138
166
  executables: []
@@ -142,6 +170,7 @@ files:
142
170
  - ".gitignore"
143
171
  - ".rspec"
144
172
  - ".travis.yml"
173
+ - CHANGELOG.md
145
174
  - CODE_OF_CONDUCT.md
146
175
  - Gemfile
147
176
  - LICENSE.txt
@@ -156,6 +185,7 @@ files:
156
185
  - lib/active_record_importer/attribute/find_options_builder.rb
157
186
  - lib/active_record_importer/attribute/helpers.rb
158
187
  - lib/active_record_importer/batch_importer.rb
188
+ - lib/active_record_importer/batch_persister.rb
159
189
  - lib/active_record_importer/data_processor.rb
160
190
  - lib/active_record_importer/dispatcher.rb
161
191
  - lib/active_record_importer/engine.rb
@@ -166,16 +196,30 @@ files:
166
196
  - lib/active_record_importer/instance_builder.rb
167
197
  - lib/active_record_importer/options_builder.rb
168
198
  - lib/active_record_importer/railtie.rb
199
+ - lib/active_record_importer/record_preloader.rb
169
200
  - lib/active_record_importer/transition_processor.rb
170
201
  - lib/active_record_importer/version.rb
171
202
  - lib/app/models/import.rb
203
+ - spec/active_record_importer/attribute/builders_spec.rb
204
+ - spec/active_record_importer/batch_persister_spec.rb
205
+ - spec/active_record_importer/data_processor_spec.rb
206
+ - spec/active_record_importer/failed_file_builder_spec.rb
207
+ - spec/active_record_importer/import_callbacker_spec.rb
208
+ - spec/active_record_importer/instance_builder_spec.rb
209
+ - spec/active_record_importer/options_builder_spec.rb
210
+ - spec/active_record_importer/record_preloader_spec.rb
172
211
  - spec/active_record_importer_spec.rb
212
+ - spec/integration/import_spec.rb
173
213
  - spec/spec_helper.rb
214
+ - spec/support/csv_helper.rb
215
+ - spec/support/database.rb
216
+ - spec/support/models.rb
217
+ - spec/support/query_counter.rb
174
218
  homepage: https://github.com/michaelnera/active_record_importer
175
219
  licenses:
176
220
  - MIT
177
221
  metadata: {}
178
- post_install_message:
222
+ post_install_message:
179
223
  rdoc_options: []
180
224
  require_paths:
181
225
  - lib
@@ -183,16 +227,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
183
227
  requirements:
184
228
  - - ">="
185
229
  - !ruby/object:Gem::Version
186
- version: '0'
230
+ version: '2.7'
187
231
  required_rubygems_version: !ruby/object:Gem::Requirement
188
232
  requirements:
189
233
  - - ">="
190
234
  - !ruby/object:Gem::Version
191
235
  version: '0'
192
236
  requirements: []
193
- rubyforge_project:
194
- rubygems_version: 2.4.6
195
- signing_key:
237
+ rubygems_version: 3.5.23
238
+ signing_key:
196
239
  specification_version: 4
197
240
  summary: Active Record Importer
198
241
  test_files: []