Skip to content

Instantly share code, notes, and snippets.

@marekjelen
Created July 20, 2018 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marekjelen/60bcf6edcac53cce865dc08b2add71cf to your computer and use it in GitHub Desktop.
Save marekjelen/60bcf6edcac53cce865dc08b2add71cf to your computer and use it in GitHub Desktop.
CTE based tree in Ruby on Rails
def ancestors(options = {})
options[:direction] ||= :up
self.tree_relation_builder(options)
end
def descendants(options = {})
options[:direction] ||= :down
self.tree_relation_builder(options)
end
def ancestors_and_self(options = {})
options[:self] ||= true
ancestors(options)
end
def descendants_and_self(options = {})
options[:self] ||= true
descendants(options)
end
def root
ancestors.last
end
def root?
self.parent == nil
end
def path
ancestors_and_self(reverse: true)
end
def children
self.class.where(:parent => self)
end
def has_parent?
self.parent != nil
end
def has_children?
self.class.exists?(:parent => self)
end
def siblings(options = {})
options[:self] ||= false
sbs = self.class.where(:parent => self.parent)
sbs = sbs.where.not(id: self.id) unless options[:self]
sbs
end
def siblings_and_self
self.siblings(self: true)
end
def has_siblings?
self.siblings.exists?
end
def depth
self.ancestors.length
end
def parent_of?(item)
item.parent == self
end
def child_of?(item)
self.parent == item
end
def root_of?(item)
item.root == self
end
def ancestor_of?(item)
item.ancestors.include?(self)
end
def tree_sql_builder(options = {})
options[:self] ||= false
options[:id] ||= self.id
options[:before_depth] ||= nil
options[:to_depth] ||= nil
options[:at_depth] ||= nil
options[:from_depth] ||= nil
options[:after_depth] ||= nil
options[:direction] ||= :up
options[:reverse] ||= false
t = "\"#{self.class.table_name}\""
if options[:direction] == :down
direction_condition = "#{t}.\"parent_id\" = \"tree.id\""
else
direction_condition = "#{t}.\"id\" = \"tree\".\"parent_id\""
end
unless options[:self]
options[:from_depth] ||= 1
end
depth_pre_condition = depth_post_condition = ''
depth_pre_condition += " AND \"tree\".\"depth\" < #{options[:before_depth]}" if options[:before_depth]
depth_pre_condition += " AND \"tree\".\"depth\" <= #{options[:to_depth]}" if options[:to_depth]
depth_pre_condition += " AND \"tree\".\"depth\" = #{options[:at_depth]}" if options[:at_depth]
depth_post_condition += " AND \"tree\".\"depth\" >= #{options[:from_depth]}" if options[:from_depth]
depth_post_condition += " AND \"tree\".\"depth\" > #{options[:after_depth]}" if options[:after_depth]
if options[:reverse]
reverse_order = "DESC"
else
reverse_order = "ASC"
end
<<-EOF
(WITH RECURSIVE "tree" ("id", "parent_id", "path", "depth") AS (
SELECT #{t}."id", #{t}."parent_id", ARRAY["id"], 0 FROM #{t}
WHERE #{t}."id"=#{options[:id]}
UNION ALL
SELECT #{t}."id", #{t}."parent_id", "tree"."path" || #{t}."id", "tree"."depth" + 1 FROM "tree", #{t}
WHERE NOT #{t}."id" = ANY("path")
AND #{direction_condition} #{depth_pre_condition}
) SELECT #{t}.*, "tree".* FROM #{t}, "tree"
WHERE #{t}."id" = "tree"."id" #{depth_post_condition}
ORDER BY "tree"."path" #{reverse_order})
AS #{t}
EOF
end
def tree_relation_builder(options = {})
self.class.from(tree_sql_builder(options))
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment