partitioned 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +17 -0
- data/LICENSE +30 -0
- data/PARTITIONING_EXPLAINED.txt +351 -0
- data/README +111 -0
- data/Rakefile +27 -0
- data/examples/README +23 -0
- data/examples/company_id.rb +417 -0
- data/examples/company_id_and_created_at.rb +689 -0
- data/examples/created_at.rb +590 -0
- data/examples/created_at_referencing_awards.rb +1000 -0
- data/examples/id.rb +475 -0
- data/examples/lib/by_company_id.rb +11 -0
- data/examples/lib/command_line_tool_mixin.rb +71 -0
- data/examples/lib/company.rb +29 -0
- data/examples/lib/get_options.rb +44 -0
- data/examples/lib/roman.rb +41 -0
- data/examples/start_date.rb +621 -0
- data/init.rb +1 -0
- data/lib/monkey_patch_activerecord.rb +92 -0
- data/lib/monkey_patch_postgres.rb +73 -0
- data/lib/partitioned.rb +26 -0
- data/lib/partitioned/active_record_overrides.rb +34 -0
- data/lib/partitioned/bulk_methods_mixin.rb +288 -0
- data/lib/partitioned/by_created_at.rb +13 -0
- data/lib/partitioned/by_foreign_key.rb +21 -0
- data/lib/partitioned/by_id.rb +35 -0
- data/lib/partitioned/by_integer_field.rb +32 -0
- data/lib/partitioned/by_monthly_time_field.rb +23 -0
- data/lib/partitioned/by_time_field.rb +65 -0
- data/lib/partitioned/by_weekly_time_field.rb +30 -0
- data/lib/partitioned/multi_level.rb +20 -0
- data/lib/partitioned/multi_level/configurator/data.rb +14 -0
- data/lib/partitioned/multi_level/configurator/dsl.rb +32 -0
- data/lib/partitioned/multi_level/configurator/reader.rb +162 -0
- data/lib/partitioned/multi_level/partition_manager.rb +47 -0
- data/lib/partitioned/partitioned_base.rb +354 -0
- data/lib/partitioned/partitioned_base/configurator.rb +6 -0
- data/lib/partitioned/partitioned_base/configurator/data.rb +62 -0
- data/lib/partitioned/partitioned_base/configurator/dsl.rb +628 -0
- data/lib/partitioned/partitioned_base/configurator/reader.rb +209 -0
- data/lib/partitioned/partitioned_base/partition_manager.rb +138 -0
- data/lib/partitioned/partitioned_base/sql_adapter.rb +286 -0
- data/lib/partitioned/version.rb +3 -0
- data/lib/tasks/desirable_tasks.rake +4 -0
- data/partitioned.gemspec +21 -0
- data/spec/dummy/.rspec +1 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +9 -0
- data/spec/dummy/app/assets/stylesheets/application.css +7 -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.ru +4 -0
- data/spec/dummy/config/application.rb +51 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +32 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +30 -0
- data/spec/dummy/config/environments/production.rb +60 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -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/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/spec/spec_helper.rb +27 -0
- data/spec/monkey_patch_posgres_spec.rb +176 -0
- data/spec/partitioned/bulk_methods_mixin_spec.rb +512 -0
- data/spec/partitioned/by_created_at_spec.rb +62 -0
- data/spec/partitioned/by_foreign_key_spec.rb +95 -0
- data/spec/partitioned/by_id_spec.rb +97 -0
- data/spec/partitioned/by_integer_field_spec.rb +143 -0
- data/spec/partitioned/by_monthly_time_field_spec.rb +100 -0
- data/spec/partitioned/by_time_field_spec.rb +182 -0
- data/spec/partitioned/by_weekly_time_field_spec.rb +100 -0
- data/spec/partitioned/multi_level/configurator/dsl_spec.rb +88 -0
- data/spec/partitioned/multi_level/configurator/reader_spec.rb +147 -0
- data/spec/partitioned/partitioned_base/configurator/dsl_spec.rb +459 -0
- data/spec/partitioned/partitioned_base/configurator/reader_spec.rb +513 -0
- data/spec/partitioned/partitioned_base/sql_adapter_spec.rb +204 -0
- data/spec/partitioned/partitioned_base_spec.rb +173 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/shared_example_spec_helper_for_integer_key.rb +137 -0
- data/spec/support/shared_example_spec_helper_for_time_key.rb +147 -0
- data/spec/support/tables_spec_helper.rb +47 -0
- metadata +250 -0
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'partitioned.rb'
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/base'
|
3
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
4
|
+
require 'active_record/relation.rb'
|
5
|
+
require 'active_record/persistence.rb'
|
6
|
+
|
7
|
+
#
|
8
|
+
# patching activerecord to allow specifying the table name as a function of
|
9
|
+
# attributes
|
10
|
+
#
|
11
|
+
module ActiveRecord
|
12
|
+
module Persistence
|
13
|
+
def create
|
14
|
+
if self.id.nil? && self.class.respond_to?(:prefetch_primary_key?) && self.class.prefetch_primary_key?
|
15
|
+
self.id = connection.next_sequence_value(self.class.sequence_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
attributes_values = arel_attributes_values(!id.nil?)
|
19
|
+
|
20
|
+
new_id = self.class.unscoped.insert attributes_values
|
21
|
+
|
22
|
+
self.id ||= new_id
|
23
|
+
|
24
|
+
IdentityMap.add(self) if IdentityMap.enabled?
|
25
|
+
@new_record = false
|
26
|
+
id
|
27
|
+
end
|
28
|
+
end
|
29
|
+
#
|
30
|
+
# patches for relation to allow back hooks into the activerecord
|
31
|
+
# requesting name of table as a function of attributes
|
32
|
+
#
|
33
|
+
class Relation
|
34
|
+
#
|
35
|
+
# patches activerecord's building of an insert statement to request
|
36
|
+
# of the model a table name with respect to attribute values being
|
37
|
+
# inserted
|
38
|
+
#
|
39
|
+
# the differences between this and the original code are small and marked
|
40
|
+
# with PARTITIONED comment
|
41
|
+
def insert(values)
|
42
|
+
primary_key_value = nil
|
43
|
+
|
44
|
+
if primary_key && Hash === values
|
45
|
+
primary_key_value = values[values.keys.find { |k|
|
46
|
+
k.name == primary_key
|
47
|
+
}]
|
48
|
+
|
49
|
+
if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
|
50
|
+
primary_key_value = connection.next_sequence_value(klass.sequence_name)
|
51
|
+
values[klass.arel_table[klass.primary_key]] = primary_key_value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
im = arel.create_insert
|
56
|
+
#
|
57
|
+
# PARTITIONED ADDITION. get arel_table from class with respect to the
|
58
|
+
# current values to placed in the table (which hopefully hold the values
|
59
|
+
# that are used to determine the child table this insert should be
|
60
|
+
# redirected to)
|
61
|
+
#
|
62
|
+
actual_arel_table = @klass.dynamic_arel_table(Hash[*values.map{|k,v| [k.name,v]}.flatten]) if @klass.respond_to? :dynamic_arel_table
|
63
|
+
actual_arel_table = @table unless actual_arel_table
|
64
|
+
im.into actual_arel_table
|
65
|
+
|
66
|
+
conn = @klass.connection
|
67
|
+
|
68
|
+
substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
|
69
|
+
binds = substitutes.map do |arel_attr, value|
|
70
|
+
[@klass.columns_hash[arel_attr.name], value]
|
71
|
+
end
|
72
|
+
|
73
|
+
substitutes.each_with_index do |tuple, i|
|
74
|
+
tuple[1] = conn.substitute_at(binds[i][0], i)
|
75
|
+
end
|
76
|
+
|
77
|
+
if values.empty? # empty insert
|
78
|
+
im.values = Arel.sql(connection.empty_insert_statement_value)
|
79
|
+
else
|
80
|
+
im.insert substitutes
|
81
|
+
end
|
82
|
+
|
83
|
+
conn.insert(
|
84
|
+
im,
|
85
|
+
'SQL',
|
86
|
+
primary_key,
|
87
|
+
primary_key_value,
|
88
|
+
nil,
|
89
|
+
binds)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/base'
|
3
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
4
|
+
|
5
|
+
module ActiveRecord::ConnectionAdapters
|
6
|
+
class TableDefinition
|
7
|
+
def check_constraint(constraint)
|
8
|
+
@columns << Struct.new(:to_sql).new("CHECK (#{constraint})")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class PostgreSQLAdapter < AbstractAdapter
|
13
|
+
#
|
14
|
+
# get the next value in a sequence. used on INSERT operation for
|
15
|
+
# partitioning like by_id because the ID is required before the insert
|
16
|
+
# so that the specific child table is known ahead of time.
|
17
|
+
#
|
18
|
+
def next_sequence_value(sequence_name)
|
19
|
+
return execute("select nextval('#{sequence_name}')").field_values("nextval").first
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# get the some next values in a sequence.
|
24
|
+
# batch_size - count of values
|
25
|
+
#
|
26
|
+
def next_sequence_values(sequence_name, batch_size)
|
27
|
+
result = execute("select nextval('#{sequence_name}') from generate_series(1, #{batch_size})")
|
28
|
+
return result.field_values("nextval").map(&:to_i)
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# causes active resource to fetch the primary key for the table (using next_sequence_value())
|
33
|
+
# just before an insert. We need the prefetch to happen but we don't have enough information
|
34
|
+
# here to determine if it should happen, so Relation::insert has been modified to request of
|
35
|
+
# the ActiveRecord::Base derived class if it requires a prefetch.
|
36
|
+
#
|
37
|
+
def prefetch_primary_key?(table_name)
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# creates a schema given a name.
|
43
|
+
# options:
|
44
|
+
# :unless_exists - check if schema exists.
|
45
|
+
#
|
46
|
+
def create_schema(name, options = {})
|
47
|
+
if options[:unless_exists]
|
48
|
+
return if execute("select count(*) from pg_namespace where nspname = '#{name}'").getvalue(0,0).to_i > 0
|
49
|
+
end
|
50
|
+
execute("CREATE SCHEMA #{name}")
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# drop a schema given a name.
|
55
|
+
# options:
|
56
|
+
# :if_exists - check if schema exists.
|
57
|
+
# :cascade - cascade drop to dependant objects
|
58
|
+
#
|
59
|
+
def drop_schema(name, options = {})
|
60
|
+
if options[:if_exists]
|
61
|
+
return if execute("select count(*) from pg_namespace where nspname = '#{name}'").getvalue(0,0).to_i == 0
|
62
|
+
end
|
63
|
+
execute("DROP SCHEMA #{name}#{' cascade' if options[:cascade]}")
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# add foreign key constraint to table.
|
68
|
+
#
|
69
|
+
def add_foreign_key(referencing_table_name, referencing_field_name, referenced_table_name, referenced_field_name = :id)
|
70
|
+
execute("ALTER TABLE #{referencing_table_name} add foreign key (#{referencing_field_name}) references #{referenced_table_name}(#{referenced_field_name})")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/partitioned.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'monkey_patch_activerecord'
|
2
|
+
require 'monkey_patch_postgres'
|
3
|
+
|
4
|
+
require 'partitioned/bulk_methods_mixin'
|
5
|
+
require 'partitioned/active_record_overrides'
|
6
|
+
require 'partitioned/partitioned_base/configurator.rb'
|
7
|
+
require 'partitioned/partitioned_base/configurator/data'
|
8
|
+
require 'partitioned/partitioned_base/configurator/dsl'
|
9
|
+
require 'partitioned/partitioned_base.rb'
|
10
|
+
require 'partitioned/partitioned_base/configurator/reader'
|
11
|
+
require 'partitioned/partitioned_base/partition_manager'
|
12
|
+
require 'partitioned/partitioned_base/sql_adapter'
|
13
|
+
|
14
|
+
require 'partitioned/by_time_field'
|
15
|
+
require 'partitioned/by_monthly_time_field'
|
16
|
+
require 'partitioned/by_weekly_time_field'
|
17
|
+
require 'partitioned/by_created_at'
|
18
|
+
require 'partitioned/by_integer_field'
|
19
|
+
require 'partitioned/by_id'
|
20
|
+
require 'partitioned/by_foreign_key'
|
21
|
+
|
22
|
+
require 'partitioned/multi_level'
|
23
|
+
require 'partitioned/multi_level/configurator/data'
|
24
|
+
require 'partitioned/multi_level/configurator/dsl'
|
25
|
+
require 'partitioned/multi_level/configurator/reader'
|
26
|
+
require 'partitioned/multi_level/partition_manager'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#
|
2
|
+
# these are things our base class must fix in ActiveRecord::Base
|
3
|
+
#
|
4
|
+
# no need to monkey patch these, just override them.
|
5
|
+
#
|
6
|
+
module Partitioned
|
7
|
+
module ActiveRecordOverrides
|
8
|
+
#
|
9
|
+
# arel_attribute_values needs to return attributes (and their values) associated with the dynamic_arel_table instead of the
|
10
|
+
# static arel_table provided by ActiveRecord.
|
11
|
+
#
|
12
|
+
# the standard release of this function gathers a collection of attributes and creates a wrapper function around them
|
13
|
+
# that names the table they are associated with. that naming is incorrect for partitioned tables.
|
14
|
+
#
|
15
|
+
# we call the standard release's method then retrofit our partitioned table into the hash that is returned.
|
16
|
+
#
|
17
|
+
def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
|
18
|
+
attrs = super
|
19
|
+
actual_arel_table = dynamic_arel_table(self.class.table_name)
|
20
|
+
return Hash[*attrs.map{|k,v| [actual_arel_table[k.name], v]}.flatten]
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# delete just needs a wrapper around it to specify the specific partition.
|
25
|
+
#
|
26
|
+
def delete
|
27
|
+
if persisted?
|
28
|
+
self.class.from_partition(*self.class.partition_key_values(attributes)).delete(id)
|
29
|
+
end
|
30
|
+
@destroyed = true
|
31
|
+
freeze
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
module Partitioned
|
2
|
+
module BulkMethodsMixin
|
3
|
+
class BulkUploadDataInconsistent < StandardError
|
4
|
+
def initialize(model, table_name, expected_columns, found_columns, while_doing)
|
5
|
+
super("#{model.name}: for table: #{table_name}; #{expected_columns} != #{found_columns}; #{while_doing}")
|
6
|
+
end
|
7
|
+
end
|
8
|
+
#
|
9
|
+
# BULK creation of many rows
|
10
|
+
#
|
11
|
+
# rows: an array of hashtables of data to insert into the database
|
12
|
+
# each hashtable must have the same number of keys (and same
|
13
|
+
# names for each key).
|
14
|
+
#
|
15
|
+
# options:
|
16
|
+
# :slice_size = 1000
|
17
|
+
# :returning = nil
|
18
|
+
# :check_consistency = true
|
19
|
+
#
|
20
|
+
# examples:
|
21
|
+
# first example didn't uses more options.
|
22
|
+
#
|
23
|
+
# rows = [{
|
24
|
+
# :name => 'Keith',
|
25
|
+
# :salary => 1000,
|
26
|
+
# },
|
27
|
+
# {
|
28
|
+
# :name => 'Alex',
|
29
|
+
# :salary => 2000,
|
30
|
+
# }]
|
31
|
+
#
|
32
|
+
# Employee.create_many(rows)
|
33
|
+
#
|
34
|
+
# this second example uses :returning option
|
35
|
+
# to returns key values
|
36
|
+
#
|
37
|
+
# rows = [{
|
38
|
+
# :name => 'Keith',
|
39
|
+
# :salary => 1000,
|
40
|
+
# },
|
41
|
+
# {
|
42
|
+
# :name => 'Alex',
|
43
|
+
# :salary => 2000,
|
44
|
+
# }]
|
45
|
+
#
|
46
|
+
# options = {
|
47
|
+
# :returning => [:id]
|
48
|
+
# }
|
49
|
+
#
|
50
|
+
# Employee.create_many(rows, options) returns [#<Employee id: 1>, #<Employee id: 2>]
|
51
|
+
#
|
52
|
+
# third example uses :slice_size option.
|
53
|
+
# Slice_size - is an integer that specifies how many
|
54
|
+
# records will be created in a single SQL query.
|
55
|
+
#
|
56
|
+
# rows = [{
|
57
|
+
# :name => 'Keith',
|
58
|
+
# :salary => 1000,
|
59
|
+
# },
|
60
|
+
# {
|
61
|
+
# :name => 'Alex',
|
62
|
+
# :salary => 2000,
|
63
|
+
# },
|
64
|
+
# {
|
65
|
+
# :name => 'Mark',
|
66
|
+
# :salary => 3000,
|
67
|
+
# }]
|
68
|
+
#
|
69
|
+
# options = {
|
70
|
+
# :slice_size => 2
|
71
|
+
# }
|
72
|
+
#
|
73
|
+
# Employee.create_many(rows, options) will generate two insert queries
|
74
|
+
#
|
75
|
+
def create_many(rows, options = {})
|
76
|
+
return [] if rows.blank?
|
77
|
+
options[:slice_size] = 1000 unless options.has_key?(:slice_size)
|
78
|
+
options[:check_consistency] = true unless options.has_key?(:check_consistency)
|
79
|
+
returning_clause = ""
|
80
|
+
if options[:returning]
|
81
|
+
if options[:returning].is_a? Array
|
82
|
+
returning_list = options[:returning].join(',')
|
83
|
+
else
|
84
|
+
returning_list = options[:returning]
|
85
|
+
end
|
86
|
+
returning_clause = " returning #{returning_list}"
|
87
|
+
end
|
88
|
+
returning = []
|
89
|
+
|
90
|
+
created_at_value = Time.zone.now
|
91
|
+
|
92
|
+
num_sequences_needed = rows.reject{|r| r[:id].present?}.length
|
93
|
+
if num_sequences_needed > 0
|
94
|
+
row_ids = connection.next_sequence_values(sequence_name, num_sequences_needed)
|
95
|
+
else
|
96
|
+
row_ids = []
|
97
|
+
end
|
98
|
+
rows.each do |row|
|
99
|
+
# set the primary key if it needs to be set
|
100
|
+
row[:id] ||= row_ids.shift
|
101
|
+
end.each do |row|
|
102
|
+
# set :created_at if need be
|
103
|
+
row[:created_at] ||= created_at_value
|
104
|
+
end.group_by do |row|
|
105
|
+
respond_to?(:partition_name) ? partition_name(*partition_key_values(row)) : table_name
|
106
|
+
end.each do |table_name, rows_for_table|
|
107
|
+
column_names = rows_for_table[0].keys.sort{|a,b| a.to_s <=> b.to_s}
|
108
|
+
sql_insert_string = "insert into #{table_name} (#{column_names.join(',')}) values "
|
109
|
+
rows_for_table.map do |row|
|
110
|
+
if options[:check_consistency]
|
111
|
+
row_column_names = row.keys.sort{|a,b| a.to_s <=> b.to_s}
|
112
|
+
if column_names != row_column_names
|
113
|
+
raise BulkUploadDataInconsistent.new(self, table_name, column_names, row_column_names, "while attempting to build insert statement")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
column_values = column_names.map do |column_name|
|
117
|
+
quote_value(row[column_name], columns_hash[column_name.to_s])
|
118
|
+
end.join(',')
|
119
|
+
"(#{column_values})"
|
120
|
+
end.each_slice(options[:slice_size]) do |insert_slice|
|
121
|
+
returning += find_by_sql(sql_insert_string + insert_slice.join(',') + returning_clause)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
return returning
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# BULK updates of many rows
|
129
|
+
#
|
130
|
+
# rows: an array of hashtables of data to insert into the database
|
131
|
+
# each hashtable must have the same number of keys (and same
|
132
|
+
# names for each key).
|
133
|
+
#
|
134
|
+
# options:
|
135
|
+
# :slice_size = 1000
|
136
|
+
# :returning = nil
|
137
|
+
# :set_array = from first row passed in
|
138
|
+
# :check_consistency = true
|
139
|
+
# :where = '"#{table_name}.id = datatable.id"'
|
140
|
+
#
|
141
|
+
# examples:
|
142
|
+
# this first example uses "set_array" to add the value of "salary"
|
143
|
+
# to the specific employee's salary
|
144
|
+
# the default where clause is to match IDs so, it works here.
|
145
|
+
# rows = [{
|
146
|
+
# :id => 1,
|
147
|
+
# :salary => 1000,
|
148
|
+
# },
|
149
|
+
# {
|
150
|
+
# :id => 10,
|
151
|
+
# :salary => 2000,
|
152
|
+
# },
|
153
|
+
# {
|
154
|
+
# :id => 23,
|
155
|
+
# :salary => 2500,
|
156
|
+
# }]
|
157
|
+
#
|
158
|
+
# options = {
|
159
|
+
# :set_array => '"salary = datatable.salary"'
|
160
|
+
# }
|
161
|
+
#
|
162
|
+
# Employee.update_many(rows, options)
|
163
|
+
#
|
164
|
+
#
|
165
|
+
# this versions sets the where clause to match Salaries.
|
166
|
+
# rows = [{
|
167
|
+
# :id => 1,
|
168
|
+
# :salary => 1000,
|
169
|
+
# :company_id => 10
|
170
|
+
# },
|
171
|
+
# {
|
172
|
+
# :id => 10,
|
173
|
+
# :salary => 2000,
|
174
|
+
# :company_id => 12
|
175
|
+
# },
|
176
|
+
# {
|
177
|
+
# :id => 23,
|
178
|
+
# :salary => 2500,
|
179
|
+
# :company_id => 5
|
180
|
+
# }]
|
181
|
+
#
|
182
|
+
# options = {
|
183
|
+
# :set_array => '"company_id = datatable.company_id"',
|
184
|
+
# :where => '"#{table_name}.salary = datatable.salary"'
|
185
|
+
# }
|
186
|
+
#
|
187
|
+
# Employee.update_many(rows, options)
|
188
|
+
#
|
189
|
+
#
|
190
|
+
# this version sets the where clause to the KEY of the hash passed in
|
191
|
+
# and the set_array is generated from the VALUES
|
192
|
+
#
|
193
|
+
# rows = {
|
194
|
+
# { :id => 1 } => {
|
195
|
+
# :salary => 100000,
|
196
|
+
# :company_id => 10
|
197
|
+
# },
|
198
|
+
# { :id => 10 } => {
|
199
|
+
# :salary => 110000,
|
200
|
+
# :company_id => 12
|
201
|
+
# },
|
202
|
+
# { :id => 23 } => {
|
203
|
+
# :salary => 90000,
|
204
|
+
# :company_id => 5
|
205
|
+
# }
|
206
|
+
# }
|
207
|
+
#
|
208
|
+
# Employee.update_many(rows)
|
209
|
+
#
|
210
|
+
# Remember that you should probably set updated_at using "updated = datatable.updated_at"
|
211
|
+
# or "updated_at = now()" in the set_array if you want to follow
|
212
|
+
# the standard active record model for time columns (and you have an updated_at column)
|
213
|
+
|
214
|
+
def update_many(rows, options = {})
|
215
|
+
return [] if rows.blank?
|
216
|
+
if rows.is_a?(Hash)
|
217
|
+
options[:where] = '"' + rows.keys[0].keys.map{|key| '#{table_name}.' + "#{key} = datatable.#{key}"}.join(' and ') + '"'
|
218
|
+
options[:set_array] = '"' + rows.values[0].keys.map{|key| "#{key} = datatable.#{key}"}.join(',') + '"' unless options[:set_array]
|
219
|
+
r = []
|
220
|
+
rows.each do |key,value|
|
221
|
+
r << key.merge(value)
|
222
|
+
end
|
223
|
+
rows = r
|
224
|
+
end
|
225
|
+
unless options[:set_array]
|
226
|
+
column_names = rows[0].keys
|
227
|
+
columns_to_remove = [:id]
|
228
|
+
columns_to_remove += [partition_keys].map{|k| k.to_sym} if respond_to?(:partition_keys)
|
229
|
+
options[:set_array] = '"' + (column_names - columns_to_remove.flatten).map{|cn| "#{cn} = datatable.#{cn}"}.join(',') + '"'
|
230
|
+
end
|
231
|
+
options[:slice_size] = 1000 unless options[:slice_size]
|
232
|
+
options[:check_consistency] = true unless options.has_key?(:check_consistency)
|
233
|
+
returning_clause = ""
|
234
|
+
if options[:returning]
|
235
|
+
if options[:returning].is_a?(Array)
|
236
|
+
returning_list = options[:returning].map{|r| '#{table_name}.' + r.to_s}.join(',')
|
237
|
+
else
|
238
|
+
returning_list = options[:returning]
|
239
|
+
end
|
240
|
+
returning_clause = "\" returning #{returning_list}\""
|
241
|
+
end
|
242
|
+
options[:where] = '"#{table_name}.id = datatable.id"' unless options[:where]
|
243
|
+
|
244
|
+
returning = []
|
245
|
+
|
246
|
+
rows.group_by do |row|
|
247
|
+
respond_to?(:partition_name) ? partition_name(*partition_key_values(row)) : table_name
|
248
|
+
end.each do |table_name, rows_for_table|
|
249
|
+
column_names = rows_for_table[0].keys.sort{|a,b| a.to_s <=> b.to_s}
|
250
|
+
rows_for_table.each_slice(options[:slice_size]) do |update_slice|
|
251
|
+
datatable_rows = []
|
252
|
+
update_slice.each_with_index do |row,i|
|
253
|
+
if options[:check_consistency]
|
254
|
+
row_column_names = row.keys.sort{|a,b| a.to_s <=> b.to_s}
|
255
|
+
if column_names != row_column_names
|
256
|
+
raise BulkUploadDataInconsistent.new(self, table_name, column_names, row_column_names, "while attempting to build update statement")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
datatable_rows << row.map do |column_name,column_value|
|
260
|
+
column_name = column_name.to_s
|
261
|
+
columns_hash_value = columns_hash[column_name]
|
262
|
+
if i == 0
|
263
|
+
"#{quote_value(column_value, columns_hash_value)}::#{columns_hash_value.sql_type} as #{column_name}"
|
264
|
+
else
|
265
|
+
quote_value(column_value, columns_hash_value)
|
266
|
+
end
|
267
|
+
end.join(',')
|
268
|
+
end
|
269
|
+
datatable = datatable_rows.join(' union select ')
|
270
|
+
|
271
|
+
sql_update_string = <<-SQL
|
272
|
+
update #{table_name} set
|
273
|
+
#{eval(options[:set_array])}
|
274
|
+
from
|
275
|
+
(select
|
276
|
+
#{datatable}
|
277
|
+
) as datatable
|
278
|
+
where
|
279
|
+
#{eval(options[:where])}
|
280
|
+
#{eval(returning_clause)}
|
281
|
+
SQL
|
282
|
+
returning += find_by_sql(sql_update_string)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
return returning
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|