feat(orm): add Prism and Orm
This commit is contained in:
1 parent
39e69b4cfe
commit
b41a513317
1 file changed
+132
@@ -0,0 +1,132 @@
|
||||
module Prism
|
||||
module Orm
|
||||
# Raised inside a `transaction` block to roll back every write made so
|
||||
# far without the error propagating to the caller - mirrors
|
||||
# ActiveRecord::Rollback exactly (`transaction { ...; raise Rollback }`
|
||||
# returns nil from the block, no exception visible outside it). Any
|
||||
# OTHER exception rolls back the same way but keeps propagating past
|
||||
# the transaction boundary, same as Rails.
|
||||
class Rollback < StandardError; end
|
||||
|
||||
# Tracks, per Store, which records took part in the currently-open
|
||||
# (possibly nested) transaction, so after_commit/after_rollback fire
|
||||
# exactly once, only when the OUTERMOST transaction actually finishes.
|
||||
# A plain nested `transaction` block is not a real commit point - it
|
||||
# mirrors Store#within_transaction, which already flattens nested calls
|
||||
# into the single enclosing PStore transaction, so a raise from deep
|
||||
# inside a chain of nested `Model.transaction` blocks still rolls back
|
||||
# every write made by any of them, not just the innermost one.
|
||||
#
|
||||
# `transaction(requires_new: true)`, in contrast, is a real SAVEPOINT:
|
||||
# its participants live in their own frame on `@frames`, separate from
|
||||
# whatever frame is beneath them, so a raise inside only rolls back and
|
||||
# fires after_rollback for that frame - the parent frame(s) are
|
||||
# untouched and keep waiting for the real outermost commit/rollback.
|
||||
class TransactionRegistry
|
||||
def initialize
|
||||
@depth = 0
|
||||
@frames = []
|
||||
end
|
||||
|
||||
def nested?
|
||||
@depth.positive?
|
||||
end
|
||||
|
||||
def within
|
||||
@depth += 1
|
||||
@frames.push([]) if @depth == 1
|
||||
outermost = @depth == 1
|
||||
committed = false
|
||||
begin
|
||||
result = yield
|
||||
committed = true
|
||||
result
|
||||
rescue Rollback
|
||||
nil
|
||||
ensure
|
||||
@depth -= 1
|
||||
finish(committed) if outermost
|
||||
end
|
||||
end
|
||||
|
||||
# Runs block in a fresh participant frame with a table snapshot taken
|
||||
# first. On success the frame is merged into its parent, still
|
||||
# waiting for that parent's own commit/rollback. On failure the store
|
||||
# is restored to the snapshot and after_rollback fires immediately for
|
||||
# just this frame - Rollback is swallowed (mirrors `within`), any
|
||||
# other exception still propagates so an enclosing transaction can
|
||||
# also decide to abort.
|
||||
def within_savepoint(store)
|
||||
@depth += 1
|
||||
@frames.push([])
|
||||
snapshot = store.snapshot_tables
|
||||
begin
|
||||
result = yield
|
||||
frame = @frames.pop
|
||||
@frames.last.concat(frame)
|
||||
result
|
||||
rescue Rollback
|
||||
rollback_frame(store, snapshot)
|
||||
nil
|
||||
rescue StandardError
|
||||
rollback_frame(store, snapshot)
|
||||
raise
|
||||
ensure
|
||||
@depth -= 1
|
||||
end
|
||||
end
|
||||
|
||||
# No-op outside any open transaction, so a bare `record.save` with no
|
||||
# surrounding `Model.transaction` block never queues a participant
|
||||
# for a commit/rollback that will never come from here (the write's
|
||||
# OWN implicit transaction, opened by Model#save itself, is what
|
||||
# fires its callbacks in that case).
|
||||
def enlist(record, operation)
|
||||
@frames.last << [record, operation] if @depth.positive?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rollback_frame(store, snapshot)
|
||||
store.restore_tables(snapshot)
|
||||
@frames.pop.each { |record, operation| record.send(:fire_transaction_callback, :after_rollback, operation) }
|
||||
end
|
||||
|
||||
def finish(committed)
|
||||
batch = @frames.pop
|
||||
kind = committed ? :after_commit : :after_rollback
|
||||
batch.each { |record, operation| record.send(:fire_transaction_callback, kind, operation) }
|
||||
end
|
||||
end
|
||||
|
||||
# Mixed into Model: gives every model class/instance a `transaction`
|
||||
# that groups writes (its own and any other model sharing the same
|
||||
# underlying Store) into one atomic unit, plus after_commit/
|
||||
# after_rollback hooks that only fire once that unit is truly settled.
|
||||
module Transactional
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def transaction(requires_new: false, &block)
|
||||
store.transaction_with_callbacks(requires_new: requires_new, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def transaction(requires_new: false, &block)
|
||||
self.class.transaction(requires_new: requires_new, &block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def enlist_in_transaction(operation)
|
||||
self.class.store.transaction_registry.enlist(self, operation)
|
||||
end
|
||||
|
||||
def fire_transaction_callback(kind, operation)
|
||||
run_callbacks(kind, operation)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user