activerecord_bulkoperation 0.0.2

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 (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