bulk_data_methods 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +108 -0
- data/LICENSE +30 -0
- data/README +68 -0
- data/Rakefile +1 -0
- data/bulk_data_methods.gemspec +20 -0
- data/lib/bulk_data_methods/bulk_methods_mixin.rb +228 -0
- data/lib/bulk_data_methods/monkey_patch_postgres.rb +30 -0
- data/lib/bulk_data_methods/version.rb +3 -0
- data/lib/bulk_data_methods.rb +3 -0
- data/spec/bulk_data_methods/bulk_methods_mixin_spec.rb +436 -0
- data/spec/dummy/.rspec +1 -0
- data/spec/dummy/README +1 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +65 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database-sample.yml +32 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/spec/spec_helper.rb +38 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/support/tables_spec_helper.rb +47 -0
- metadata +173 -0
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
2
|
+
#
|
3
|
+
# If you find yourself ignoring temporary files generated by your text editor
|
4
|
+
# or operating system, you probably want to add a global ignore instead:
|
5
|
+
# git config --global core.excludesfile ~/.gitignore_global
|
6
|
+
|
7
|
+
# Ignore bundler config
|
8
|
+
/.bundle
|
9
|
+
|
10
|
+
# Ignore the default SQLite database.
|
11
|
+
/db/*.sqlite3
|
12
|
+
|
13
|
+
# Ignore all logfiles and tempfiles.
|
14
|
+
/log/*.log
|
15
|
+
/tmp
|
16
|
+
.idea/*
|
17
|
+
database.yml
|
18
|
+
/log/*.pid
|
19
|
+
spec/dummy/db/*.sqlite3
|
20
|
+
spec/dummy/log/*.log
|
21
|
+
spec/dummy/tmp/
|
22
|
+
spec/dummy/.sass-cache
|
23
|
+
spec/dummy/config/database.yml
|
24
|
+
spec/dummy/db/schema.rb
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
bulk_data_methods (1.0.0)
|
5
|
+
pg
|
6
|
+
rails (>= 3.0.0)
|
7
|
+
rspec-rails
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
actionmailer (3.2.8)
|
13
|
+
actionpack (= 3.2.8)
|
14
|
+
mail (~> 2.4.4)
|
15
|
+
actionpack (3.2.8)
|
16
|
+
activemodel (= 3.2.8)
|
17
|
+
activesupport (= 3.2.8)
|
18
|
+
builder (~> 3.0.0)
|
19
|
+
erubis (~> 2.7.0)
|
20
|
+
journey (~> 1.0.4)
|
21
|
+
rack (~> 1.4.0)
|
22
|
+
rack-cache (~> 1.2)
|
23
|
+
rack-test (~> 0.6.1)
|
24
|
+
sprockets (~> 2.1.3)
|
25
|
+
activemodel (3.2.8)
|
26
|
+
activesupport (= 3.2.8)
|
27
|
+
builder (~> 3.0.0)
|
28
|
+
activerecord (3.2.8)
|
29
|
+
activemodel (= 3.2.8)
|
30
|
+
activesupport (= 3.2.8)
|
31
|
+
arel (~> 3.0.2)
|
32
|
+
tzinfo (~> 0.3.29)
|
33
|
+
activeresource (3.2.8)
|
34
|
+
activemodel (= 3.2.8)
|
35
|
+
activesupport (= 3.2.8)
|
36
|
+
activesupport (3.2.8)
|
37
|
+
i18n (~> 0.6)
|
38
|
+
multi_json (~> 1.0)
|
39
|
+
arel (3.0.2)
|
40
|
+
builder (3.0.3)
|
41
|
+
diff-lcs (1.1.3)
|
42
|
+
erubis (2.7.0)
|
43
|
+
hike (1.2.1)
|
44
|
+
i18n (0.6.1)
|
45
|
+
journey (1.0.4)
|
46
|
+
json (1.7.5)
|
47
|
+
mail (2.4.4)
|
48
|
+
i18n (>= 0.4.0)
|
49
|
+
mime-types (~> 1.16)
|
50
|
+
treetop (~> 1.4.8)
|
51
|
+
mime-types (1.19)
|
52
|
+
multi_json (1.3.6)
|
53
|
+
pg (0.14.1)
|
54
|
+
polyglot (0.3.3)
|
55
|
+
rack (1.4.1)
|
56
|
+
rack-cache (1.2)
|
57
|
+
rack (>= 0.4)
|
58
|
+
rack-ssl (1.3.2)
|
59
|
+
rack
|
60
|
+
rack-test (0.6.1)
|
61
|
+
rack (>= 1.0)
|
62
|
+
rails (3.2.8)
|
63
|
+
actionmailer (= 3.2.8)
|
64
|
+
actionpack (= 3.2.8)
|
65
|
+
activerecord (= 3.2.8)
|
66
|
+
activeresource (= 3.2.8)
|
67
|
+
activesupport (= 3.2.8)
|
68
|
+
bundler (~> 1.0)
|
69
|
+
railties (= 3.2.8)
|
70
|
+
railties (3.2.8)
|
71
|
+
actionpack (= 3.2.8)
|
72
|
+
activesupport (= 3.2.8)
|
73
|
+
rack-ssl (~> 1.3.2)
|
74
|
+
rake (>= 0.8.7)
|
75
|
+
rdoc (~> 3.4)
|
76
|
+
thor (>= 0.14.6, < 2.0)
|
77
|
+
rake (0.9.2.2)
|
78
|
+
rdoc (3.12)
|
79
|
+
json (~> 1.4)
|
80
|
+
rspec (2.11.0)
|
81
|
+
rspec-core (~> 2.11.0)
|
82
|
+
rspec-expectations (~> 2.11.0)
|
83
|
+
rspec-mocks (~> 2.11.0)
|
84
|
+
rspec-core (2.11.1)
|
85
|
+
rspec-expectations (2.11.3)
|
86
|
+
diff-lcs (~> 1.1.3)
|
87
|
+
rspec-mocks (2.11.3)
|
88
|
+
rspec-rails (2.11.0)
|
89
|
+
actionpack (>= 3.0)
|
90
|
+
activesupport (>= 3.0)
|
91
|
+
railties (>= 3.0)
|
92
|
+
rspec (~> 2.11.0)
|
93
|
+
sprockets (2.1.3)
|
94
|
+
hike (~> 1.2)
|
95
|
+
rack (~> 1.0)
|
96
|
+
tilt (~> 1.1, != 1.3.0)
|
97
|
+
thor (0.16.0)
|
98
|
+
tilt (1.3.3)
|
99
|
+
treetop (1.4.10)
|
100
|
+
polyglot
|
101
|
+
polyglot (>= 0.3.1)
|
102
|
+
tzinfo (0.3.33)
|
103
|
+
|
104
|
+
PLATFORMS
|
105
|
+
ruby
|
106
|
+
|
107
|
+
DEPENDENCIES
|
108
|
+
bulk_data_methods!
|
data/LICENSE
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
Copyright (c) 2010-2012, Fiksu, Inc.
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are
|
6
|
+
met:
|
7
|
+
|
8
|
+
o Redistributions of source code must retain the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer.
|
10
|
+
|
11
|
+
o Redistributions in binary form must reproduce the above copyright
|
12
|
+
notice, this list of conditions and the following disclaimer in the
|
13
|
+
documentation and/or other materials provided with the
|
14
|
+
distribution.
|
15
|
+
|
16
|
+
o Fiksu, Inc. nor the names of its contributors may be used to
|
17
|
+
endorse or promote products derived from this software without
|
18
|
+
specific prior written permission.
|
19
|
+
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
21
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
22
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
23
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
24
|
+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
25
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
26
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
27
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
28
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
29
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
bulk_data_methods
|
2
|
+
==================
|
3
|
+
MixIn used to extend ActiveRecord::Base classes implementing bulk insert and update operations
|
4
|
+
through {#create_many} and {#update_many}.
|
5
|
+
|
6
|
+
Examples
|
7
|
+
========
|
8
|
+
|
9
|
+
class Company < ActiveRecord::Base
|
10
|
+
extend BulkMethodsMixin
|
11
|
+
end
|
12
|
+
__________________________
|
13
|
+
BULK creation of many rows:
|
14
|
+
|
15
|
+
example no options used
|
16
|
+
rows = [
|
17
|
+
{ :name => 'Keith', :salary => 1000 },
|
18
|
+
{ :name => 'Alex', :salary => 2000 }
|
19
|
+
]
|
20
|
+
Employee.create_many(rows)
|
21
|
+
|
22
|
+
example with :returning option to returns key value
|
23
|
+
rows = [
|
24
|
+
{ :name => 'Keith', :salary => 1000 },
|
25
|
+
{ :name => 'Alex', :salary => 2000 }
|
26
|
+
]
|
27
|
+
options = { :returning => [:id] }
|
28
|
+
Employee.create_many(rows, options)
|
29
|
+
|
30
|
+
example with :slice_size option (will generate two insert queries)
|
31
|
+
rows = [
|
32
|
+
{ :name => 'Keith', :salary => 1000 },
|
33
|
+
{ :name => 'Alex', :salary => 2000 },
|
34
|
+
{ :name => 'Mark', :salary => 3000 }
|
35
|
+
]
|
36
|
+
options = { :slice_size => 2 }
|
37
|
+
Employee.create_many(rows, options)
|
38
|
+
_________________________
|
39
|
+
BULK updates of many rows:
|
40
|
+
|
41
|
+
example using "set_array" to add the value of "salary" to the specific employee's salary the default where clause matches IDs so, it works here.
|
42
|
+
rows = [
|
43
|
+
{ :id => 1, :salary => 1000 },
|
44
|
+
{ :id => 10, :salary => 2000 },
|
45
|
+
{ :id => 23, :salary => 2500 }
|
46
|
+
]
|
47
|
+
options = { :set_array => '"salary = datatable.salary"' }
|
48
|
+
Employee.update_many(rows, options)
|
49
|
+
|
50
|
+
example using where clause to match salary.
|
51
|
+
rows = [
|
52
|
+
{ :id => 1, :salary => 1000, :company_id => 10 },
|
53
|
+
{ :id => 10, :salary => 2000, :company_id => 12 },
|
54
|
+
{ :id => 23, :salary => 2500, :company_id => 5 }
|
55
|
+
]
|
56
|
+
options = {
|
57
|
+
:set_array => '"company_id = datatable.company_id"',
|
58
|
+
:where => '"#{table_name}.salary = datatable.salary"'
|
59
|
+
}
|
60
|
+
Employee.update_many(rows, options)
|
61
|
+
|
62
|
+
example setting where clause to the KEY of the hash passed in and the set_array is generated from the VALUES
|
63
|
+
rows = {
|
64
|
+
{ :id => 1 } => { :salary => 100000, :company_id => 10 },
|
65
|
+
{ :id => 10 } => { :salary => 110000, :company_id => 12 },
|
66
|
+
{ :id => 23 } => { :salary => 90000, :company_id => 5 }
|
67
|
+
}
|
68
|
+
Employee.update_many(rows)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,20 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path("../lib", __FILE__)
|
2
|
+
require 'bulk_data_methods/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "bulk_data_methods"
|
6
|
+
s.version = BulkDataMethods::VERSION
|
7
|
+
s.license = 'New BSD License'
|
8
|
+
s.date = '2012-09-21'
|
9
|
+
s.summary = 'MixIn used to extend ActiveRecord::Base classes implementing bulk insert and update operations through {#create_many} and {#update_many}.'
|
10
|
+
s.description = 'MixIn used to extend ActiveRecord::Base classes implementing bulk insert and update operations through {#create_many} and {#update_many}.'
|
11
|
+
s.authors = ["Keith Gabryelski"]
|
12
|
+
s.email = 'keith@fiksu.com'
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.require_path = 'lib'
|
16
|
+
s.homepage = 'http://github.com/fiksu/bulk_data_methods'
|
17
|
+
s.add_dependency "pg"
|
18
|
+
s.add_dependency "rails", '>= 3.0.0'
|
19
|
+
s.add_dependency 'rspec-rails'
|
20
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
# MixIn used to extend ActiveRecord::Base classes implementing bulk insert and update operations
|
2
|
+
# through {#create_many} and {#update_many}.
|
3
|
+
# @example to use:
|
4
|
+
# class Company < ActiveRecord::Base
|
5
|
+
# extend BulkMethodsMixin
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
module BulkMethodsMixin
|
9
|
+
# exception thrown when row data structures are inconsistent between rows in single call to {#create_many} or {#update_many}
|
10
|
+
class BulkUploadDataInconsistent < StandardError
|
11
|
+
def initialize(model, table_name, expected_columns, found_columns, while_doing)
|
12
|
+
super("#{model.name}: for table: #{table_name}; #{expected_columns} != #{found_columns}; #{while_doing}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# BULK creation of many rows
|
17
|
+
#
|
18
|
+
# @example no options used
|
19
|
+
# rows = [
|
20
|
+
# { :name => 'Keith', :salary => 1000 },
|
21
|
+
# { :name => 'Alex', :salary => 2000 }
|
22
|
+
# ]
|
23
|
+
# Employee.create_many(rows)
|
24
|
+
#
|
25
|
+
# @example with :returning option to returns key value
|
26
|
+
# rows = [
|
27
|
+
# { :name => 'Keith', :salary => 1000 },
|
28
|
+
# { :name => 'Alex', :salary => 2000 }
|
29
|
+
# ]
|
30
|
+
# options = { :returning => [:id] }
|
31
|
+
# Employee.create_many(rows, options)
|
32
|
+
# [#<Employee id: 1>, #<Employee id: 2>]
|
33
|
+
#
|
34
|
+
# @example with :slice_size option (will generate two insert queries)
|
35
|
+
# rows = [
|
36
|
+
# { :name => 'Keith', :salary => 1000 },
|
37
|
+
# { :name => 'Alex', :salary => 2000 },
|
38
|
+
# { :name => 'Mark', :salary => 3000 }
|
39
|
+
# ]
|
40
|
+
# options = { :slice_size => 2 }
|
41
|
+
# Employee.create_many(rows, options)
|
42
|
+
#
|
43
|
+
# @param [Array<Hash>] rows ([]) data to be inserted into database
|
44
|
+
# @param [Hash] options ({}) options for bulk inserts
|
45
|
+
# @option options [Integer] :slice_size (1000) how many records will be created in a single SQL query
|
46
|
+
# @option options [Boolean] :check_consitency (true) ensure some modicum of sanity on the incoming dataset, specifically: does each row define the same set of key/value pairs
|
47
|
+
# @option options [Array or String] :returning (nil) list of fields to return.
|
48
|
+
# @return [Array<Hash>] rows returned from DB as option[:returning] requests
|
49
|
+
# @raise [BulkUploadDataInconsistent] raised when key/value pairs between rows are inconsistent (check disabled with option :check_consistency)
|
50
|
+
def create_many(rows, options = {})
|
51
|
+
return [] if rows.blank?
|
52
|
+
options[:slice_size] = 1000 unless options.has_key?(:slice_size)
|
53
|
+
options[:check_consistency] = true unless options.has_key?(:check_consistency)
|
54
|
+
returning_clause = ""
|
55
|
+
if options[:returning]
|
56
|
+
if options[:returning].is_a? Array
|
57
|
+
returning_list = options[:returning].join(',')
|
58
|
+
else
|
59
|
+
returning_list = options[:returning]
|
60
|
+
end
|
61
|
+
returning_clause = " returning #{returning_list}"
|
62
|
+
end
|
63
|
+
returning = []
|
64
|
+
|
65
|
+
created_at_value = Time.zone.now
|
66
|
+
|
67
|
+
num_sequences_needed = rows.reject{|r| r[:id].present?}.length
|
68
|
+
if num_sequences_needed > 0
|
69
|
+
row_ids = connection.next_sequence_values(sequence_name, num_sequences_needed)
|
70
|
+
else
|
71
|
+
row_ids = []
|
72
|
+
end
|
73
|
+
rows.each do |row|
|
74
|
+
# set the primary key if it needs to be set
|
75
|
+
row[:id] ||= row_ids.shift
|
76
|
+
end.each do |row|
|
77
|
+
# set :created_at if need be
|
78
|
+
row[:created_at] ||= created_at_value
|
79
|
+
end.group_by do |row|
|
80
|
+
respond_to?(:partition_table_name) ? partition_table_name(*partition_key_values(row)) : table_name
|
81
|
+
end.each do |table_name, rows_for_table|
|
82
|
+
column_names = rows_for_table[0].keys.sort{|a,b| a.to_s <=> b.to_s}
|
83
|
+
sql_insert_string = "insert into #{table_name} (#{column_names.join(',')}) values "
|
84
|
+
rows_for_table.map do |row|
|
85
|
+
if options[:check_consistency]
|
86
|
+
row_column_names = row.keys.sort{|a,b| a.to_s <=> b.to_s}
|
87
|
+
if column_names != row_column_names
|
88
|
+
raise BulkUploadDataInconsistent.new(self, table_name, column_names, row_column_names, "while attempting to build insert statement")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
column_values = column_names.map do |column_name|
|
92
|
+
quote_value(row[column_name], columns_hash[column_name.to_s])
|
93
|
+
end.join(',')
|
94
|
+
"(#{column_values})"
|
95
|
+
end.each_slice(options[:slice_size]) do |insert_slice|
|
96
|
+
returning += find_by_sql(sql_insert_string + insert_slice.join(',') + returning_clause)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
return returning
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# BULK updates of many rows
|
104
|
+
#
|
105
|
+
# @return [Array<Hash>] rows returned from DB as option[:returning] requests
|
106
|
+
# @raise [BulkUploadDataInconsistent] raised when key/value pairs between rows are inconsistent (check disabled with option :check_consistency)
|
107
|
+
# @param [Hash] options ({}) options for bulk inserts
|
108
|
+
# @option options [Integer] :slice_size (1000) how many records will be created in a single SQL query
|
109
|
+
# @option options [Boolean] :check_consitency (true) ensure some modicum of sanity on the incoming dataset, specifically: does each row define the same set of key/value pairs
|
110
|
+
# @option options [Array] :returning (nil) list of fields to return.
|
111
|
+
# @option options [String] :returning (nil) single field to return.
|
112
|
+
#
|
113
|
+
# @overload update_many(rows = [], options = {})
|
114
|
+
# @param [Array<Hash>] rows ([]) data to be updated
|
115
|
+
# @option options [String] :set_array (built from first row passed in) the set clause
|
116
|
+
# @option options [String] :where ('"#{table_name}.id = datatable.id"') the where clause
|
117
|
+
#
|
118
|
+
# @overload update_many(rows = {}, options = {})
|
119
|
+
# @param [Hash<Hash, Hash>] rows ({}) data to be updated
|
120
|
+
# @option options [String] :set_array (built from the values in the first key/value pair of `rows`) the set clause
|
121
|
+
# @option options [String] :where (built from the keys in the first key/value pair of `rows`) the where clause
|
122
|
+
#
|
123
|
+
# @example using "set_array" to add the value of "salary" to the specific employee's salary the default where clause matches IDs so, it works here.
|
124
|
+
# rows = [
|
125
|
+
# { :id => 1, :salary => 1000 },
|
126
|
+
# { :id => 10, :salary => 2000 },
|
127
|
+
# { :id => 23, :salary => 2500 }
|
128
|
+
# ]
|
129
|
+
# options = { :set_array => '"salary = datatable.salary"' }
|
130
|
+
# Employee.update_many(rows, options)
|
131
|
+
#
|
132
|
+
# @example using where clause to match salary.
|
133
|
+
# rows = [
|
134
|
+
# { :id => 1, :salary => 1000, :company_id => 10 },
|
135
|
+
# { :id => 10, :salary => 2000, :company_id => 12 },
|
136
|
+
# { :id => 23, :salary => 2500, :company_id => 5 }
|
137
|
+
# ]
|
138
|
+
# options = {
|
139
|
+
# :set_array => '"company_id = datatable.company_id"',
|
140
|
+
# :where => '"#{table_name}.salary = datatable.salary"'
|
141
|
+
# }
|
142
|
+
# Employee.update_many(rows, options)
|
143
|
+
#
|
144
|
+
# @example setting where clause to the KEY of the hash passed in and the set_array is generated from the VALUES
|
145
|
+
# rows = {
|
146
|
+
# { :id => 1 } => { :salary => 100000, :company_id => 10 },
|
147
|
+
# { :id => 10 } => { :salary => 110000, :company_id => 12 },
|
148
|
+
# { :id => 23 } => { :salary => 90000, :company_id => 5 }
|
149
|
+
# }
|
150
|
+
# Employee.update_many(rows)
|
151
|
+
#
|
152
|
+
# @note Remember that you should probably set updated_at using "updated = datatable.updated_at"
|
153
|
+
# or "updated_at = now()" in the set_array if you want to follow
|
154
|
+
# the standard active record model for time columns (and you have an updated_at column)
|
155
|
+
def update_many(rows, options = {})
|
156
|
+
return [] if rows.blank?
|
157
|
+
if rows.is_a?(Hash)
|
158
|
+
options[:where] = '"' + rows.keys[0].keys.map{|key| '#{table_name}.' + "#{key} = datatable.#{key}"}.join(' and ') + '"'
|
159
|
+
options[:set_array] = '"' + rows.values[0].keys.map{|key| "#{key} = datatable.#{key}"}.join(',') + '"' unless options[:set_array]
|
160
|
+
r = []
|
161
|
+
rows.each do |key,value|
|
162
|
+
r << key.merge(value)
|
163
|
+
end
|
164
|
+
rows = r
|
165
|
+
end
|
166
|
+
unless options[:set_array]
|
167
|
+
column_names = rows[0].keys
|
168
|
+
columns_to_remove = [:id]
|
169
|
+
columns_to_remove += [partition_keys].map{|k| k.to_sym} if respond_to?(:partition_keys)
|
170
|
+
options[:set_array] = '"' + (column_names - columns_to_remove.flatten).map{|cn| "#{cn} = datatable.#{cn}"}.join(',') + '"'
|
171
|
+
end
|
172
|
+
options[:slice_size] = 1000 unless options[:slice_size]
|
173
|
+
options[:check_consistency] = true unless options.has_key?(:check_consistency)
|
174
|
+
returning_clause = ""
|
175
|
+
if options[:returning]
|
176
|
+
if options[:returning].is_a?(Array)
|
177
|
+
returning_list = options[:returning].map{|r| '#{table_name}.' + r.to_s}.join(',')
|
178
|
+
else
|
179
|
+
returning_list = options[:returning]
|
180
|
+
end
|
181
|
+
returning_clause = "\" returning #{returning_list}\""
|
182
|
+
end
|
183
|
+
options[:where] = '"#{table_name}.id = datatable.id"' unless options[:where]
|
184
|
+
|
185
|
+
returning = []
|
186
|
+
|
187
|
+
rows.group_by do |row|
|
188
|
+
respond_to?(:partition_table_name) ? partition_table_name(*partition_key_values(row)) : table_name
|
189
|
+
end.each do |table_name, rows_for_table|
|
190
|
+
column_names = rows_for_table[0].keys.sort{|a,b| a.to_s <=> b.to_s}
|
191
|
+
rows_for_table.each_slice(options[:slice_size]) do |update_slice|
|
192
|
+
datatable_rows = []
|
193
|
+
update_slice.each_with_index do |row,i|
|
194
|
+
if options[:check_consistency]
|
195
|
+
row_column_names = row.keys.sort{|a,b| a.to_s <=> b.to_s}
|
196
|
+
if column_names != row_column_names
|
197
|
+
raise BulkUploadDataInconsistent.new(self, table_name, column_names, row_column_names, "while attempting to build update statement")
|
198
|
+
end
|
199
|
+
end
|
200
|
+
datatable_rows << row.map do |column_name,column_value|
|
201
|
+
column_name = column_name.to_s
|
202
|
+
columns_hash_value = columns_hash[column_name]
|
203
|
+
if i == 0
|
204
|
+
"#{quote_value(column_value, columns_hash_value)}::#{columns_hash_value.sql_type} as #{column_name}"
|
205
|
+
else
|
206
|
+
quote_value(column_value, columns_hash_value)
|
207
|
+
end
|
208
|
+
end.join(',')
|
209
|
+
end
|
210
|
+
datatable = datatable_rows.join(' union select ')
|
211
|
+
|
212
|
+
sql_update_string = <<-SQL
|
213
|
+
update #{table_name} set
|
214
|
+
#{eval(options[:set_array])}
|
215
|
+
from
|
216
|
+
(select
|
217
|
+
#{datatable}
|
218
|
+
) as datatable
|
219
|
+
where
|
220
|
+
#{eval(options[:where])}
|
221
|
+
#{eval(returning_clause)}
|
222
|
+
SQL
|
223
|
+
returning += find_by_sql(sql_update_string)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
return returning
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/base'
|
3
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
4
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
5
|
+
|
6
|
+
module ActiveRecord::ConnectionAdapters
|
7
|
+
class PostgreSQLAdapter < AbstractAdapter
|
8
|
+
#
|
9
|
+
# Get the next value in a sequence. Used on INSERT operation for
|
10
|
+
# partitioning like by_id because the ID is required before the insert
|
11
|
+
# so that the specific child table is known ahead of time.
|
12
|
+
#
|
13
|
+
# @param [String] sequence_name the name of the sequence to fetch the next value from
|
14
|
+
# @return [Integer] the value from the sequence
|
15
|
+
def next_sequence_value(sequence_name)
|
16
|
+
return execute("select nextval('#{sequence_name}')").field_values("nextval").first.to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Get the some next values in a sequence.
|
21
|
+
#
|
22
|
+
# @param [String] sequence_name the name of the sequence to fetch the next values from
|
23
|
+
# @param [Integer] batch_size count of values.
|
24
|
+
# @return [Array<Integer>] an array of values from the sequence
|
25
|
+
def next_sequence_values(sequence_name, batch_size)
|
26
|
+
result = execute("select nextval('#{sequence_name}') from generate_series(1, #{batch_size})")
|
27
|
+
return result.field_values("nextval").map(&:to_i)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|