Skip to content

Instantly share code, notes, and snippets.

@sdhull
Last active January 16, 2018 18:45
Show Gist options
  • Save sdhull/a7d35d04ca3e579d08d68e0fae8d50d8 to your computer and use it in GitHub Desktop.
Save sdhull/a7d35d04ca3e579d08d68e0fae8d50d8 to your computer and use it in GitHub Desktop.
class Node < ApplicationRecord
# class that implements materialized path hierarchy
# a la https://medium.com/notes-from-a-messy-desk/representing-trees-in-postgresql-cbcdae419022
# Normal associations don't work
#
# has_many :children, ->(node) { where(path: node.path + [node.id]) }, class_name: "Node", foreign_key: nil
# primary_key: nil,
# inverse_of: :parent
# belongs_to :parent, ->(node) { find_by_sql(id: node.path.last) },
# class_name: "Node",
# foreign_key: nil,
# primary_key: nil,
# inverse_of: :children,
# optional: true
def ancestors
if path.present?
@ancestors ||= self.class.where(id: path)
else
self.class.none
end
end
def children
@children ||= self.class.where(path: path + [id])
end
def siblings
@siblings ||= self.class.where(path: path)
end
def descendants
@descendants ||= self.class.where(":id = ANY(path)", id: id)
end
def parent=(node)
self.path = node.path + [node.id]
@root = @parent.root
@parent = node
end
def parent
@parent ||= self.class.find_by_id(self.path.last) if self.path.last
end
def root
@root ||= if root_id = path.first
self.class.find(root_id)
else
self
end
end
scope :roots { where(path: []) }
# Must be last in scope method chain
scope :load_descendants!, -> {
scope_copy = all.load
ids = scope_copy.map(&:id)
inclusive_descendants = unscoped.where("(path && ARRAY[:ids]::int[]) OR (id IN (:ids))", ids: ids)
id_map = {}
child_map = Hash.new { Array.new }
inclusive_descendants.each do |descendant|
id_map[descendant.id] = descendant
child_map[descendant.path.last] += [descendant]
end
scope_copy.each do |node|
id_map[node.id] = node
end
child_map.each do |id, children|
next if id.nil? # roots
parent = id_map[id]
parent.children.send(:load_records, children)
children.each { |c| c.parent = parent }
end
scope_copy
}
# if you know for sure that a parent will always
# be earlier in the descendants list than its own children
def load_descendants!
id_map = {id: self}
descendants.each do |descendant|
id_map[descendant.id] = descendant
if parent = id_map[descendant.path.last]
if parent.children.instance_variable_get("@records").frozen?
parent.children.instance_variable_set("@records", [])
parent.children.instance_variable_set("@loaded", true) # no way of knowing if this is the last child for this parent
end
parent.children.records.push(descendant)
end
end
self
end
# if children may appear in the #descendants list prior to their
# own parents
def load_descendants!
id_map = {id: self}
child_map = Hash.new { Array.new }
descendants.each do |descendant|
id_map[descendant.id] = descendant
child_map[descendant.path.last] += [descendant]
end
child_map.each do |id, children|
parent = id_map[id]
parent.children.send(:load_records, children)
end
self
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment