rimportor 0.3

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 11df56cdb93fc78219c02060fa2ad2dbbf944217
4
+ data.tar.gz: e5f6b49bc437eff5e28ad65c08da8a98d6314f1f
5
+ SHA512:
6
+ metadata.gz: fa2bddde54b617c167030ba41bb7a7a79dde80eeccf3455287cf227eda27440a2b4bbe9cea4307f9b0d2ad711d4d3d608c9507faf84b36f0fa9bcd12d62e14e1
7
+ data.tar.gz: 1e849d21abee6a89233174355441bb78d22cfc2996da0a501fb0cfd893333bba1bc97bc71b9777e1c2a6d1ea6dfa6c187631c64f7077b3a9101ec7f0b33b267e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Erwin Schens
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Rimportor
2
+
3
+ Rimportor is a new and modern bulk importing gem.
4
+ It utilizes arel under the hood to generate insert statements.
5
+ By working directly on the model Rimportor is able to execute callbacks and validate the records before inserting them into the database - **which is missing in most importing gems**.
6
+
7
+ ### Features
8
+ - Import in batches
9
+ - Validation of the bulk
10
+ - Callback execution
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'rimportor', '~> 0.3'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install rimportor
27
+
28
+ ## Usage
29
+ Rimportor adds to every ActiveRecord model an additional method called rimport. This method then takes a collection of your records you want to persist.
30
+ Let me give you an example.
31
+ ```ruby
32
+ users = []
33
+ 1000.times.each { User.new(some_params) }
34
+ User.rimport users # Imports your collection as a bulk insert to your database
35
+ ```
36
+ But wait... what about validations and callbacks of my bulk?
37
+ Rimportor got you! Just add some configuration options for your rimport.
38
+ Let me show you what i mean.
39
+ ```ruby
40
+ users = []
41
+ 1000.times.each { User.new(some_params) }
42
+
43
+ # true if bulk valid and imported else false
44
+ User.rimport users, before_callbacks: true,
45
+ after_callbacks: true,
46
+ validate_bulk: true
47
+ ```
48
+ The rimport method returns true if your bulk is valid and all callbacks are executed.
49
+ If an error occurs Rimportor won't insert your bulk in the database.
50
+
51
+ And what if i want to insert my records in batches? Rimportor got your back on that too.
52
+ ```ruby
53
+ users = []
54
+ 1000.times.each { User.new(some_params) }
55
+
56
+ # Rimportor will insert the 1000 records in 100 chunks
57
+ User.rimport users, batch_size: 100
58
+ ```
59
+
60
+ ## Benchmarks
61
+
62
+ In progress...
63
+
64
+ ## Development
65
+
66
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
67
+
68
+ 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` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
69
+
70
+ ## Contributing
71
+
72
+ 1. Fork it
73
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
74
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
75
+ 4. Push to the branch (`git push origin my-new-feature`)
76
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Rimportor'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
@@ -0,0 +1,11 @@
1
+ require 'rails/generators'
2
+
3
+ module Rimportor
4
+ class InstallGenerator < ::Rails::Generators::Base
5
+ source_root(File.expand_path(File.dirname(__FILE__)))
6
+
7
+ def copy_initializer
8
+ copy_file '../templates/rimportor.rb', 'config/initializers/rimportor.rb'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,40 @@
1
+ module Rimportor
2
+ module ActiveRecord
3
+ module Adapter
4
+ class Mysql2
5
+
6
+ def max_allowed_packet
7
+ exec_in_pool do |connection|
8
+ result = connection.execute("SHOW VARIABLES like 'max_allowed_packet';")
9
+ val = result.respond_to?(:fetch_row) ? result.fetch_row[1] : result.first[1]
10
+ val.to_i
11
+ end
12
+ end
13
+
14
+ def exec_in_pool
15
+ ::Rimportor::Util::Connection.in_pool do |connection|
16
+ yield(connection)
17
+ end
18
+ end
19
+
20
+ def statement_too_big?(statement)
21
+ statement.size > max_allowed_packet
22
+ end
23
+
24
+ def exec_insert(import_statement)
25
+ insert_statement, value_statements = import_statement
26
+ if statement_too_big? ("#{insert_statement}, #{value_statements.join(',')}")
27
+ puts 'Statement too big'
28
+ else
29
+ exec_statement "#{insert_statement},#{value_statements.join(',')}"
30
+ end
31
+ end
32
+
33
+ def exec_statement(statement)
34
+ exec_in_pool { |connection| connection.execute statement }
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,70 @@
1
+ require 'parallel'
2
+
3
+ module Rimportor
4
+ module ActiveRecord
5
+ class Import
6
+
7
+ def initialize(bulk, adapter, opts = {})
8
+ @bulk = bulk
9
+ @adapter = adapter
10
+ @before_callbacks = !!opts[:before_callbacks]
11
+ @after_callbacks = !!opts[:after_callbacks]
12
+ @validate_bulk = !!opts[:validate_bulk]
13
+ @batch_size = opts[:batch_size] ? opts[:batch_size] : 1000
14
+ @threads = Rimportor.configuration.threads
15
+ end
16
+
17
+ def run_before_callbacks
18
+ ::Parallel.map(@bulk, in_threads: @threads) do |element|
19
+ execute_callbacks(element, :before)
20
+ end
21
+ end
22
+
23
+ def run_after_callbacks
24
+ ::Parallel.map(@bulk, in_threads: @threads) do |element|
25
+ execute_callbacks(element, :after)
26
+ end
27
+ end
28
+
29
+ def run_validations
30
+ validation_result = ::Parallel.map(@bulk, in_threads: @threads) do |element|
31
+ element.valid?
32
+ end.all?
33
+ raise Rimportor::Error::BulkValidation.new("Your bulk is not valid") unless validation_result
34
+ end
35
+
36
+ def execute_callbacks(element, before_or_after)
37
+ case before_or_after
38
+ when :before
39
+ element.run_callbacks(:save) { false }
40
+ when :after
41
+ element.run_callbacks(:save) { true }
42
+ end
43
+ end
44
+
45
+ def import_statement(batch)
46
+ insert_statement = SqlBuilder.new(batch.first).full_insert_statement
47
+ result = ::Parallel.map(batch.drop(1), in_threads: @threads) do |element|
48
+ @adapter.exec_in_pool { SqlBuilder.new(element).partial_insert_statement.gsub('VALUES', '') }
49
+ end
50
+ [insert_statement, result]
51
+ end
52
+
53
+ def exec_statement
54
+ begin
55
+ run_validations if @validate_bulk
56
+ run_before_callbacks if @before_callbacks
57
+ @bulk.each_slice(@batch_size) do |batch|
58
+ @adapter.exec_insert(import_statement(batch))
59
+ end
60
+ run_after_callbacks if @after_callbacks
61
+ true
62
+ rescue => e
63
+ puts "Error importing the bulk. Reason #{e.message}"
64
+ false
65
+ end
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,43 @@
1
+ module Rimportor
2
+ module ActiveRecord
3
+ class SqlBuilder
4
+
5
+ def initialize(model)
6
+ @model = model
7
+ set_timestamps
8
+ end
9
+
10
+ def full_insert_statement
11
+ insert_manager.tap do |im|
12
+ im.insert(arel_for_create)
13
+ end.to_sql
14
+ end
15
+
16
+ def partial_insert_statement
17
+ insert_manager.insert(arel_for_create).to_sql
18
+ end
19
+
20
+ def arel_for_create
21
+ @model.send(:arel_attributes_with_values_for_create, @model.attribute_names)
22
+ end
23
+
24
+ def insert_manager
25
+ @model.class.arel_table.create_insert
26
+ end
27
+
28
+ def set_timestamps
29
+ set_created_at
30
+ set_updated_at
31
+ end
32
+
33
+ def set_created_at
34
+ @model.updated_at = Time.zone.now if @model.respond_to? :updated_at
35
+ end
36
+
37
+ def set_updated_at
38
+ @model.created_at = Time.zone.now if @model.respond_to? :created_at
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,6 @@
1
+ module Rimportor
2
+ module Error
3
+ class BulkValidation < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Rimportor
2
+ module Error
3
+ class InvalidAdapter < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,28 @@
1
+ module Rimportor
2
+ module Plugin
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ end
7
+
8
+ module ClassMethods
9
+ def rimport(records, options = {})
10
+ ::Rimportor::ActiveRecord::Import.new(records, self.current_adapter, options).exec_statement
11
+ end
12
+
13
+ def current_adapter
14
+ load_adapter(::ActiveRecord::Base.connection_config[:adapter])
15
+ end
16
+
17
+ def load_adapter(adapter_name)
18
+ begin
19
+ ::Rimportor::ActiveRecord::Adapter.const_get(adapter_name.to_s.camelize).new
20
+ rescue => e
21
+ raise ::Rimportor::Error::InvalidAdapter.new("Invalid adapter. Reason #{e}")
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ module Rimportor
2
+ module Util
3
+ class Connection
4
+
5
+ def self.in_pool
6
+ ::ActiveRecord::Base.connection_pool.with_connection do |connection|
7
+ yield(connection)
8
+ end
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Rimportor
2
+ VERSION = "0.3"
3
+ end
data/lib/rimportor.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'rimportor/active_record/sql_builder'
2
+ require 'rimportor/active_record/import'
3
+ require 'rimportor/plugin'
4
+ require 'rimportor/error/bulk_validation'
5
+ require 'rimportor/error/invalid_adapter'
6
+ require 'rimportor/active_record/adapter/mysql2'
7
+ require 'rimportor/util/connection'
8
+ require 'generators/install_generator'
9
+
10
+ module Rimportor
11
+ class << self
12
+ attr_accessor :configuration
13
+ end
14
+
15
+ def self.configure
16
+ self.configuration ||= Configuration.new
17
+ yield(configuration) if block_given?
18
+ end
19
+
20
+ class Configuration
21
+ attr_accessor :threads
22
+
23
+ def initialize
24
+ @threads = 4
25
+ end
26
+ end
27
+ end
28
+
29
+ ActiveRecord::Base.send :include, Rimportor::Plugin
@@ -0,0 +1,7 @@
1
+ Rimportor.configure do |config|
2
+ # Configure how many threads rimportor should use for importing.
3
+ # Consider that rimportor will use threads not only for building the statement
4
+ # but also for running validations for your bulk.
5
+ # The default value are 4 threads
6
+ # config.threads = 4
7
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rimportor
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.3'
5
+ platform: ruby
6
+ authors:
7
+ - Erwin Schens
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-11-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 4.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '4.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 4.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: parallel
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.5'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.5.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.5'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.5.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: sqlite3
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ - !ruby/object:Gem::Dependency
68
+ name: rspec-rails
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: 3.0.0
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 3.0.0
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: 3.0.0
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 3.0.0
87
+ description: Fast, modern and concurrent bulk import for ruby on rails.
88
+ email:
89
+ - erwin.schens@qurasoft.de
90
+ executables: []
91
+ extensions: []
92
+ extra_rdoc_files: []
93
+ files:
94
+ - MIT-LICENSE
95
+ - README.md
96
+ - Rakefile
97
+ - lib/generators/install_generator.rb
98
+ - lib/rimportor.rb
99
+ - lib/rimportor/active_record/adapter/mysql2.rb
100
+ - lib/rimportor/active_record/import.rb
101
+ - lib/rimportor/active_record/sql_builder.rb
102
+ - lib/rimportor/error/bulk_validation.rb
103
+ - lib/rimportor/error/invalid_adapter.rb
104
+ - lib/rimportor/plugin.rb
105
+ - lib/rimportor/util/connection.rb
106
+ - lib/rimportor/version.rb
107
+ - lib/templates/rimportor.rb
108
+ homepage: https://github.com/ndea/rimportor
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.4.8
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Fast, modern and concurrent bulk import for ruby on rails.
132
+ test_files: []