Skip to content

feat: Add sort cache column #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/generators/active_record/templates/migration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class AddResortFieldsTo<%= table_name.camelize %> < ActiveRecord::Migration
# Adds Resort fields, next_id and first, and indexes to a given model
def self.up
add_column :<%= table_name %>, :next_id, :integer
add_column :<%= table_name %>, :sort, :integer
add_column :<%= table_name %>, :first, :boolean
add_index :<%= table_name %>, :next_id
add_index :<%= table_name %>, :first
Expand All @@ -11,6 +12,7 @@ def self.up
# Removes Resort fields
def self.down
remove_column :<%= table_name %>, :next_id
remove_column :<%= table_name %>, :sort
remove_column :<%= table_name %>, :first
end
end
Expand Down
78 changes: 68 additions & 10 deletions lib/resort.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def included(base)
base.send :include, InstanceMethods

base.has_one :previous, class_name: base.name, foreign_key: 'next_id', inverse_of: :next
base.belongs_to :next, class_name: base.name, inverse_of: :previous
base.belongs_to :next, class_name: base.name, inverse_of: :previous, optional: true

base.after_create :include_in_list!
base.after_destroy :delete_from_list
Expand All @@ -85,11 +85,17 @@ def last_in_order
all.where(next_id: nil).first
end

# Returns chain in order.
# @return ActiveRecord::Relation the ordered elements
def ordered
all.except(:order).order(:sort)
end

# Returns eager-loaded Components in order.
#
# OPTIMIZE: Use IdentityMap when available
# @return [Array<ActiveRecord::Base>] the ordered elements
def ordered
def eager_ordered
ordered_elements = []
elements = {}

Expand Down Expand Up @@ -135,6 +141,17 @@ def siblings
self.class.all
end

# Regenerate the sort based on the eager ordered list
def regenerate_sort!
transaction do
elements = siblings.eager_ordered
elements.each { |element| element.lock! }
elements.each_with_index do |element, index|
raise(ActiveRecord::RecordNotSaved) unless element.update_attributes(sort: index + 1)
end
end
end

# Includes the object in the linked list.
#
# If there are no other objects, it prepends the object so that it is
Expand All @@ -156,10 +173,11 @@ def prepend
if _siblings.count > 0
delete_from_list
old_first = _siblings.first_in_order
raise ActiveRecord::RecordNotSaved, "[Resort] - Couldn't set next_id from previous first element." unless update_attribute(:next_id, old_first.id)
raise ActiveRecord::RecordNotSaved, "[Resort] - Couldn't reset previous first element" unless old_first.update_attribute(:first, false)
raise ActiveRecord::RecordNotSaved, "[Resort] - Couldn't set next_id from previous first element." unless update_attributes(next_id: old_first.id)
raise ActiveRecord::RecordNotSaved, "[Resort] - Couldn't reset previous first element" unless old_first.update_attributes(first: false)
end
raise(ActiveRecord::RecordNotSaved) unless update_attribute(:first, true)
raise(ActiveRecord::RecordNotSaved) unless update_attributes(first: true, sort: 1)
_increase_next_element_sort(self) if siblings.count > 0
end
end

Expand All @@ -177,13 +195,24 @@ def append_to(another)
lock!
return if another.next_id == id
another.lock!
old_sort = sort
delete_from_list
if next_id || (another && another.next_id)
raise ActiveRecord::RecordNotSaved, "[Resort] - Couldn't append element" unless update_attribute(:next_id, another.next_id)
raise ActiveRecord::RecordNotSaved, "[Resort] - Couldn't append element" unless update_attributes(next_id: another.next_id)
end
if another
raise ActiveRecord::RecordNotSaved, "[Resort] - Couldn't set this element to another's next" unless another.update_attribute(:next_id, id)
raise ActiveRecord::RecordNotSaved, "[Resort] - Couldn't set this element to another's next" unless another.update_attributes(next_id: id)
end
new_sort =
if old_sort.nil?
another.sort + 1
elsif old_sort < another.sort
another.sort - 1
elsif old_sort > another.sort
another.sort
end
raise ActiveRecord::RecordNotSaved unless update_attributes(sort: new_sort)
_increase_next_element_sort(another)
end
end

Expand All @@ -194,7 +223,8 @@ def last?
def last!
self.class.transaction do
lock!
raise(ActiveRecord::RecordNotSaved) unless _siblings.last_in_order.update_attribute(:next_id, id)
raise(ActiveRecord::RecordNotSaved) unless _siblings.last_in_order.update_attributes(next_id: id)
raise(ActiveRecord::RecordNotSaved) unless update_attributes(sort: _siblings.count + 1)
end
end

Expand All @@ -203,16 +233,19 @@ def last!
def delete_from_list
if first? && self.next
self.next.lock!
raise(ActiveRecord::RecordNotSaved) unless self.next.update_attribute(:first, true)
raise(ActiveRecord::RecordNotSaved) unless self.next.update_attributes(first: true)
_decrease_next_element_sort(self)
elsif previous
previous.lock!
p = previous
self.previous = nil unless frozen?
raise(ActiveRecord::RecordNotSaved) unless p.update_column(:next_id, next_id)
raise(ActiveRecord::RecordNotSaved) unless p.update_attributes(next_id: next_id)
_decrease_next_element_sort(p) if p.next
end
unless frozen?
self.first = false
self.next = nil
self.sort = nil
save!
end
end
Expand All @@ -221,6 +254,31 @@ def _siblings
table = self.class.arel_table
siblings.where(table[:id].not_eq(id))
end

def _increase_previous_element_sort(element, sort_limit = nil)
_change_element_sort(element, :+, :previous, sort_limit, :<)
end

def _decrease_previous_element_sort(element, sort_limit = nil)
_change_element_sort(element, :-, :previous, sort_limit, :<)
end

def _increase_next_element_sort(element, sort_limit = nil)
_change_element_sort(element, :+, :next, sort_limit, :>)
end

def _decrease_next_element_sort(element, sort_limit = nil)
_change_element_sort(element, :-, :next, sort_limit, :>)
end

def _change_element_sort(element, operation_method, sibling_method, sort_limit = nil, sort_operator = nil)
loop do
element = element.public_send(sibling_method)
break if element.nil? or (sort_limit and element.sort.send(sort_operator, sort_limit))
element.lock!
raise(ActiveRecord::RecordNotSaved) unless element.update_attributes(sort: element.sort.public_send(operation_method, 1))
end
end
end
end
# Helper class methods to be injected into ActiveRecord::Base class.
Expand Down
Loading