activerecord_bulkoperation 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +13 -0
  5. data/README.md +1 -0
  6. data/Rakefile +25 -0
  7. data/activerecord_bulkoperation.gemspec +23 -0
  8. data/gemfiles/4.2.gemfile +14 -0
  9. data/lib/activerecord_bulkoperation.rb +20 -0
  10. data/lib/activerecord_bulkoperation/active_record/adapters/abstract_adapter.rb +49 -0
  11. data/lib/activerecord_bulkoperation/active_record/adapters/oracle_enhanced_adapter.rb +10 -0
  12. data/lib/activerecord_bulkoperation/active_record/associations/associations.rb +169 -0
  13. data/lib/activerecord_bulkoperation/adapters/abstract_adapter.rb +43 -0
  14. data/lib/activerecord_bulkoperation/adapters/oracle_enhanced_adapter.rb +44 -0
  15. data/lib/activerecord_bulkoperation/base.rb +53 -0
  16. data/lib/activerecord_bulkoperation/bulkoperation.rb +260 -0
  17. data/lib/activerecord_bulkoperation/connection_adapters/oracle_enhanced/jdbc_connection.rb +111 -0
  18. data/lib/activerecord_bulkoperation/connection_adapters/oracle_enhanced/oci_connection.rb +106 -0
  19. data/lib/activerecord_bulkoperation/group_operations.rb +296 -0
  20. data/lib/activerecord_bulkoperation/group_operations_select.rb +60 -0
  21. data/lib/activerecord_bulkoperation/util/connection_object.rb +22 -0
  22. data/lib/activerecord_bulkoperation/util/entity_hash.rb +78 -0
  23. data/lib/activerecord_bulkoperation/util/flush_dirty_objects.rb +126 -0
  24. data/lib/activerecord_bulkoperation/util/sequence_cache.rb +52 -0
  25. data/lib/activerecord_bulkoperation/util/transaction_object.rb +36 -0
  26. data/lib/activerecord_bulkoperation/version.rb +5 -0
  27. data/test/active_record_connection_test.rb +41 -0
  28. data/test/adapters/oracle_enhanced.rb +1 -0
  29. data/test/bulkoperation_test.rb +176 -0
  30. data/test/database.yml +8 -0
  31. data/test/entity_hash_test.rb +11 -0
  32. data/test/find_group_by_test.rb +132 -0
  33. data/test/flush_dirty_objects_test.rb +11 -0
  34. data/test/models/assembly.rb +3 -0
  35. data/test/models/course.rb +3 -0
  36. data/test/models/group.rb +3 -0
  37. data/test/models/item.rb +2 -0
  38. data/test/models/part.rb +3 -0
  39. data/test/models/product.rb +7 -0
  40. data/test/models/student.rb +3 -0
  41. data/test/models/test_table.rb +2 -0
  42. data/test/postgresql/bulk_test.rb +13 -0
  43. data/test/schema/generic_schema.rb +59 -0
  44. data/test/sequence_cache_test.rb +31 -0
  45. data/test/support/postgresql/bulk_examples.rb +8 -0
  46. data/test/test_helper.rb +45 -0
  47. data/test/transaction_object_test.rb +11 -0
  48. metadata +141 -0
@@ -0,0 +1,296 @@
1
+ module ActiveRecord
2
+ class Base
3
+
4
+ attr_reader :orginal_selected_record
5
+
6
+ def save_original
7
+ @orginal_selected_record = @attributes.to_hash.clone unless @orginal_selected_record
8
+ self
9
+ end
10
+
11
+ def callbacks_closed_scheduled_operation
12
+ ( @callbacks_closed_scheduled_operation ||= Set.new)
13
+ end
14
+
15
+ def on_closed_scheduled_operation
16
+ @callbacks_closed_scheduled_operation && @callbacks_closed_scheduled_operation.each { |c| c.on_closed_scheduled_operation }
17
+ end
18
+
19
+ def unset_new_record
20
+ @new_record = false
21
+ end
22
+ class << self
23
+ # will insert or update the given array
24
+ #
25
+ # +group+ array of activerecord objects
26
+ def merge_group(group, options = {})
27
+ check_group(group)
28
+
29
+ to_insert = group.select { |i| i.new_record? }
30
+
31
+ to_update = group.select { |i| not i.new_record? }
32
+
33
+ affected_rows = 0
34
+
35
+ affected_rows += insert_group(to_insert, options) unless to_insert.empty?
36
+ affected_rows += update_group(to_update, options) unless to_update.empty?
37
+
38
+ affected_rows
39
+ end
40
+
41
+ def to_type_symbol(column)
42
+ return :string if column.sql_type.index('CHAR')
43
+ return :date if column.sql_type.index('DATE') or column.sql_type.index('TIMESTAMP')
44
+ return :integer if column.sql_type.index('NUMBER') and column.sql_type.count(',') == 0
45
+ return :float if column.sql_type.index('NUMBER') and column.sql_type.count(',') == 1
46
+ fail ArgumentError.new("type #{column.sql_type} of #{column.name} is unsupported")
47
+ end
48
+
49
+ def foreign_detail_tables
50
+ @foreign_detail_tables ||= find_foreign_detail_tables(table_name)
51
+ end
52
+
53
+ def foreign_master_tables
54
+ @foreign_master_tables ||= find_foreign_master_tables(table_name)
55
+ end
56
+
57
+ def check_group(group)
58
+ fail ArgumentError.new("Array expected. Got #{group.class.name}.") unless group.is_a? Array or group.is_a? Set or group.is_a? ActiveRecord::Relation
59
+ fail ArgumentError.new("only records of #{name} expected. Unexpected #{group.select { |i| not i.is_a? self }.map { |i|i.class.name }.uniq.join(',') } found.") unless group.select { |i| not i.is_a? self }.empty?
60
+ end
61
+
62
+ def insert_group(group, options = {})
63
+ to_insert = group.select { |i| not i.new_record? }
64
+
65
+ sql = "INSERT INTO #{table_name} " \
66
+ '( ' +
67
+ "#{columns.map { |c| c.name }.join(', ') } " +
68
+ ') VALUES ( ' +
69
+ "#{(1..columns.count).map { |i| ":#{i}" }.join(', ') } " +
70
+ ')'
71
+
72
+ types = []
73
+
74
+ for c in columns
75
+ type = to_type_symbol(c)
76
+ types << type
77
+ end
78
+
79
+ values = []
80
+
81
+ for record in group
82
+
83
+ row = []
84
+
85
+ for c in columns
86
+ v = record.read_attribute(c.name)
87
+ row << v
88
+ end
89
+
90
+ values << row
91
+
92
+ end
93
+
94
+ result = execute_batch_update(sql, types, values)
95
+
96
+ group.each { |r| r.unset_new_record }
97
+
98
+ result
99
+ end
100
+
101
+ def update_group(group, options = {})
102
+ check_group(group)
103
+
104
+ optimistic = options[:optimistic]
105
+ optimistic = true if optimistic.nil?
106
+
107
+ fail NoOrginalRecordFound.new("#{name} ( #{table_name} )") if optimistic and not group.select { |i| not i.orginal_selected_record }.empty?
108
+
109
+ sql = optimistic ? build_optimistic_update_sql : build_update_by_primary_key_sql
110
+
111
+ types = []
112
+
113
+ for c in columns
114
+ type = to_type_symbol(c)
115
+ types << type
116
+ end
117
+
118
+ if optimistic
119
+
120
+ for c in columns
121
+ type = to_type_symbol(c)
122
+ types << type
123
+ types << type if c.null
124
+ end
125
+
126
+ else
127
+
128
+ keys = primary_key_columns
129
+
130
+ for c in keys
131
+ type = to_type_symbol(c)
132
+ types << type
133
+ end
134
+
135
+ end
136
+
137
+ values = []
138
+
139
+ keys = primary_key_columns
140
+
141
+ for record in group
142
+ row = []
143
+
144
+ for c in columns
145
+ v = record.read_attribute(c.name)
146
+ row << v
147
+ end
148
+
149
+ if optimistic
150
+
151
+ orginal = record.orginal_selected_record
152
+ for c in columns
153
+
154
+ v = orginal[ c.name]
155
+ row << v
156
+ row << v if c.null
157
+
158
+ end
159
+
160
+ else
161
+
162
+ keys.each { |c| row << record[ c.name] }
163
+
164
+ end
165
+
166
+ values << row
167
+
168
+ end
169
+
170
+ count = execute_batch_update(sql, types, values, optimistic)
171
+
172
+ count
173
+ end
174
+
175
+ public
176
+ def insert_on_missing_group( keys, group, options = {} )
177
+
178
+ #fail 'the give key array is empty' if keys.empty?
179
+
180
+ keys = Array( primary_key ) if keys.nil? || keys.empty?
181
+
182
+ sql ="
183
+ merge into #{table_name} target
184
+ using ( select #{columns.map{|c| ":#{columns.index(c)+1} #{c.name}" }.join(', ')} from dual ) source
185
+ on ( #{keys.map{|c| "target.#{c} = source.#{c}" }.join(' and ')} )
186
+ when not matched then
187
+ insert ( #{columns.map{|c| c.name }.join(', ')} )
188
+ values( #{columns.map{|c| "source.#{c.name}" }.join(', ')} )
189
+ "
190
+
191
+
192
+ types = columns.map{ |c| to_type_symbol(c) }
193
+
194
+ values = []
195
+
196
+ for record in group
197
+
198
+ row = []
199
+
200
+ for c in columns
201
+ v = record.read_attribute(c.name)
202
+ row << v
203
+ end
204
+
205
+ values << row
206
+
207
+ end
208
+
209
+ begin
210
+ result = execute_batch_update(sql, types, values,false)
211
+ rescue => e
212
+ raise ActiveRecord::StatementInvalid.new( "#{e.message}\n#{sql}" )
213
+ end
214
+
215
+ group.each { |r| r.unset_new_record }
216
+
217
+ result
218
+
219
+ end
220
+
221
+ def delete_group(group, options = {})
222
+ check_group(group)
223
+
224
+ optimistic = options[:optimistic]
225
+ optimistic = true if optimistic.nil?
226
+
227
+ to_delete = group.select { |i| not i.new_record? }
228
+
229
+ fail NoOrginalRecordFound.new if optimistic and not to_delete.select { |i| not i.orginal_selected_record }.empty?
230
+
231
+ sql = optimistic ? build_optimistic_delete_sql : build_delete_by_primary_key_sql
232
+
233
+ types = []
234
+
235
+ if optimistic
236
+
237
+ for c in columns
238
+ type = to_type_symbol(c)
239
+ types << type
240
+ types << type if c.null
241
+ end
242
+
243
+ else
244
+
245
+ keys = primary_key_columns
246
+
247
+ for c in keys
248
+ type = to_type_symbol(c)
249
+ types << type
250
+ end
251
+
252
+ end
253
+
254
+ values = []
255
+
256
+ if optimistic
257
+
258
+ for record in to_delete
259
+ row = []
260
+ orginal = record.orginal_selected_record
261
+ for c in columns
262
+
263
+ v = orginal[ c.name]
264
+ row << v
265
+ row << v if c.null
266
+
267
+ end
268
+
269
+ values << row
270
+
271
+ end
272
+
273
+ else
274
+
275
+ keys = primary_key_columns
276
+
277
+ for record in to_delete
278
+ row = keys.map { |c| record[ c.name] }
279
+ values << row
280
+ end
281
+
282
+ end
283
+
284
+ count = execute_batch_update(sql, types, values, optimistic)
285
+
286
+ count
287
+
288
+ rescue ExternalDataChange => e
289
+ raise e
290
+ rescue Exception => e
291
+ raise StatementError.new("#{sql} #{e.message}")
292
+ end
293
+
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,60 @@
1
+ module ActiveRecord
2
+ class Base
3
+
4
+ class << self
5
+
6
+ # old style group find like PItem.find_group_by( :i_company => [45,45,45], :i_itemno => [1,2,3], :i_size => [0,0,0] )
7
+ # leads to a sql statement in the form of 'SELECT * FROM table WHERE ((i_company, i_itemno, i_size) IN ((45, 1, 0), (45, 2, 0), (45, 3, 0)))'
8
+
9
+ def find_group_by(args)
10
+ fail 'no hash given, use Table.all() instead' if args.nil? || args.empty? || (not args.is_a?(Hash))
11
+ fail 'args is not a hash of arrays' if args.select { |k,v| k.is_a?(Symbol) || v.is_a?(Array) }.size != args.size
12
+
13
+ conditions = ( args.is_a?(Hash) && args[:conditions] ) || args
14
+ tuple_size = conditions.size
15
+ array_size = 0
16
+ conditions.each do |k,v|
17
+ array_size = v.size unless array_size > 0
18
+ fail 'not all arrays are of the same size' unless v.size == array_size
19
+ end
20
+
21
+ symbols = conditions.keys
22
+ values = Array.new(array_size) { Array.new(tuple_size) }
23
+
24
+ # to recurse is golden, to transpose ... divine!
25
+ i = 0
26
+ (array_size*tuple_size).times do
27
+ values[i/tuple_size][i%tuple_size] = conditions.values[i%tuple_size][i/tuple_size]
28
+ i += 1
29
+ end
30
+
31
+ return where_tuple(symbols, values)
32
+ end
33
+
34
+ # not really compatible to the rest of ActiveRecord but it works
35
+ # provide parameters like this: symbols_tuple = [ :col1, :col2, :col3 ]
36
+ # and values_tuples = [ [1, 4, 7], [2, 5, 8], [3, 6, 9]]
37
+ def where_tuple(symbols_tuple, values_tuples)
38
+ if symbols_tuple.nil? || symbols_tuple.size == 0 || (not symbols_tuple.is_a?(Array)) || (not symbols_tuple.select { |s| not s.is_a?(Symbol) }.empty?)
39
+ fail 'no symbols given or not every entry is a symbol'
40
+ end
41
+
42
+ tuple_size = symbols_tuple.size
43
+ #fail "don't use this method if you're not looking for tuples." if tuple_size < 2
44
+
45
+ if values_tuples.nil? || values_tuples.size == 0 || (not values_tuples.is_a?(Array)) || (not values_tuples.select { |s| not s.is_a?(Array) }.empty?)
46
+ fail 'no values given or not every value is an array'
47
+ end
48
+
49
+ tuple_part = "(#{(['?']*tuple_size).join(',')})"
50
+ in_stmt = "(#{([tuple_part]*values_tuples.size).join(', ')})"
51
+ stmt = "(#{symbols_tuple.map { |sym| sym.to_s }.join(', ')}) IN #{in_stmt}"
52
+
53
+ res = where(stmt, *(values_tuples.flatten!))
54
+
55
+ return res
56
+ end
57
+
58
+ end # end of class << self
59
+ end # end of class Base
60
+ end
@@ -0,0 +1,22 @@
1
+ # A ConnectionObject belongs to a connection. It's like a singleton for each connection.
2
+ #
3
+ # Author:: Andre Kullmann
4
+ #
5
+ class ConnectionObject
6
+ def self.get
7
+ result = ActiveRecord::Base.connection.connection_listeners.select { |l| l.class == self }.first
8
+ unless result
9
+ result = new
10
+ ActiveRecord::Base.connection.connection_listeners << result
11
+ end
12
+ result
13
+ end
14
+
15
+ def before_close
16
+ close
17
+ ActiveRecord::Base.connection.connection_listeners.delete(self)
18
+ end
19
+
20
+ def close
21
+ end
22
+ end
@@ -0,0 +1,78 @@
1
+ #
2
+ #
3
+ # Author:: Andre Kullmann
4
+ #
5
+ module ActiveRecord
6
+ module Bulkoperation
7
+ module Util
8
+ class EntityHash < Hash
9
+
10
+ def each_pair_in_detail_hierarchy(&block)
11
+ visited = []
12
+
13
+ each_pair do |k, v|
14
+
15
+ next if visited.include?(k)
16
+
17
+ in_detail_hierarchy(k, visited, &block)
18
+
19
+ end
20
+ end
21
+
22
+ def each_pair_in_master_hierarchy(&block)
23
+ visited = []
24
+
25
+ each_pair do |k, v|
26
+
27
+ next if visited.include?(k)
28
+
29
+ in_master_hierarchy(k, visited, &block)
30
+
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def in_detail_hierarchy(record_class, visited, &block)
37
+ return if visited.include?(record_class)
38
+
39
+ visited << record_class
40
+
41
+ record_class.foreign_detail_tables.each do |table_name|
42
+
43
+ if key = find_key_by_table_name(table_name)
44
+
45
+ in_detail_hierarchy(key, visited, &block)
46
+ end
47
+ end
48
+
49
+ list = self[ record_class] and yield record_class, list
50
+ end
51
+
52
+ def in_master_hierarchy(record_class, visited, &block)
53
+ return if visited.include?(record_class)
54
+
55
+ visited << record_class
56
+
57
+ record_class.foreign_master_tables.each do |table_name|
58
+
59
+ if key = find_key_by_table_name(table_name)
60
+
61
+ in_master_hierarchy(key, visited, &block)
62
+ end
63
+ end
64
+
65
+ list = self[ record_class] and yield record_class, list
66
+ end
67
+
68
+ def find_key_by_table_name(table_name)
69
+ name = table_name.upcase
70
+ result = keys.select { |k| k.table_name.upcase == name }
71
+ fail '' if result.size > 1
72
+ result.first
73
+ end
74
+
75
+ end
76
+ end
77
+ end
78
+ end