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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +40 -0
- data/Gemfile +7 -0
- data/README.md +72 -5
- data/active_record_importer.gemspec +8 -4
- data/lib/active_record_importer/attribute/attributes_builder.rb +1 -0
- data/lib/active_record_importer/batch_importer.rb +61 -34
- data/lib/active_record_importer/batch_persister.rb +103 -0
- data/lib/active_record_importer/data_processor.rb +72 -26
- data/lib/active_record_importer/dispatcher.rb +34 -4
- data/lib/active_record_importer/failed_file_builder.rb +12 -55
- data/lib/active_record_importer/import_callbacker.rb +2 -2
- data/lib/active_record_importer/importable.rb +11 -4
- data/lib/active_record_importer/instance_builder.rb +17 -9
- data/lib/active_record_importer/options_builder.rb +4 -0
- data/lib/active_record_importer/record_preloader.rb +72 -0
- data/lib/active_record_importer/version.rb +1 -1
- data/lib/active_record_importer.rb +6 -1
- data/spec/active_record_importer/attribute/builders_spec.rb +51 -0
- data/spec/active_record_importer/batch_persister_spec.rb +87 -0
- data/spec/active_record_importer/data_processor_spec.rb +89 -0
- data/spec/active_record_importer/failed_file_builder_spec.rb +26 -0
- data/spec/active_record_importer/import_callbacker_spec.rb +25 -0
- data/spec/active_record_importer/instance_builder_spec.rb +77 -0
- data/spec/active_record_importer/options_builder_spec.rb +19 -0
- data/spec/active_record_importer/record_preloader_spec.rb +59 -0
- data/spec/active_record_importer_spec.rb +1 -5
- data/spec/integration/import_spec.rb +457 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/support/csv_helper.rb +42 -0
- data/spec/support/database.rb +100 -0
- data/spec/support/models.rb +112 -0
- data/spec/support/query_counter.rb +22 -0
- 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.
|
|
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:
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
-
|
|
194
|
-
|
|
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: []
|