partitioned 0.8.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/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
|