feat(orm): extend migrator with status and unscoped

This commit is contained in:
dualfroz committed 2026-09-08 15:06:17 +02:00
1 parent 2aede88152
commit 13b4613023
2 files changed
+175 -5

No files matched your search

+9
View File
@@ -20,6 +20,15 @@ module Prism
entries.reject { |entry| applied.include?(entry.version) }
end
# Pairs every migration file on disk with whether it has been applied
# to this store, in the same order `entries`/`pending` use - the data
# `prism db:status` (like `rails db:migrate:status`) prints, but kept
# here so it is available to any caller, not just the CLI.
def status
applied = @store.applied_migrations
entries.map { |entry| [entry, applied.include?(entry.version)] }
end
def migrate!
applied = pending.map { |entry| apply(entry) }
applied.map(&:version)
+166 -5
View File
@@ -5,7 +5,22 @@ module Prism
class Model
include Validatable
include Callbacks
include Attributes
include Dirty
include Locking
include Pessimistic
include Paranoia
include ListPosition
include Serialization
include Associable
include NestedAttributes
include SecurePassword
include SecureToken
include TokenFor
include Encryption
include Transactional
include Enum
class << self
def store
@@ -58,17 +73,44 @@ module Prism
def find(id)
row = store.find(table_name, id)
raise RecordNotFound, "no #{name} with id #{id}" unless row
raise RecordNotFound, "no #{name} with id #{id}" if paranoid? && !row[paranoid_column].nil?
instantiate(row)
end
def all
base = @default_scope_block ? Relation.new(self).instance_exec(&@default_scope_block) : Relation.new(self)
paranoid_scope(base)
end
def unscoped
Relation.new(self)
end
def default_scope(&block)
@default_scope_block = block
end
def scopes
@scopes ||= {}
end
def scope(name, body)
scopes[name.to_sym] = body
define_singleton_method(name) { |*args| all.instance_exec(*args, &body) }
end
def where(conditions)
all.where(conditions)
end
def not(conditions)
all.not(conditions)
end
def none
all.none
end
def order(column, direction = :asc)
all.order(column, direction)
end
@@ -77,20 +119,90 @@ module Prism
all.limit(count)
end
def distinct
all.distinct
end
def find_by(conditions)
all.find_by(conditions)
end
def find_by!(conditions)
all.find_by!(conditions)
end
def first
all.first
end
def first!
all.first!
end
def last
all.last
end
def last!
all.last!
end
def sum(column)
all.sum(column)
end
def average(column)
all.average(column)
end
def minimum(column)
all.minimum(column)
end
def maximum(column)
all.maximum(column)
end
def group(column)
all.group(column)
end
def find_each(batch_size: 1000, &block)
all.find_each(batch_size: batch_size, &block)
end
def find_in_batches(batch_size: 1000, &block)
all.find_in_batches(batch_size: batch_size, &block)
end
def pick(*attributes)
all.pick(*attributes)
end
def ids
all.ids
end
def page(number)
all.page(number)
end
def per(size)
all.per(size)
end
def count
store.count(table_name)
(@default_scope_block || paranoid?) ? all.count : store.count(table_name)
end
# Raw counter bump used by belongs_to counter_cache - see
# Store#increment_counter for why this bypasses Model#save.
def increment_counter(counter_name, id, by = 1)
store.increment_counter(table_name, id, counter_name, by)
end
def decrement_counter(counter_name, id, by = 1)
increment_counter(counter_name, id, -by)
end
end
@@ -99,6 +211,7 @@ module Prism
@attributes = {}
@new_record = true
@destroyed = false
apply_attribute_defaults
assign(attributes)
end
@@ -107,13 +220,25 @@ module Prism
end
def []=(key, value)
@attributes[key.to_s] = value
key = key.to_s
track_attribute_change(key, @attributes[key], value)
@attributes[key] = value
end
def id
self['id']
end
# Rails' `to_param` hook: the value a model contributes to a URL when
# a route segment is built from it. The router never calls this
# itself (every url_for/path caller passes an explicit params hash,
# see lib/prism/renderable.rb#url_for) - it exists so a model can
# define ONE place to override (e.g. a slug) and callers opt in by
# writing `post.to_param` instead of `post.id`.
def to_param
id&.to_s
end
def new_record?
@new_record
end
@@ -126,19 +251,55 @@ module Prism
@destroyed
end
# Mass-assignment goes through a writer method when one is defined
# (plain columns, associations, and virtual attributes like
# SecurePassword's `password=`) so `Model.new(password: 'x')` runs
# the same logic as calling `record.password = 'x'` by hand, instead
# of poking the raw column hash and skipping it.
def assign(attributes)
attributes.each { |key, value| self[key] = value }
attributes.each do |key, value|
setter = "#{key}="
respond_to?(setter) ? send(setter, value) : (self[key] = value)
end
self
end
# The whole before_save..after_save sequence runs inside one
# `transaction` so after_commit only fires once persistence is truly
# settled, and any exception raised by a callback rolls the write
# back (StaleObjectError included) instead of leaving it half-done.
# A `return false` from an aborted before_save/before_create still
# unwinds through that transaction as an uncommitted attempt, which
# is why after_rollback can fire even when nothing was ever written -
# there was never anything to roll back on disk, but the write this
# record attempted did not go through, matching what the hook name
# promises.
def save
return false unless valid?
touch_timestamps
if new_record?
creating = new_record?
self.class.transaction do
enlist_in_transaction(creating ? :create : :update)
return false unless run_callbacks(:before_save)
return false unless run_callbacks(creating ? :before_create : :before_update)
if creating
initialize_lock_version
@attributes['id'] = self.class.store.insert(self.class.table_name, @attributes)
@new_record = false
elsif self.class.locking_enabled?
begin
row = self.class.store.update(self.class.table_name, id, dirty_attributes,
[self.class.locking_column, lock_version.to_i])
rescue Store::StaleRowError
raise StaleObjectError.new(self, :update)
end
@attributes[self.class.locking_column] = row[self.class.locking_column]
else
self.class.store.update(self.class.table_name, id, @attributes)
self.class.store.update(self.class.table_name, id, dirty_attributes)
end
changes_applied
run_callbacks(creating ? :after_create : :after_update)
run_callbacks(:after_save)
end
true
end