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,260 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class NoPersistentRecord < ActiveRecordError
|
3
|
+
end
|
4
|
+
|
5
|
+
class NoOrginalRecordFound < ActiveRecordError
|
6
|
+
end
|
7
|
+
|
8
|
+
class ExternalDataChange < ActiveRecordError
|
9
|
+
end
|
10
|
+
|
11
|
+
class Base
|
12
|
+
extend ActiveRecord::Bulkoperation::BatchUpdate::InstanceMethods
|
13
|
+
class << self
|
14
|
+
def flush_scheduled_operations(args = {})
|
15
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush_record_class(self, args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def has_id_column?
|
19
|
+
@has_id_column ||= columns_hash.key?('id')
|
20
|
+
end
|
21
|
+
|
22
|
+
def sequence_exists?(name)
|
23
|
+
arr = connection.find_by_sequence_name_sql_array(name)
|
24
|
+
sql = sanitize_sql(arr)
|
25
|
+
find_by_sql(sql).count == 1
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def check_sequence
|
30
|
+
if sequence_exists?("#{table_name}_seq")
|
31
|
+
"#{table_name}_seq"
|
32
|
+
else
|
33
|
+
sequence_name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def next_sequence_value
|
38
|
+
@sequence_cache ||= ActiveRecord::Bulkoperation::Util::SequenceCache.new(check_sequence)
|
39
|
+
@sequence_cache.next_value
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_foreign_master_tables(table_name)
|
43
|
+
arr = connection.find_foreign_master_tables_sql_array(table_name)
|
44
|
+
sql = sanitize_sql(arr)
|
45
|
+
Array(find_by_sql(sql)).map { |e|e.table_name }
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_foreign_detail_tables(table_name)
|
49
|
+
arr = connection.find_foreign_detail_tables_sql_array(table_name)
|
50
|
+
sql = sanitize_sql(arr)
|
51
|
+
Array(find_by_sql(sql)).map { |e|e.table_name }
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.find_detail_references(table_name)
|
55
|
+
arr = connection.find_detail_references_sql_array(table_name)
|
56
|
+
sql = sanitize_sql([sql, table_name.upcase])
|
57
|
+
find_by_sql(sql)
|
58
|
+
end
|
59
|
+
|
60
|
+
def extract_options_from_args!(args) #:nodoc:
|
61
|
+
args.last.is_a?(Hash) ? args.pop : {}
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_delete_by_primary_key_sql
|
65
|
+
keys = primary_key_columns
|
66
|
+
|
67
|
+
index = 1
|
68
|
+
"DELETE FROM #{table_name} " \
|
69
|
+
'WHERE ' + "#{keys.map { |c| string = "#{c.name} = :#{index}"; index += 1; string }.join(' AND ') } "
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_optimistic_delete_sql
|
73
|
+
index = 1
|
74
|
+
"DELETE FROM #{table_name} " \
|
75
|
+
'WHERE ' + "#{columns.map { |c| string = build_optimistic_where_element(index, c); index += 1; index += 1 if c.null; string }.join(' AND ') } "
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_optimistic_update_sql
|
80
|
+
index = 1
|
81
|
+
"UPDATE #{table_name} " \
|
82
|
+
'SET ' +
|
83
|
+
"#{columns.map { |c| string = c.name + " = :#{index}"; index += 1; string }.join(', ') } " +
|
84
|
+
'WHERE ' +
|
85
|
+
"#{columns.map { |c| string = build_optimistic_where_element(index, c); index += 1; index += 1 if c.null; string }.join(' AND ') } "# +
|
86
|
+
#"AND ROWID = :#{index}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_update_by_primary_key_sql
|
90
|
+
keys = primary_key_columns
|
91
|
+
|
92
|
+
index = 1
|
93
|
+
"UPDATE #{table_name} " \
|
94
|
+
'SET ' +
|
95
|
+
"#{columns.map { |c| string = c.name + " = :#{index}"; index += 1; string }.join(', ') } " +
|
96
|
+
'WHERE ' +
|
97
|
+
"#{keys.map { |c| string = "#{c.name} = :#{index}"; index += 1; string }.join(' AND ') } "
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def primary_key_columns
|
103
|
+
@primary_key_columns ||= primary_key_as_array.map { |c| columns_hash[c.to_s] }
|
104
|
+
end
|
105
|
+
|
106
|
+
def primary_key_as_array
|
107
|
+
primary_key.is_a?(Array) ? primary_key : [primary_key]
|
108
|
+
end
|
109
|
+
|
110
|
+
def build_optimistic_where_element(index, column)
|
111
|
+
sql = '( '
|
112
|
+
# AK: remove timezone information. ruby appends the summer/winter timezone releated to the date and send this as timestamp to oracle
|
113
|
+
# and a timestamp with timezone is not equals the date in the row
|
114
|
+
if column.type == :datetime
|
115
|
+
sql += "#{column.name} = cast( :#{index} as date ) "
|
116
|
+
else
|
117
|
+
sql += "#{column.name} = :#{index} "
|
118
|
+
end
|
119
|
+
|
120
|
+
index += 1
|
121
|
+
if column.null
|
122
|
+
sql += "OR ( #{column.name} IS NULL AND :#{index} IS NULL ) "
|
123
|
+
index += 1
|
124
|
+
end
|
125
|
+
|
126
|
+
sql += ') '
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
# Checks if a active record object was changed and if schedule if for DB merge
|
132
|
+
#
|
133
|
+
# args - Hash
|
134
|
+
#
|
135
|
+
# return - self
|
136
|
+
def schedule_merge_on_change( args={} )
|
137
|
+
if orginal_selected_record.nil?
|
138
|
+
schedule_merge
|
139
|
+
return self
|
140
|
+
end
|
141
|
+
|
142
|
+
ignore_columns = args[:ignore_columns] || []
|
143
|
+
|
144
|
+
attributes.each_pair do |key, value|
|
145
|
+
next if ignore_columns.include?(key)
|
146
|
+
if value != orginal_selected_record[key]
|
147
|
+
schedule_merge
|
148
|
+
return self
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
self
|
153
|
+
end
|
154
|
+
|
155
|
+
def schedule_merge
|
156
|
+
set_id_from_sequence
|
157
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.add_merge(self)
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
def schedule_insert_on_missing( *unique_columns )
|
162
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.add_insert_on_missing( unique_columns, self )
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
def insert_on_missing( *unique_columns )
|
168
|
+
unique_columns = Array( self.class.primary_key ) if unique_columns.nil? || unique_columns.empty?
|
169
|
+
set_id_from_sequence
|
170
|
+
self.class.insert_on_missing_group( unique_columns, [self] ) > 0
|
171
|
+
end
|
172
|
+
|
173
|
+
def schedule_delete
|
174
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.add_delete(self)
|
175
|
+
self
|
176
|
+
end
|
177
|
+
|
178
|
+
# if id column is nil a value will be fetched from the sequence and set to the id property
|
179
|
+
def set_id_from_sequence
|
180
|
+
if self.class.has_id_column? && @attributes.fetch_value( 'id' ).nil?
|
181
|
+
new_id = self.class.next_sequence_value
|
182
|
+
if self.class.primary_key == 'id'
|
183
|
+
self.id = new_id
|
184
|
+
else
|
185
|
+
id_will_change! # <- neu
|
186
|
+
@attributes.write_from_user( 'id', new_id )
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
def optimistic_update
|
193
|
+
fail NoPersistentRecord.new if @new_record
|
194
|
+
fail NoOrginalRecordFound.new unless orginal_selected_record
|
195
|
+
|
196
|
+
sql = self.class.build_optimistic_update_sql
|
197
|
+
|
198
|
+
types = []
|
199
|
+
|
200
|
+
for c in self.class.columns
|
201
|
+
type = self.class.to_type_symbol(c)
|
202
|
+
types << type
|
203
|
+
end
|
204
|
+
|
205
|
+
for c in self.class.columns
|
206
|
+
type = self.class.to_type_symbol(c)
|
207
|
+
types << type
|
208
|
+
types << type if c.null
|
209
|
+
end
|
210
|
+
|
211
|
+
binds = self.class.columns.map { |c| read_attribute(c.name) }
|
212
|
+
|
213
|
+
get_optimistic_where_binds.each { |v| binds << v }
|
214
|
+
|
215
|
+
self.class.execute_batch_update(sql, types, [binds])
|
216
|
+
end
|
217
|
+
|
218
|
+
def optimistic_delete
|
219
|
+
fail NoPersistentRecord.new if @new_record
|
220
|
+
fail NoOrginalRecordFound.new unless orginal_selected_record
|
221
|
+
|
222
|
+
sql = self.class.build_optimistic_delete_sql
|
223
|
+
|
224
|
+
binds = get_optimistic_where_binds
|
225
|
+
|
226
|
+
types = []
|
227
|
+
|
228
|
+
for c in self.class.columns
|
229
|
+
type = self.class.to_type_symbol(c)
|
230
|
+
types << type
|
231
|
+
types << type if c.null
|
232
|
+
end
|
233
|
+
|
234
|
+
self.class.execute_batch_update(sql, types, [binds])
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def get_optimistic_where_binds
|
240
|
+
binds = []
|
241
|
+
self.class.columns.map do |c|
|
242
|
+
v = orginal_selected_record[ c.name]
|
243
|
+
binds << v
|
244
|
+
binds << v if c.null
|
245
|
+
end
|
246
|
+
|
247
|
+
binds
|
248
|
+
end
|
249
|
+
|
250
|
+
def rowid=(id)
|
251
|
+
self['rowid'] = id
|
252
|
+
end
|
253
|
+
alias_method :row_id=, :rowid=
|
254
|
+
|
255
|
+
def rowid
|
256
|
+
self['rowid']
|
257
|
+
end
|
258
|
+
alias_method :row_id, :rowid
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'java'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Bulkoperation
|
5
|
+
module BatchUpdate
|
6
|
+
module InstanceMethods
|
7
|
+
|
8
|
+
def enhanced_write_lobs #:nodoc:
|
9
|
+
# disable this feature for jruby
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute_batch_update(sql, types, values, optimistic = true)
|
13
|
+
fail ArgumentError.new('String expected') unless sql.is_a? String
|
14
|
+
|
15
|
+
types = [types] unless types.is_a? Array
|
16
|
+
fail ArgumentError.new('Array of symbols expected') unless types.select { |i| not i.is_a? Symbol }.empty?
|
17
|
+
|
18
|
+
fail ArgumentError.new('Array expected') unless values.is_a? Array
|
19
|
+
values = [values] unless values.select { |i| not i.is_a? Array }.empty?
|
20
|
+
|
21
|
+
unless values.select { |i| not i.count == types.count }.empty?
|
22
|
+
fail ArgumentError.new('types.count must be equal to arr.count for every arr in values')
|
23
|
+
end
|
24
|
+
|
25
|
+
unless connection.adapter_name == 'Oracle' || connection.adapter_name == 'OracleEnhanced'
|
26
|
+
fail "Operation is provided only on Oracle connections. (adapter_name is #{connection.adapter_name})"
|
27
|
+
end
|
28
|
+
|
29
|
+
stmt = connection.raw_connection.prepareStatement(sql)
|
30
|
+
|
31
|
+
count = 0
|
32
|
+
|
33
|
+
begin
|
34
|
+
stmt.setExecuteBatch(300) # TODO
|
35
|
+
|
36
|
+
for row in values
|
37
|
+
i = 1
|
38
|
+
( 0..row.count - 1).each{ |j|
|
39
|
+
bind(stmt, i, types[j], row[j])
|
40
|
+
i += 1
|
41
|
+
}
|
42
|
+
|
43
|
+
count += stmt.executeUpdate
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
count += stmt.sendBatch
|
48
|
+
|
49
|
+
fail ExternalDataChange.new(sql) if count != values.count and optimistic
|
50
|
+
|
51
|
+
rescue Exception => e
|
52
|
+
exc = e
|
53
|
+
while exc.kind_of?(Java.java.lang.Exception) and exc.cause
|
54
|
+
exc = exc.cause
|
55
|
+
end
|
56
|
+
raise exc
|
57
|
+
ensure
|
58
|
+
stmt.close
|
59
|
+
end
|
60
|
+
|
61
|
+
count
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def bind(stmt, index, type, value)
|
66
|
+
if type == :string
|
67
|
+
stmt.setString(index, value) if value
|
68
|
+
stmt.setNull(index, Java.java.sql.Types::VARCHAR) unless value
|
69
|
+
|
70
|
+
elsif type == :integer
|
71
|
+
stmt.setLong(index, value) if value
|
72
|
+
stmt.setNull(index, Java.java.sql.Types::NUMERIC) unless value
|
73
|
+
|
74
|
+
elsif type == :float
|
75
|
+
stmt.setDouble(index, value) if value
|
76
|
+
stmt.setNull(index, Java.java.sql.Types::NUMERIC) unless value
|
77
|
+
|
78
|
+
elsif type == :date
|
79
|
+
|
80
|
+
if value
|
81
|
+
# TODO Timezone handling; if the given value have another timezone as default
|
82
|
+
c = Java.java.util.Calendar.getInstance
|
83
|
+
c.set(Java.java.util.Calendar::YEAR, value.year)
|
84
|
+
c.set(Java.java.util.Calendar::MONTH, value.month - 1)
|
85
|
+
c.set(Java.java.util.Calendar::DATE, value.day)
|
86
|
+
if value.kind_of?(DateTime) or value.kind_of?(Time)
|
87
|
+
c.set(Java.java.util.Calendar::HOUR_OF_DAY, value.hour)
|
88
|
+
c.set(Java.java.util.Calendar::MINUTE, value.min)
|
89
|
+
c.set(Java.java.util.Calendar::SECOND, value.sec)
|
90
|
+
else
|
91
|
+
c.set(Java.java.util.Calendar::HOUR_OF_DAY, 0)
|
92
|
+
c.set(Java.java.util.Calendar::MINUTE, 0)
|
93
|
+
c.set(Java.java.util.Calendar::SECOND, 0)
|
94
|
+
end
|
95
|
+
c.set(Java.java.util.Calendar::MILLISECOND, 0)
|
96
|
+
|
97
|
+
stmt.setTimestamp(index, Java.java.sql.Timestamp.new(c.getTimeInMillis))
|
98
|
+
|
99
|
+
else
|
100
|
+
stmt.setNull(index, Java.java.sql.Types::TIMESTAMP)
|
101
|
+
end
|
102
|
+
|
103
|
+
else
|
104
|
+
|
105
|
+
fail ArgumentError.new("unsupported type #{type}")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Bulkoperation
|
3
|
+
module BatchUpdate
|
4
|
+
module InstanceMethods
|
5
|
+
|
6
|
+
def execute_batch_update(sql, types, values, optimistic = true)
|
7
|
+
fail ArgumentError.new('String expected') unless sql.is_a? String
|
8
|
+
|
9
|
+
types = [types] unless types.is_a? Array
|
10
|
+
fail ArgumentError.new('Array of Symbol expected') unless types.select { |i| not i.is_a? Symbol }.empty?
|
11
|
+
|
12
|
+
fail ArgumentError.new('Array expected') unless values.is_a? Array
|
13
|
+
values = [values] unless values.select { |i| not i.is_a? Array }.empty?
|
14
|
+
|
15
|
+
unless values.select { |i| not i.count == types.count }.empty?
|
16
|
+
fail ArgumentError.new('types.count must be equal to arr.count for every arr in values')
|
17
|
+
end
|
18
|
+
|
19
|
+
unless connection.adapter_name == 'Oracle' || connection.adapter_name == 'OracleEnhanced'
|
20
|
+
fail "Operation is provided only on Oracle connections. (adapter_name is #{connection.adapter_name})"
|
21
|
+
end
|
22
|
+
|
23
|
+
oci_conn = connection.raw_connection if connected?
|
24
|
+
fail 'Unable to access the raw OCI connection.' unless oci_conn
|
25
|
+
cursor = oci_conn.parse(sql)
|
26
|
+
fail "Unable to obtain cursor for this statement:\n#{sql}." unless cursor
|
27
|
+
|
28
|
+
affected_rows = 0
|
29
|
+
|
30
|
+
begin
|
31
|
+
values.each do |row|
|
32
|
+
|
33
|
+
(0..row.count - 1).each do |i|
|
34
|
+
bind(cursor, i + 1, types[i], row[i])
|
35
|
+
end
|
36
|
+
|
37
|
+
result = cursor.exec
|
38
|
+
#result = 1
|
39
|
+
if result != 1 and optimistic
|
40
|
+
|
41
|
+
msg = sql
|
42
|
+
i = 1
|
43
|
+
row.each do |v|
|
44
|
+
val = get_value(v)
|
45
|
+
msg = msg.gsub(":#{i},", connection.quote(val) + ',')
|
46
|
+
msg = msg.gsub(":#{i} ", connection.quote(val) + ' ')
|
47
|
+
i += 1
|
48
|
+
end
|
49
|
+
i -= 1
|
50
|
+
msg = msg.gsub(":#{i}", connection.quote(get_value(row.last)))
|
51
|
+
|
52
|
+
fail ExternalDataChange.new("The record you want to update was updated by another process.\n#{msg}")
|
53
|
+
end
|
54
|
+
|
55
|
+
affected_rows += result
|
56
|
+
|
57
|
+
connection.send(:log, sql, 'Update') {}
|
58
|
+
end
|
59
|
+
ensure
|
60
|
+
cursor.close
|
61
|
+
end
|
62
|
+
|
63
|
+
affected_rows
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def get_value(value)
|
69
|
+
if(value.respond_to? :value)
|
70
|
+
value.value
|
71
|
+
else
|
72
|
+
value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def bind(cursor, index, type, input_value)
|
77
|
+
value = get_value(input_value)
|
78
|
+
if type == :string
|
79
|
+
cursor.bind_param(":#{index}", value, String)
|
80
|
+
|
81
|
+
elsif type == :integer
|
82
|
+
cursor.bind_param(":#{index}", value, Fixnum)
|
83
|
+
|
84
|
+
elsif type == :float
|
85
|
+
cursor.bind_param(":#{index}", value, Float)
|
86
|
+
|
87
|
+
elsif type == :date
|
88
|
+
if value.nil?
|
89
|
+
cursor.bind_param(":#{index}", nil, Date)
|
90
|
+
else
|
91
|
+
if ( value.kind_of?(DateTime) or value.kind_of?(Time)) and value.hour == 0 and value.min == 0 and value.sec == 0
|
92
|
+
cursor.bind_param(":#{index}", value, Date)
|
93
|
+
else
|
94
|
+
cursor.bind_param(":#{index}", value, DateTime)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
else
|
99
|
+
|
100
|
+
fail ArgumentError.new("unsupported type #{type}")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|