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