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