activerecord_bulkoperation 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/README.md +1 -0
- data/Rakefile +25 -0
- data/activerecord_bulkoperation.gemspec +23 -0
- data/gemfiles/4.2.gemfile +14 -0
- data/lib/activerecord_bulkoperation.rb +20 -0
- data/lib/activerecord_bulkoperation/active_record/adapters/abstract_adapter.rb +49 -0
- data/lib/activerecord_bulkoperation/active_record/adapters/oracle_enhanced_adapter.rb +10 -0
- data/lib/activerecord_bulkoperation/active_record/associations/associations.rb +169 -0
- data/lib/activerecord_bulkoperation/adapters/abstract_adapter.rb +43 -0
- data/lib/activerecord_bulkoperation/adapters/oracle_enhanced_adapter.rb +44 -0
- data/lib/activerecord_bulkoperation/base.rb +53 -0
- data/lib/activerecord_bulkoperation/bulkoperation.rb +260 -0
- data/lib/activerecord_bulkoperation/connection_adapters/oracle_enhanced/jdbc_connection.rb +111 -0
- data/lib/activerecord_bulkoperation/connection_adapters/oracle_enhanced/oci_connection.rb +106 -0
- data/lib/activerecord_bulkoperation/group_operations.rb +296 -0
- data/lib/activerecord_bulkoperation/group_operations_select.rb +60 -0
- data/lib/activerecord_bulkoperation/util/connection_object.rb +22 -0
- data/lib/activerecord_bulkoperation/util/entity_hash.rb +78 -0
- data/lib/activerecord_bulkoperation/util/flush_dirty_objects.rb +126 -0
- data/lib/activerecord_bulkoperation/util/sequence_cache.rb +52 -0
- data/lib/activerecord_bulkoperation/util/transaction_object.rb +36 -0
- data/lib/activerecord_bulkoperation/version.rb +5 -0
- data/test/active_record_connection_test.rb +41 -0
- data/test/adapters/oracle_enhanced.rb +1 -0
- data/test/bulkoperation_test.rb +176 -0
- data/test/database.yml +8 -0
- data/test/entity_hash_test.rb +11 -0
- data/test/find_group_by_test.rb +132 -0
- data/test/flush_dirty_objects_test.rb +11 -0
- data/test/models/assembly.rb +3 -0
- data/test/models/course.rb +3 -0
- data/test/models/group.rb +3 -0
- data/test/models/item.rb +2 -0
- data/test/models/part.rb +3 -0
- data/test/models/product.rb +7 -0
- data/test/models/student.rb +3 -0
- data/test/models/test_table.rb +2 -0
- data/test/postgresql/bulk_test.rb +13 -0
- data/test/schema/generic_schema.rb +59 -0
- data/test/sequence_cache_test.rb +31 -0
- data/test/support/postgresql/bulk_examples.rb +8 -0
- data/test/test_helper.rb +45 -0
- data/test/transaction_object_test.rb +11 -0
- 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
|