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.
Files changed (95) hide show
  1. data/Gemfile +17 -0
  2. data/LICENSE +30 -0
  3. data/PARTITIONING_EXPLAINED.txt +351 -0
  4. data/README +111 -0
  5. data/Rakefile +27 -0
  6. data/examples/README +23 -0
  7. data/examples/company_id.rb +417 -0
  8. data/examples/company_id_and_created_at.rb +689 -0
  9. data/examples/created_at.rb +590 -0
  10. data/examples/created_at_referencing_awards.rb +1000 -0
  11. data/examples/id.rb +475 -0
  12. data/examples/lib/by_company_id.rb +11 -0
  13. data/examples/lib/command_line_tool_mixin.rb +71 -0
  14. data/examples/lib/company.rb +29 -0
  15. data/examples/lib/get_options.rb +44 -0
  16. data/examples/lib/roman.rb +41 -0
  17. data/examples/start_date.rb +621 -0
  18. data/init.rb +1 -0
  19. data/lib/monkey_patch_activerecord.rb +92 -0
  20. data/lib/monkey_patch_postgres.rb +73 -0
  21. data/lib/partitioned.rb +26 -0
  22. data/lib/partitioned/active_record_overrides.rb +34 -0
  23. data/lib/partitioned/bulk_methods_mixin.rb +288 -0
  24. data/lib/partitioned/by_created_at.rb +13 -0
  25. data/lib/partitioned/by_foreign_key.rb +21 -0
  26. data/lib/partitioned/by_id.rb +35 -0
  27. data/lib/partitioned/by_integer_field.rb +32 -0
  28. data/lib/partitioned/by_monthly_time_field.rb +23 -0
  29. data/lib/partitioned/by_time_field.rb +65 -0
  30. data/lib/partitioned/by_weekly_time_field.rb +30 -0
  31. data/lib/partitioned/multi_level.rb +20 -0
  32. data/lib/partitioned/multi_level/configurator/data.rb +14 -0
  33. data/lib/partitioned/multi_level/configurator/dsl.rb +32 -0
  34. data/lib/partitioned/multi_level/configurator/reader.rb +162 -0
  35. data/lib/partitioned/multi_level/partition_manager.rb +47 -0
  36. data/lib/partitioned/partitioned_base.rb +354 -0
  37. data/lib/partitioned/partitioned_base/configurator.rb +6 -0
  38. data/lib/partitioned/partitioned_base/configurator/data.rb +62 -0
  39. data/lib/partitioned/partitioned_base/configurator/dsl.rb +628 -0
  40. data/lib/partitioned/partitioned_base/configurator/reader.rb +209 -0
  41. data/lib/partitioned/partitioned_base/partition_manager.rb +138 -0
  42. data/lib/partitioned/partitioned_base/sql_adapter.rb +286 -0
  43. data/lib/partitioned/version.rb +3 -0
  44. data/lib/tasks/desirable_tasks.rake +4 -0
  45. data/partitioned.gemspec +21 -0
  46. data/spec/dummy/.rspec +1 -0
  47. data/spec/dummy/README.rdoc +261 -0
  48. data/spec/dummy/Rakefile +7 -0
  49. data/spec/dummy/app/assets/javascripts/application.js +9 -0
  50. data/spec/dummy/app/assets/stylesheets/application.css +7 -0
  51. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  52. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  53. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  54. data/spec/dummy/config.ru +4 -0
  55. data/spec/dummy/config/application.rb +51 -0
  56. data/spec/dummy/config/boot.rb +10 -0
  57. data/spec/dummy/config/database.yml +32 -0
  58. data/spec/dummy/config/environment.rb +5 -0
  59. data/spec/dummy/config/environments/development.rb +30 -0
  60. data/spec/dummy/config/environments/production.rb +60 -0
  61. data/spec/dummy/config/environments/test.rb +39 -0
  62. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  63. data/spec/dummy/config/initializers/inflections.rb +10 -0
  64. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  65. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  66. data/spec/dummy/config/initializers/session_store.rb +8 -0
  67. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  68. data/spec/dummy/config/locales/en.yml +5 -0
  69. data/spec/dummy/config/routes.rb +58 -0
  70. data/spec/dummy/public/404.html +26 -0
  71. data/spec/dummy/public/422.html +26 -0
  72. data/spec/dummy/public/500.html +26 -0
  73. data/spec/dummy/public/favicon.ico +0 -0
  74. data/spec/dummy/script/rails +6 -0
  75. data/spec/dummy/spec/spec_helper.rb +27 -0
  76. data/spec/monkey_patch_posgres_spec.rb +176 -0
  77. data/spec/partitioned/bulk_methods_mixin_spec.rb +512 -0
  78. data/spec/partitioned/by_created_at_spec.rb +62 -0
  79. data/spec/partitioned/by_foreign_key_spec.rb +95 -0
  80. data/spec/partitioned/by_id_spec.rb +97 -0
  81. data/spec/partitioned/by_integer_field_spec.rb +143 -0
  82. data/spec/partitioned/by_monthly_time_field_spec.rb +100 -0
  83. data/spec/partitioned/by_time_field_spec.rb +182 -0
  84. data/spec/partitioned/by_weekly_time_field_spec.rb +100 -0
  85. data/spec/partitioned/multi_level/configurator/dsl_spec.rb +88 -0
  86. data/spec/partitioned/multi_level/configurator/reader_spec.rb +147 -0
  87. data/spec/partitioned/partitioned_base/configurator/dsl_spec.rb +459 -0
  88. data/spec/partitioned/partitioned_base/configurator/reader_spec.rb +513 -0
  89. data/spec/partitioned/partitioned_base/sql_adapter_spec.rb +204 -0
  90. data/spec/partitioned/partitioned_base_spec.rb +173 -0
  91. data/spec/spec_helper.rb +32 -0
  92. data/spec/support/shared_example_spec_helper_for_integer_key.rb +137 -0
  93. data/spec/support/shared_example_spec_helper_for_time_key.rb +147 -0
  94. data/spec/support/tables_spec_helper.rb +47 -0
  95. 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
@@ -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