bulk_insert 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +96 -0
  4. data/Rakefile +34 -0
  5. data/lib/bulk_insert.rb +24 -0
  6. data/lib/bulk_insert/version.rb +7 -0
  7. data/lib/bulk_insert/worker.rb +64 -0
  8. data/test/bulk_insert/worker_test.rb +84 -0
  9. data/test/bulk_insert_test.rb +22 -0
  10. data/test/dummy/README.rdoc +28 -0
  11. data/test/dummy/Rakefile +6 -0
  12. data/test/dummy/app/assets/javascripts/application.js +13 -0
  13. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  14. data/test/dummy/app/controllers/application_controller.rb +5 -0
  15. data/test/dummy/app/helpers/application_helper.rb +2 -0
  16. data/test/dummy/app/models/testing.rb +2 -0
  17. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  18. data/test/dummy/bin/bundle +3 -0
  19. data/test/dummy/bin/rails +4 -0
  20. data/test/dummy/bin/rake +4 -0
  21. data/test/dummy/bin/setup +29 -0
  22. data/test/dummy/config.ru +4 -0
  23. data/test/dummy/config/application.rb +26 -0
  24. data/test/dummy/config/boot.rb +5 -0
  25. data/test/dummy/config/database.yml +25 -0
  26. data/test/dummy/config/environment.rb +5 -0
  27. data/test/dummy/config/environments/development.rb +41 -0
  28. data/test/dummy/config/environments/production.rb +79 -0
  29. data/test/dummy/config/environments/test.rb +42 -0
  30. data/test/dummy/config/initializers/assets.rb +11 -0
  31. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  32. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  33. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  34. data/test/dummy/config/initializers/inflections.rb +16 -0
  35. data/test/dummy/config/initializers/mime_types.rb +4 -0
  36. data/test/dummy/config/initializers/session_store.rb +3 -0
  37. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  38. data/test/dummy/config/locales/en.yml +23 -0
  39. data/test/dummy/config/routes.rb +56 -0
  40. data/test/dummy/config/secrets.yml +22 -0
  41. data/test/dummy/db/development.sqlite3 +0 -0
  42. data/test/dummy/db/migrate/20151008181535_create_testings.rb +11 -0
  43. data/test/dummy/db/schema.rb +24 -0
  44. data/test/dummy/db/test.sqlite3 +0 -0
  45. data/test/dummy/log/development.log +10 -0
  46. data/test/dummy/log/test.log +1567 -0
  47. data/test/dummy/public/404.html +67 -0
  48. data/test/dummy/public/422.html +67 -0
  49. data/test/dummy/public/500.html +66 -0
  50. data/test/dummy/public/favicon.ico +0 -0
  51. data/test/test_helper.rb +19 -0
  52. metadata +180 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a4e0d183ca8739d8b1fc3a49f9fa202a2e680ab9
4
+ data.tar.gz: c5e41cf70f177ddd5d0dd8238c82cb037c9f0055
5
+ SHA512:
6
+ metadata.gz: 53ceccf9f12ca48381a924f6b7d72b398003b88e17c364700c85293e309cc2db8aa6710db51312614fff4fa9cd1691e8dd5fc089fa55de1c499ab1099716a47b
7
+ data.tar.gz: da31d80b39821b97292d5da6a1ca2f1df6b9b6ced154637ce81472c2fdecb6aa4fc623ec64b5ee15470ac9f0a9f6195bb4261fc0d9940c436931aae9c73a43b7
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Jamis Buck
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.
@@ -0,0 +1,96 @@
1
+ # BulkInsert
2
+
3
+ A little ActiveRecord extension for helping to insert lots of rows in a
4
+ single insert statement.
5
+
6
+ ## Installation
7
+
8
+ Add it to your Gemfile:
9
+
10
+ gem 'bulk_insert'
11
+
12
+ ## Usage
13
+
14
+ BulkInsert adds a new class method to your ActiveRecord models:
15
+
16
+ class Book < ActiveRecord::Base
17
+ end
18
+
19
+ book_attrs = ... # some array of hashes, for instance
20
+ Book.bulk_insert do |worker|
21
+ book_attrs.each do |attrs|
22
+ worker.add(attrs)
23
+ end
24
+ end
25
+
26
+ All of those `#add` calls will be accumulated into a single SQL insert
27
+ statement, vastly improving the performance of multiple sequential
28
+ inserts (think data imports and the like).
29
+
30
+ By default, the columns to be inserted will be all columns in the table,
31
+ minus the `id` column, but if you want, you can explicitly enumerate
32
+ the columns:
33
+
34
+ Book.bulk_insert(:title, :author) do |worker|
35
+ # specify a row as an array of values...
36
+ worker.add ["Eye of the World", "Robert Jordan"]
37
+
38
+ # or as a hash
39
+ worker.add title: "Lord of Light", author: "Roger Zelazny"
40
+ end
41
+
42
+ It will automatically set `created_at`/`updated_at` columns to the current
43
+ date, as well.
44
+
45
+ Book.bulk_insert(:title, :author, :created_at, :updated_at) do |worker|
46
+ # specify created_at/updated_at explicitly...
47
+ worker.add ["The Chosen", "Chaim Potok", Time.now, Time.now]
48
+
49
+ # or let BulkInsert set them by default...
50
+ worker.add ["Hello Ruby", "Linda Liukas"]
51
+ end
52
+
53
+ By default, the batch is always saved when the block finishes, but you
54
+ can explicitly save inside the block whenever you want, by calling
55
+ `#save!` on the worker:
56
+
57
+ Book.bulk_insert do |worker|
58
+ worker.add(...)
59
+ worker.add(...)
60
+
61
+ worker.save!
62
+
63
+ worker.add(...)
64
+ #...
65
+ end
66
+
67
+ That will save the batch as it has been defined to that point, and then
68
+ empty the batch so that you can add more rows to it if you want.
69
+
70
+
71
+ ### Batch Set Size
72
+
73
+ By default, the size of the insert is limited to 500 rows at a time.
74
+ This is called the _set size_. If you add another row that causes the
75
+ set to exceed the set size, the insert statement is automatically built
76
+ and executed, and the batch is reset.
77
+
78
+ If you want a larger (or smaller) set size, you can specify it in
79
+ two ways:
80
+
81
+ # specify set_size when initializing the bulk insert...
82
+ Book.bulk_insert(set_size: 100) do |worker|
83
+ # ...
84
+ end
85
+
86
+ # specify it on the worker directly...
87
+ Book.bulk_insert do |worker|
88
+ worker.set_size = 100
89
+ # ...
90
+ end
91
+
92
+
93
+ ## License
94
+
95
+ BulkInsert is released under the MIT license (see MIT-LICENSE) by
96
+ Jamis Buck (jamis@jamisbuck.org).
@@ -0,0 +1,34 @@
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 = 'BulkInsert'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
@@ -0,0 +1,24 @@
1
+ require 'bulk_insert/worker'
2
+
3
+ module BulkInsert
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def bulk_insert(*columns, set_size:500)
8
+ columns = self.column_names - %w(id) if columns.empty?
9
+ worker = BulkInsert::Worker.new(connection, table_name, columns, set_size)
10
+
11
+ if block_given?
12
+ transaction do
13
+ yield worker
14
+ worker.save!
15
+ end
16
+ self
17
+ else
18
+ worker
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ ActiveRecord::Base.send :include, BulkInsert
@@ -0,0 +1,7 @@
1
+ module BulkInsert
2
+ MAJOR = 1
3
+ MINOR = 0
4
+ TINY = 0
5
+
6
+ VERSION = [MAJOR, MINOR, TINY].join(".")
7
+ end
@@ -0,0 +1,64 @@
1
+ module BulkInsert
2
+ class Worker
3
+ attr_reader :connection
4
+ attr_accessor :set_size
5
+
6
+ def initialize(connection, table_name, column_names, set_size=500)
7
+ @connection = connection
8
+ @set_size = set_size
9
+
10
+ columns = connection.columns(table_name)
11
+ column_map = columns.inject({}) { |h, c| h.update(c.name => c) }
12
+
13
+ @columns = column_names.map { |name| column_map[name.to_s] }
14
+ @table_name = connection.quote_table_name(table_name)
15
+ @column_names = column_names.map { |name| connection.quote_column_name(name) }.join(",")
16
+
17
+ @set = []
18
+ end
19
+
20
+ def pending?
21
+ @set.any?
22
+ end
23
+
24
+ def add(values)
25
+ save! if @set.length >= set_size
26
+
27
+ values = values.with_indifferent_access if values.is_a?(Hash)
28
+ mapped = @columns.map.with_index do |column, index|
29
+ value = values.is_a?(Hash) ? values[column.name] : values[index]
30
+ if value.nil?
31
+ if column.name == "created_at" || column.name == "updated_at"
32
+ Time.now
33
+ else
34
+ value
35
+ end
36
+ else
37
+ value
38
+ end
39
+ end
40
+
41
+ @set.push(mapped)
42
+ end
43
+
44
+ def save!
45
+ if pending?
46
+ sql = "INSERT INTO #{@table_name} (#{@column_names}) VALUES "
47
+
48
+ rows = []
49
+ @set.each do |row|
50
+ values = []
51
+ @columns.zip(row) do |column, value|
52
+ values << @connection.quote(value, column)
53
+ end
54
+ rows << "(#{values.join(',')})"
55
+ end
56
+
57
+ sql << rows.join(",")
58
+ @connection.execute(sql)
59
+
60
+ @set.clear
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,84 @@
1
+ require 'test_helper'
2
+
3
+ class BulkInsertWorkerTest < ActiveSupport::TestCase
4
+ setup do
5
+ @insert = BulkInsert::Worker.new(
6
+ Testing.connection,
7
+ Testing.table_name,
8
+ %w(greeting age happy created_at updated_at))
9
+ @now = Time.now
10
+ end
11
+
12
+ test "empty insert is not pending" do
13
+ assert_equal false, @insert.pending?
14
+ end
15
+
16
+ test "default set size" do
17
+ assert_equal 500, @insert.set_size
18
+ end
19
+
20
+ test "adding row to insert makes insert pending" do
21
+ @insert.add ["Hello", 15, true, @now, @now]
22
+ assert_equal true, @insert.pending?
23
+ end
24
+
25
+ test "add should default timestamp columns to current time" do
26
+ now = Time.now
27
+
28
+ @insert.add ["Hello", 15, true]
29
+ @insert.save!
30
+
31
+ record = Testing.first
32
+ assert_operator record.created_at, :>=, now
33
+ assert_operator record.updated_at, :>=, now
34
+ end
35
+
36
+ test "add should allow values given as Hash" do
37
+ @insert.add greeting: "Yo", age: 20, happy: false, created_at: @now, updated_at: @now
38
+ @insert.save!
39
+
40
+ record = Testing.first
41
+ assert_not_nil record
42
+ assert_equal "Yo", record.greeting
43
+ assert_equal 20, record.age
44
+ assert_equal false, record.happy?
45
+ end
46
+
47
+ test "add should save automatically when overflowing set size" do
48
+ @insert.set_size = 1
49
+ @insert.add ["Hello", 15, true, @now, @now]
50
+ @insert.add ["Yo", 20, false, @now, @now]
51
+ assert_equal 1, Testing.count
52
+ assert_equal "Hello", Testing.first.greeting
53
+ end
54
+
55
+ test "save! makes insert not pending" do
56
+ @insert.add ["Hello", 15, true, @now, @now]
57
+ @insert.save!
58
+ assert_equal false, @insert.pending?
59
+ end
60
+
61
+ test "save! when not pending should do nothing" do
62
+ assert_no_difference 'Testing.count' do
63
+ @insert.save!
64
+ end
65
+ end
66
+
67
+ test "save! inserts pending records" do
68
+ @insert.add ["Yo", 15, false, @now, @now]
69
+ @insert.add ["Hello", 25, true, @now, @now]
70
+ @insert.save!
71
+
72
+ yo = Testing.find_by(greeting: 'Yo')
73
+ hello = Testing.find_by(greeting: 'Hello')
74
+
75
+ assert_not_nil yo
76
+ assert_equal 15, yo.age
77
+ assert_equal false, yo.happy?
78
+
79
+ assert_not_nil hello
80
+ assert_equal 25, hello.age
81
+ assert_equal true, hello.happy?
82
+ end
83
+ end
84
+
@@ -0,0 +1,22 @@
1
+ require 'test_helper'
2
+
3
+ class BulkInsertTest < ActiveSupport::TestCase
4
+ test "bulk_insert without block should return worker" do
5
+ result = Testing.bulk_insert
6
+ assert_kind_of BulkInsert::Worker, result
7
+ end
8
+
9
+ test "bulk_insert with block should yield worker" do
10
+ result = nil
11
+ Testing.bulk_insert { |worker| result = worker }
12
+ assert_kind_of BulkInsert::Worker, result
13
+ end
14
+
15
+ test "bulk_insert with block should save automatically" do
16
+ assert_difference "Testing.count", 1 do
17
+ Testing.bulk_insert do |worker|
18
+ worker.add greeting: "Hello"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,5 @@
1
+ class ApplicationController < ActionController::Base
2
+ # Prevent CSRF attacks by raising an exception.
3
+ # For APIs, you may want to use :null_session instead.
4
+ protect_from_forgery with: :exception
5
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end