marshall-lee/acts_as_ltree

object's descendants and children enhancements

marshall-lee opened this issue · 0 comments

Currently, object.descendants is actualy a call to Model.descendants_of(object.path, min_depth: 1). min_depth: 1 here is used because both @> operator and "#{path}.*" lquery pattern also match rows with paths that are equal to object.path. But we don't want to see the object among results of querying descendants and children. So we use "#{path}.{1,}" and "#{path},{1}" lquery patterns for descendants and children respectively. It works well but it's not exactly what should happen and now i'll tell why.

In 'classic' definition of a tree it's a set of nodes each having a link to parent node. Sometimes we call a node with an empty parent link (equal to nil) as a root (but often a root is a something virtual that doesn't exist because unless we have many such records with parent == nil and it's not a tree but forest). From ltree point of view we consider path field value as a root-to-leaf path and we call an object with path a.b.c a parent of objects with paths a.b.c.d and a.b.c.e.

But the problem is that we don't require path field to be unique. There could be two or more records with path equal to a.b.c. Which one is a parent of a.b.c.d? Yeah, one could add the UNIQUE index on that field and define parent method according to the rule described above but other one may not want this unique behaviour.

We must not use the assumption that path field is unique. This is why we'll not implement parent method in acts_as_ltree (but we'll give a recommendation in docs/tutorials how to do it). And this is why we must reimplement children and descendants methods.

So my proposition is that following things should be done:

  1. Rename #descendants method to #self_and_descendants and reimplement it so it'll be possible to pass the same set of options as to descendants_of class method. No more min_depth: 1!
  2. Implement new #descendants by calling #self_and_descendants with adding where(self.class.arel_table[self.class.primary_key].not_eq(self.id)) condition to the resulting relation. (if object.id = 123 then WHERE id != 123 will be added to the sql string)
  3. Reimplement children to be simply a call to #self_and_descendants with exact_depth: 1.
  4. Implement #strict_descendants as a shortcut of self_and_descendants min_depth: 1 to save old behaviour because it's good too for some cases.