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