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