Created
July 20, 2018 16:38
-
-
Save marekjelen/60bcf6edcac53cce865dc08b2add71cf to your computer and use it in GitHub Desktop.
CTE based tree in Ruby on Rails
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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