bulk_data_methods 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.
- 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
|