partitioned 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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