sbf-dm-transactions 1.3.0.beta

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.
@@ -0,0 +1,112 @@
1
+ module DataMapper
2
+ class Transaction
3
+
4
+ module DataObjectsAdapter
5
+ extend Chainable
6
+
7
+ # Produces a fresh transaction primitive for this Adapter
8
+ #
9
+ # Used by Transaction to perform its various tasks.
10
+ #
11
+ # @return [Object]
12
+ # a new Object that responds to :close, :begin, :commit,
13
+ # and :rollback,
14
+ #
15
+ # @api private
16
+ def transaction_primitive
17
+ if current_transaction && supports_savepoints?
18
+ DataObjects::SavePoint.create_for_uri(normalized_uri, current_connection)
19
+ else
20
+ DataObjects::Transaction.create_for_uri(normalized_uri)
21
+ end
22
+ end
23
+
24
+ # Pushes the given Transaction onto the per thread Transaction stack so
25
+ # that everything done by this Adapter is done within the context of said
26
+ # Transaction.
27
+ #
28
+ # @param [Transaction] transaction
29
+ # a Transaction to be the 'current' transaction until popped.
30
+ #
31
+ # @return [Array(Transaction)]
32
+ # the stack of active transactions for the current thread
33
+ #
34
+ # @api private
35
+ def push_transaction(transaction)
36
+ transactions << transaction
37
+ end
38
+
39
+ # Pop the 'current' Transaction from the per thread Transaction stack so
40
+ # that everything done by this Adapter is no longer necessarily within the
41
+ # context of said Transaction.
42
+ #
43
+ # @return [Transaction]
44
+ # the former 'current' transaction.
45
+ #
46
+ # @api private
47
+ def pop_transaction
48
+ transactions.pop
49
+ end
50
+
51
+ # Retrieve the current transaction for this Adapter.
52
+ #
53
+ # Everything done by this Adapter is done within the context of this
54
+ # Transaction.
55
+ #
56
+ # @return [Transaction]
57
+ # the 'current' transaction for this Adapter.
58
+ #
59
+ # @api private
60
+ def current_transaction
61
+ transactions.last
62
+ end
63
+
64
+ chainable do
65
+ protected
66
+
67
+ # @api semipublic
68
+ def open_connection
69
+ current_connection || super
70
+ end
71
+
72
+ # @api semipublic
73
+ def close_connection(connection)
74
+ super unless current_connection.equal?(connection)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ # @api private
81
+ def transactions
82
+ Thread.current[:dm_transactions] ||= {}
83
+ Thread.current[:dm_transactions][object_id] ||= []
84
+ end
85
+
86
+ # Retrieve the current connection for this Adapter.
87
+ #
88
+ # @return [Transaction]
89
+ # the 'current' connection for this Adapter.
90
+ #
91
+ # @api private
92
+ def current_connection
93
+ if transaction = current_transaction
94
+ transaction.primitive_for(self).connection
95
+ end
96
+ end
97
+
98
+ # Indicate whether adapter supports transactional savepoints. Not all DO
99
+ # adapters do, so default to false.
100
+ #
101
+ # @return [Boolean]
102
+ # whether or not the adapter supports savepoints
103
+ #
104
+ # @api private
105
+ def supports_savepoints?
106
+ false
107
+ end
108
+
109
+ end # module DataObjectsAdapter
110
+
111
+ end # class Transaction
112
+ end # module DataMapper
@@ -0,0 +1,15 @@
1
+ require 'dm-transactions/adapters/dm-do-adapter'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+
6
+ module MysqlAdapter
7
+ include DataObjectsAdapter
8
+
9
+ def supports_savepoints?
10
+ true
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ require 'dm-transactions/adapters/dm-do-adapter'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+
6
+ module OracleAdapter
7
+ include DataObjectsAdapter
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ require 'dm-transactions/adapters/dm-do-adapter'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+
6
+ module PostgresAdapter
7
+ include DataObjectsAdapter
8
+
9
+ def supports_savepoints?
10
+ true
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ require 'dm-transactions/adapters/dm-do-adapter'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+
6
+ module SqliteAdapter
7
+ include DataObjectsAdapter
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'dm-transactions/adapters/dm-do-adapter'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+
6
+ module SqlserverAdapter
7
+ include DataObjectsAdapter
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module DataMapper
2
+ module Transactions
3
+ VERSION = '1.3.0.beta'
4
+ end
5
+ end
@@ -0,0 +1,443 @@
1
+ require 'dm-core'
2
+
3
+ module DataMapper
4
+ class Transaction
5
+ extend Chainable
6
+
7
+ # @api private
8
+ attr_accessor :state
9
+
10
+ # @api private
11
+ def none?
12
+ state == :none
13
+ end
14
+
15
+ # @api private
16
+ def begin?
17
+ state == :begin
18
+ end
19
+
20
+ # @api private
21
+ def rollback?
22
+ state == :rollback
23
+ end
24
+
25
+ # @api private
26
+ def commit?
27
+ state == :commit
28
+ end
29
+
30
+ # Create a new Transaction
31
+ #
32
+ # @see Transaction#link
33
+ #
34
+ # In fact, it just calls #link with the given arguments at the end of the
35
+ # constructor.
36
+ #
37
+ # @api public
38
+ def initialize(*things)
39
+ @transaction_primitives = {}
40
+ self.state = :none
41
+ @adapters = {}
42
+ link(*things)
43
+ if block_given?
44
+ warn "Passing block to #{self.class.name}.new is deprecated (#{caller[0]})"
45
+ commit { |*block_args| yield(*block_args) }
46
+ end
47
+ end
48
+
49
+ # Associate this Transaction with some things.
50
+ #
51
+ # @param [Object] things
52
+ # the things you want this Transaction associated with:
53
+ #
54
+ # Adapters::AbstractAdapter subclasses will be added as
55
+ # adapters as is.
56
+ # Arrays will have their elements added.
57
+ # Repository will have it's own @adapters added.
58
+ # Resource subclasses will have all the repositories of all
59
+ # their properties added.
60
+ # Resource instances will have all repositories of all their
61
+ # properties added.
62
+ #
63
+ # @param [Proc] block
64
+ # a block (taking one argument, the Transaction) to execute within
65
+ # this transaction. The transaction will begin and commit around
66
+ # the block, and rollback if an exception is raised.
67
+ #
68
+ # @api private
69
+ def link(*things)
70
+ unless none?
71
+ raise "Illegal state for link: #{state}"
72
+ end
73
+
74
+ things.each do |thing|
75
+ case thing
76
+ when DataMapper::Adapters::AbstractAdapter
77
+ @adapters[thing] = :none
78
+ when DataMapper::Repository
79
+ link(thing.adapter)
80
+ when DataMapper::Model
81
+ link(*thing.repositories)
82
+ when DataMapper::Resource
83
+ link(thing.model)
84
+ when Array
85
+ link(*thing)
86
+ else
87
+ raise "Unknown argument to #{self.class}#link: #{thing.inspect} (#{thing.class})"
88
+ end
89
+ end
90
+
91
+ if block_given?
92
+ commit { |*block_args| yield(*block_args) }
93
+ else
94
+ self
95
+ end
96
+ end
97
+
98
+ # Begin the transaction
99
+ #
100
+ # Before #begin is called, the transaction is not valid and can not be used.
101
+ #
102
+ # @api private
103
+ def begin
104
+ unless none?
105
+ raise "Illegal state for begin: #{state}"
106
+ end
107
+
108
+ each_adapter(:connect_adapter, [:log_fatal_transaction_breakage])
109
+ each_adapter(:begin_adapter, [:rollback_and_close_adapter_if_begin, :close_adapter_if_none])
110
+ self.state = :begin
111
+ end
112
+
113
+ # Commit the transaction
114
+ #
115
+ # If no block is given, it will simply commit any changes made since the
116
+ # Transaction did #begin.
117
+ #
118
+ # @param block<Block> a block (taking the one argument, the Transaction) to
119
+ # execute within this transaction. The transaction will begin and commit
120
+ # around the block, and roll back if an exception is raised.
121
+ #
122
+ # @api private
123
+ def commit
124
+ if block_given?
125
+ unless none?
126
+ raise "Illegal state for commit with block: #{state}"
127
+ end
128
+
129
+ begin
130
+ self.begin
131
+ rval = within { |*block_args| yield(*block_args) }
132
+ rescue Exception => exception
133
+ if begin?
134
+ rollback
135
+ end
136
+ raise exception
137
+ ensure
138
+ unless exception
139
+ if begin?
140
+ commit
141
+ end
142
+ return rval
143
+ end
144
+ end
145
+ else
146
+ unless begin?
147
+ raise "Illegal state for commit without block: #{state}"
148
+ end
149
+ each_adapter(:commit_adapter, [:log_fatal_transaction_breakage])
150
+ each_adapter(:close_adapter, [:log_fatal_transaction_breakage])
151
+ self.state = :commit
152
+ end
153
+ end
154
+
155
+ # Rollback the transaction
156
+ #
157
+ # Will undo all changes made during the transaction.
158
+ #
159
+ # @api private
160
+ def rollback
161
+ unless begin?
162
+ raise "Illegal state for rollback: #{state}"
163
+ end
164
+ each_adapter(:rollback_adapter_if_begin, [:rollback_and_close_adapter_if_begin, :close_adapter_if_none])
165
+ each_adapter(:close_adapter_if_open, [:log_fatal_transaction_breakage])
166
+ self.state = :rollback
167
+ end
168
+
169
+ # Execute a block within this Transaction.
170
+ #
171
+ # No #begin, #commit or #rollback is performed in #within, but this
172
+ # Transaction will pushed on the per thread stack of transactions for each
173
+ # adapter it is associated with, and it will ensures that it will pop the
174
+ # Transaction away again after the block is finished.
175
+ #
176
+ # @param block<Block> the block of code to execute.
177
+ #
178
+ # @api private
179
+ def within
180
+ unless block_given?
181
+ raise 'No block provided'
182
+ end
183
+
184
+ unless begin?
185
+ raise "Illegal state for within: #{state}"
186
+ end
187
+
188
+ adapters = @adapters
189
+
190
+ adapters.each_key do |adapter|
191
+ adapter.push_transaction(self)
192
+ end
193
+
194
+ begin
195
+ yield self
196
+ ensure
197
+ adapters.each_key do |adapter|
198
+ adapter.pop_transaction
199
+ end
200
+ end
201
+ end
202
+
203
+ # @api private
204
+ def method_missing(method, *args, &block)
205
+ first_arg = args.first
206
+
207
+ return super unless args.size == 1 && first_arg.kind_of?(Adapters::AbstractAdapter)
208
+ return super unless match = method.to_s.match(/\A(.*)_(if|unless)_(none|begin|rollback|commit)\z/)
209
+
210
+ action, condition, expected_state = match.captures
211
+ return super unless respond_to?(action, true)
212
+
213
+ state = state_for(first_arg).to_s
214
+ execute = (condition == 'if') == (state == expected_state)
215
+
216
+ send(action, first_arg) if execute
217
+ end
218
+
219
+ # @api private
220
+ def primitive_for(adapter)
221
+ unless @adapters.include?(adapter)
222
+ raise "Unknown adapter #{adapter}"
223
+ end
224
+
225
+ unless @transaction_primitives.include?(adapter)
226
+ raise "No primitive for #{adapter}"
227
+ end
228
+
229
+ @transaction_primitives[adapter]
230
+ end
231
+
232
+ private
233
+
234
+ # @api private
235
+ def validate_primitive(primitive)
236
+ [:close, :begin, :rollback, :commit].each do |meth|
237
+ unless primitive.respond_to?(meth)
238
+ raise "Invalid primitive #{primitive}: doesnt respond_to?(#{meth.inspect})"
239
+ end
240
+ end
241
+
242
+ primitive
243
+ end
244
+
245
+ # @api private
246
+ def each_adapter(method, on_fail)
247
+ adapters = @adapters
248
+ begin
249
+ adapters.each_key do |adapter|
250
+ send(method, adapter)
251
+ end
252
+ rescue Exception => exception
253
+ adapters.each_key do |adapter|
254
+ on_fail.each do |fail_handler|
255
+ begin
256
+ send(fail_handler, adapter)
257
+ rescue Exception => inner_exception
258
+ DataMapper.logger.fatal("#{self}#each_adapter(#{method.inspect}, #{on_fail.inspect}) failed with #{exception.inspect}: #{exception.backtrace.join("\n")} - and when sending #{fail_handler} to #{adapter} we failed again with #{inner_exception.inspect}: #{inner_exception.backtrace.join("\n")}")
259
+ end
260
+ end
261
+ end
262
+ raise exception
263
+ end
264
+ end
265
+
266
+ # @api private
267
+ def state_for(adapter)
268
+ unless @adapters.include?(adapter)
269
+ raise "Unknown adapter #{adapter}"
270
+ end
271
+
272
+ @adapters[adapter]
273
+ end
274
+
275
+ # @api private
276
+ def do_adapter(adapter, what, prerequisite)
277
+ unless @transaction_primitives.include?(adapter)
278
+ raise "No primitive for #{adapter}"
279
+ end
280
+
281
+ state = state_for(adapter)
282
+
283
+ unless state == prerequisite
284
+ raise "Illegal state for #{what}: #{state}"
285
+ end
286
+
287
+ DataMapper.logger.debug("#{adapter.name}: #{what}")
288
+ @transaction_primitives[adapter].send(what)
289
+ @adapters[adapter] = what
290
+ end
291
+
292
+ # @api private
293
+ def log_fatal_transaction_breakage(adapter)
294
+ DataMapper.logger.fatal("#{self} experienced a totally broken transaction execution. Presenting member #{adapter.inspect}.")
295
+ end
296
+
297
+ # @api private
298
+ def connect_adapter(adapter)
299
+ if @transaction_primitives.key?(adapter)
300
+ raise "Already a primitive for adapter #{adapter}"
301
+ end
302
+
303
+ @transaction_primitives[adapter] = validate_primitive(adapter.transaction_primitive)
304
+ end
305
+
306
+ # @api private
307
+ def close_adapter_if_open(adapter)
308
+ if @transaction_primitives.include?(adapter)
309
+ close_adapter(adapter)
310
+ end
311
+ end
312
+
313
+ # @api private
314
+ def close_adapter(adapter)
315
+ unless @transaction_primitives.include?(adapter)
316
+ raise 'No primitive for adapter'
317
+ end
318
+
319
+ @transaction_primitives[adapter].close
320
+ @transaction_primitives.delete(adapter)
321
+ end
322
+
323
+ # @api private
324
+ def begin_adapter(adapter)
325
+ do_adapter(adapter, :begin, :none)
326
+ end
327
+
328
+ # @api private
329
+ def commit_adapter(adapter)
330
+ do_adapter(adapter, :commit, :begin)
331
+ end
332
+
333
+ # @api private
334
+ def rollback_adapter(adapter)
335
+ do_adapter(adapter, :rollback, :begin)
336
+ end
337
+
338
+ # @api private
339
+ def rollback_and_close_adapter(adapter)
340
+ rollback_adapter(adapter)
341
+ close_adapter(adapter)
342
+ end
343
+
344
+ module Repository
345
+
346
+ # Produce a new Transaction for this Repository
347
+ #
348
+ # @return [Adapters::Transaction]
349
+ # a new Transaction (in state :none) that can be used
350
+ # to execute code #with_transaction
351
+ #
352
+ # @api public
353
+ def transaction
354
+ Transaction.new(self)
355
+ end
356
+ end # module Repository
357
+
358
+ module Model
359
+ # @api private
360
+ def self.included(mod)
361
+ mod.descendants.each { |model| model.extend self }
362
+ end
363
+
364
+ # Produce a new Transaction for this Resource class
365
+ #
366
+ # @return <Adapters::Transaction
367
+ # a new Adapters::Transaction with all Repositories
368
+ # of the class of this Resource added.
369
+ #
370
+ # @api public
371
+ def transaction
372
+ transaction = Transaction.new(self)
373
+ transaction.commit { |block_args| yield(*block_args) }
374
+ end
375
+ end # module Model
376
+
377
+ module Resource
378
+
379
+ # Produce a new Transaction for the class of this Resource
380
+ #
381
+ # @return [Adapters::Transaction]
382
+ # a new Adapters::Transaction for the Repository
383
+ # of the class of this Resource added.
384
+ #
385
+ # @api public
386
+ def transaction
387
+ model.transaction { |*block_args| yield(*block_args) }
388
+ end
389
+ end # module Resource
390
+
391
+ def self.include_transaction_api
392
+ [ :Repository, :Model, :Resource ].each do |name|
393
+ DataMapper.const_get(name).send(:include, const_get(name))
394
+ end
395
+ Adapters::AbstractAdapter.descendants.each do |adapter_class|
396
+ Adapters.include_transaction_api(DataMapper::Inflector.demodulize(adapter_class.name))
397
+ end
398
+ end
399
+
400
+ end # class Transaction
401
+
402
+ module Adapters
403
+
404
+ def self.include_transaction_api(const_name)
405
+ require transaction_extensions(const_name)
406
+ if Transaction.const_defined?(const_name)
407
+ adapter = const_get(const_name)
408
+ adapter.send(:include, transaction_module(const_name))
409
+ end
410
+ rescue LoadError
411
+ # Silently ignore the fact that no adapter extensions could be required
412
+ # This means that the adapter in use doesn't support transactions
413
+ end
414
+
415
+ def self.transaction_module(const_name)
416
+ Transaction.const_get(const_name)
417
+ end
418
+
419
+ class << self
420
+ private
421
+
422
+ # @api private
423
+ def transaction_extensions(const_name)
424
+ name = adapter_name(const_name)
425
+ name = 'do' if name == 'dataobjects'
426
+ "dm-transactions/adapters/dm-#{name}-adapter"
427
+ end
428
+
429
+ end
430
+
431
+ extendable do
432
+ # @api private
433
+ def const_added(const_name)
434
+ include_transaction_api(const_name)
435
+ super
436
+ end
437
+ end
438
+
439
+ end # module Adapters
440
+
441
+ Transaction.include_transaction_api
442
+
443
+ end # module DataMapper
@@ -0,0 +1,17 @@
1
+ require 'rspec'
2
+ require_relative 'require_spec'
3
+ require 'dm-core/spec/setup'
4
+
5
+ # To really test this behavior, this spec needs to be run in isolation and not
6
+ # as part of the typical rake spec run, which requires dm-transactions upfront
7
+
8
+ if %w(postgres mysql sqlite oracle sqlserver).include?(ENV['ADAPTER'])
9
+ describe "require 'dm-transactions after calling DataMapper.setup" do
10
+ before(:all) do
11
+ @adapter = DataMapper::Spec.adapter
12
+ require 'dm-transactions'
13
+ end
14
+
15
+ it_behaves_like "require 'dm-transactions'"
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'rspec'
2
+ require_relative 'require_spec'
3
+ require 'dm-core/spec/setup'
4
+
5
+ # To really test this behavior, this spec needs to be run in isolation and not
6
+ # as part of the typical rake spec run, which requires dm-transactions upfront
7
+
8
+ if %w(postgres mysql sqlite oracle sqlserver).include?(ENV['ADAPTER'])
9
+ describe "require 'dm-transactions' before calling DataMapper.setup" do
10
+ before(:all) do
11
+ require 'dm-transactions'
12
+ @adapter = DataMapper::Spec.adapter
13
+ end
14
+
15
+ it_behaves_like "require 'dm-transactions'"
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ shared_examples "require 'dm-transactions'" do
2
+ %w[ Repository Model Resource ].each do |name|
3
+ it "includes the transaction api in DataMapper::#{name}" do
4
+ expect((DataMapper.const_get(name) < DataMapper::Transaction.const_get(name))).to be(true)
5
+ end
6
+ end
7
+
8
+ it "includes the transaction api into the adapter" do
9
+ expect(@adapter.respond_to?(:push_transaction)).to be(true)
10
+ expect(@adapter.respond_to?(:pop_transaction)).to be(true)
11
+ expect(@adapter.respond_to?(:current_transaction)).to be(true)
12
+ end
13
+ end