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,126 @@
1
+ # It's used to store the modified/deleted entity objects at transaction level.
2
+ # Before the transaction will be closed the stored enities will be send to the database.
3
+ #
4
+ # Author:: Andre Kullmann
5
+ #
6
+ module ActiveRecord
7
+ module Bulkoperation
8
+ module Util
9
+ class FlushDirtyObjects < Util::TransactionObject
10
+ class DirtyObjects
11
+ attr_reader :insert_or_update
12
+ attr_reader :insert_on_missing
13
+
14
+ def initialize
15
+ @insert_or_update = Set.new
16
+ @insert_on_missing = {}
17
+ end
18
+ end
19
+
20
+ def initialize
21
+ @scheduled_merges = ActiveRecord::Bulkoperation::Util::EntityHash.new
22
+ @scheduled_deletions = ActiveRecord::Bulkoperation::Util::EntityHash.new
23
+ end
24
+
25
+ def before_commit
26
+ flush
27
+ end
28
+
29
+ def before_create_savepoint
30
+ flush
31
+ end
32
+
33
+ def after_rollback_to_savepoint
34
+ close
35
+ end
36
+
37
+ def after_rollback
38
+ close
39
+ end
40
+
41
+ def close
42
+
43
+ @scheduled_merges.values.each do |objects|
44
+ if objects
45
+ objects.insert_or_update.each { |record| record.on_closed_scheduled_operation }
46
+ objects.insert_on_missing.values.each { |array| array.each{ |record| record.on_closed_scheduled_operation } }
47
+ end
48
+ end
49
+ @scheduled_merges.clear
50
+
51
+ @scheduled_deletions.values.each { |array| array and array.each { |record| record.on_closed_scheduled_operation } }
52
+ @scheduled_deletions.clear
53
+ end
54
+
55
+ def add_insert_on_missing(keys,object)
56
+ fail 'nil object' unless object
57
+ ( ( @scheduled_merges[ object.class] ||= DirtyObjects.new).insert_on_missing[keys] ||= Set.new ) << object
58
+ end
59
+
60
+ def add_merge(object)
61
+ fail 'nil object' unless object
62
+ ( @scheduled_merges[ object.class] ||= DirtyObjects.new).insert_or_update << object
63
+ end
64
+
65
+ def add_delete(object)
66
+ fail 'nil object' unless object
67
+ ( @scheduled_deletions[ object.class] ||= []) << object
68
+ end
69
+
70
+ def flush(args = {})
71
+ fail 'nil object' unless args
72
+
73
+ affected = 0
74
+
75
+ @scheduled_deletions.each_pair_in_detail_hierarchy do |k, v|
76
+ affected += k.delete_group(v, args)
77
+ @scheduled_deletions[k] = nil
78
+ v.each { |record| record.on_closed_scheduled_operation }
79
+ end
80
+
81
+ @scheduled_merges.each_pair_in_master_hierarchy do |k, v|
82
+
83
+ affected += k.merge_group( v.insert_or_update, args )
84
+
85
+ v.insert_on_missing.each_pair do |keys,records|
86
+
87
+ affected += k.insert_on_missing_group( keys, records, args )
88
+ end
89
+
90
+ @scheduled_merges[k] = nil
91
+ v.insert_or_update.each { |record| record.on_closed_scheduled_operation }
92
+ v.insert_on_missing.values.each { |array| array.each{ |record| record.on_closed_scheduled_operation } }
93
+ end
94
+
95
+ affected
96
+ end
97
+
98
+ def flush_record_class(record_class, args = {})
99
+ fail 'nil object' unless record_class
100
+ fail 'nil object' unless args
101
+
102
+ if list = @scheduled_deletions[ record_class]
103
+ record_class.delete_group(list, args)
104
+ @scheduled_deletions[ record_class] = nil
105
+ list.each { |record| record.on_closed_scheduled_operation }
106
+ end
107
+
108
+ if objects = @scheduled_merges[ record_class]
109
+
110
+ record_class.merge_group( objects.insert_or_update, args )
111
+
112
+ objects.insert_on_missing.each_pair do |keys,records|
113
+
114
+ record_class.insert_on_missing_group( keys, records, args )
115
+ end
116
+
117
+ @scheduled_merges[record_class] = nil
118
+ objects.insert_or_update.each { |record| record.on_closed_scheduled_operation }
119
+ objects.insert_on_missing.values.each { |array| array.each{ |record| record.on_closed_scheduled_operation } }
120
+ end
121
+ end
122
+
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,52 @@
1
+ # Will fetch the next N values from a sequence and store it in a queue.
2
+ # Now the values will can be fetched from the queue without any database roundtrip.
3
+ # If the queue is empty, the next N values will be fetched from database and stored in the queue.
4
+ #
5
+ # It's using the DUAL table so it should only work with oracle.
6
+ #
7
+ # Author:: Andre Kullmann
8
+ #
9
+ module ActiveRecord
10
+ module Bulkoperation
11
+ module Util
12
+ class SequenceCache
13
+
14
+ #
15
+ # seq - the sequence name to use with the SequenceCache object
16
+ # refetch - how many values should be fetch from the database with each roundtrip
17
+ def initialize(seq, prefetch = 10)
18
+ @seq, @prefetch = seq, prefetch
19
+ @queue = Queue.new
20
+ end
21
+
22
+ #
23
+ # return - the next value from the sequence
24
+ def next_value
25
+ while ( value = next_value_from_queue ).nil?
26
+ fill_queue
27
+ end
28
+ value
29
+ end
30
+
31
+ private
32
+
33
+ def next_value_from_queue
34
+ @queue.pop(true)
35
+ rescue ThreadError => e
36
+ nil
37
+ end
38
+
39
+ def fill_queue
40
+ fetch.each do |f|
41
+ @queue << f
42
+ end
43
+ end
44
+
45
+ def fetch
46
+ st = ActiveRecord::Base.connection.exec_query("SELECT #{@seq}.nextval id FROM dual connect by level <= :a", "SQL", [[nil,@prefetch]])
47
+ st.map {|r| r['id']}
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,36 @@
1
+ # A TransactionObject belongs to a connection. It's like a singleton for each transaction.
2
+ #
3
+ # Author:: Andre Kullmann
4
+ #
5
+ module ActiveRecord
6
+ module Bulkoperation
7
+ module Util
8
+ class TransactionObject
9
+ def self.get
10
+ result = ActiveRecord::Base.connection.connection_listeners.select { |l| l.class == self }.first
11
+ unless result
12
+ result = new
13
+ ActiveRecord::Base.connection.connection_listeners << result
14
+ end
15
+ result
16
+ end
17
+
18
+ def after_commit
19
+ close
20
+ ActiveRecord::Base.connection.connection_listeners.delete(self)
21
+ end
22
+
23
+ def after_rollback
24
+ close
25
+ ActiveRecord::Base.connection.connection_listeners.delete(self)
26
+ end
27
+
28
+ def after_rollback_to_savepoint
29
+ end
30
+
31
+ def close
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module Bulkoperation
3
+ VERSION = "0.0.2"
4
+ end
5
+ end
@@ -0,0 +1,41 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ class ActiveRecordConnectionTest < ActiveSupport::TestCase
4
+ def test_method_definition
5
+ assert ActiveRecord::Base.connection.respond_to? :connection_listeners
6
+ assert ActiveRecord::Base.connection.respond_to? :commit_db_transaction
7
+ assert ActiveRecord::Base.connection.respond_to? :rollback_db_transaction
8
+ assert ActiveRecord::Base.connection.respond_to? :rollback_to_savepoint
9
+ assert ActiveRecord::Base.connection.respond_to? :create_savepoint
10
+
11
+ assert ActiveRecord::Base.connection.respond_to? :commit_db_transaction_without_callback
12
+ assert ActiveRecord::Base.connection.respond_to? :rollback_db_transaction_without_callback
13
+ assert ActiveRecord::Base.connection.respond_to? :rollback_to_savepoint_without_callback
14
+ assert ActiveRecord::Base.connection.respond_to? :create_savepoint_without_callback
15
+ end
16
+
17
+ class MyTestClass
18
+ def before_commit
19
+ puts "get calllsslsl"
20
+ end
21
+
22
+ def after_rollback
23
+ puts "get calllsslsl"
24
+ end
25
+ end
26
+
27
+ def test_method_calls_definition
28
+ test_class = MyTestClass.new
29
+ test_class.expects(:before_commit)
30
+ test_class.expects(:after_commit)
31
+
32
+ test_class.expects(:after_rollback)
33
+
34
+ ActiveRecord::Base.connection.connection_listeners << test_class
35
+
36
+ ActiveRecord::Base.connection.rollback_db_transaction
37
+
38
+ ActiveRecord::Base.connection.commit_db_transaction
39
+ ActiveRecord::Base.connection.connection_listeners.clear
40
+ end
41
+ end
@@ -0,0 +1 @@
1
+ ENV["DB_ADAPTER"] = "oracle_enhanced"
@@ -0,0 +1,176 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ class BulkoperationTest < ActiveSupport::TestCase
4
+
5
+ def teardown
6
+ TestTable.delete_all
7
+ end
8
+
9
+ def test_bulk_operation_methods
10
+ assert ActiveRecord::Base.respond_to? :flush_scheduled_operations
11
+ test_obj = TestTable.new
12
+ assert test_obj.respond_to? :schedule_merge
13
+ end
14
+
15
+ def test_bulk_operation_workflow
16
+ test_obj = TestTable.new
17
+ test_obj.author_name = 'test-workflow'
18
+ test_obj.schedule_merge
19
+
20
+ TestTable.flush_scheduled_operations
21
+ record = TestTable.first
22
+ assert_equal('test-workflow',record.author_name)
23
+ end
24
+
25
+ def test_multi_thread
26
+ thread_count = 5
27
+ inner_count = 10
28
+ threads = []
29
+ thread_count.times do |t|
30
+ t = Thread.new do
31
+ inner_count.times do |i|
32
+ test_obj = TestTable.new
33
+ test_obj.author_name = "t1-#{1}"
34
+ test_obj.schedule_merge
35
+ end
36
+ TestTable.flush_scheduled_operations
37
+ end
38
+ threads << t
39
+ end
40
+ threads.each do |t|
41
+ t.join
42
+ end
43
+
44
+ assert_equal (thread_count * inner_count) , TestTable.all.count
45
+ end
46
+
47
+ def test_multi_update
48
+ thread_count = 5
49
+ inner_count = 10
50
+ threads = []
51
+ thread_count.times do |t|
52
+ t = Thread.new do
53
+ inner_count.times do |i|
54
+ test_obj = TestTable.new
55
+ test_obj.author_name = "t1-#{1}"
56
+ test_obj.schedule_merge
57
+ end
58
+ TestTable.flush_scheduled_operations
59
+ end
60
+ threads << t
61
+ end
62
+ threads.each do |t|
63
+ t.join
64
+ end
65
+
66
+ assert_equal (thread_count * inner_count) , TestTable.all.count
67
+ TestTable.all.each do |t|
68
+ t.author_name = "tr-fff"
69
+ t.schedule_merge
70
+ end
71
+ TestTable.flush_scheduled_operations
72
+ end
73
+
74
+ def test_update_fk_relation
75
+ #some problems during database creation and recreation
76
+ return
77
+ group = Group.new
78
+ group.schedule_merge
79
+ test_obj = TestTable.new
80
+ test_obj.author_name = 'test-1'
81
+ test_obj.group_id = group.id
82
+ test_obj.schedule_merge
83
+
84
+ ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush
85
+
86
+ TestTable.flush_scheduled_operations
87
+
88
+
89
+ first = TestTable.first
90
+ assert_equal('test-1',test_obj.author_name)
91
+
92
+ first.author_name = 'test-2'
93
+ first.schedule_merge
94
+ TestTable.flush_scheduled_operations
95
+
96
+ assert_equal('test-2',first.author_name)
97
+ end
98
+
99
+ def test_connection_listener_get_called
100
+ test_obj = TestTable.new
101
+ test_obj.author_name = 'test-1'
102
+ test_obj.schedule_merge
103
+
104
+ assert_equal(0,TestTable.count)
105
+ ActiveRecord::Base.connection.commit_db_transaction
106
+ assert_equal(1,TestTable.count)
107
+ first = TestTable.first
108
+ assert_equal('test-1',test_obj.author_name)
109
+ TestTable.delete_all
110
+ end
111
+
112
+ def test_schedule_merge_relation
113
+ group = Group.new
114
+ group.schedule_merge
115
+ test_obj = TestTable.new
116
+ test_obj.author_name = 'test-1'
117
+ group.test_tables.schedule_merge(test_obj)
118
+
119
+ ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush
120
+ end
121
+
122
+ def test_schedule_merge_has_and_belongs_to_many_relation
123
+ part = Part.new
124
+ part.schedule_merge
125
+ assembly = Assembly.new
126
+ part.assemblies.schedule_merge(assembly)
127
+ ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush
128
+ db_part = Part.first
129
+ db_assembly = Assembly.first
130
+
131
+ assert_equal(1,db_part.assemblies.count)
132
+ assert_equal(db_assembly[:id],db_part.assemblies.first[:id])
133
+
134
+ assert_equal(1,db_assembly.parts.count)
135
+ assert_equal(db_part[:id],db_assembly.parts.first[:id])
136
+ end
137
+
138
+ def test_schedule_merge_has_and_belongs_to_many_relation_custom_table_and_columns
139
+
140
+ course = Course.new
141
+ course.course_id = 10
142
+ course.schedule_merge
143
+ student = Student.new
144
+ student.student_id = 12
145
+ course.students.schedule_merge(student)
146
+ ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush
147
+ return
148
+ db_part = Course.first
149
+ db_assembly = Assembly.first
150
+
151
+ assert_equal(1,db_part.assemblies.count)
152
+ assert_equal(db_assembly[:id],db_part.assemblies.first[:id])
153
+
154
+ assert_equal(1,db_assembly.parts.count)
155
+ assert_equal(db_part[:id],db_assembly.parts.first[:id])
156
+ end
157
+
158
+ def test_schedule_merge_has_and_belongs_to_many_relation_self_join
159
+ product = Product.new
160
+ product2 = Product.new
161
+ product.schedule_merge
162
+ product.related_products.schedule_merge(product2)
163
+ ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush
164
+ return
165
+ db_part = Course.first
166
+ db_assembly = Assembly.first
167
+
168
+ assert_equal(1,db_part.assemblies.count)
169
+ assert_equal(db_assembly[:id],db_part.assemblies.first[:id])
170
+
171
+ assert_equal(1,db_assembly.parts.count)
172
+ assert_equal(db_part[:id],db_assembly.parts.first[:id])
173
+ end
174
+
175
+ end
176
+
data/test/database.yml ADDED
@@ -0,0 +1,8 @@
1
+ oracle_enhanced:
2
+ adapter: oracle_enhanced
3
+ driver: oracle.jdbc.driver.OracleDriver
4
+ url: jdbc:oracle:thin:@localhost:1521:ORCL
5
+ username: test
6
+ password: test
7
+ database: ORCL
8
+ pool: 200