Coder Social home page Coder Social logo

ancestry's Introduction

Gitter

Ancestry

Overview

Ancestry is a gem that allows rails ActiveRecord models to be organized as a tree structure (or hierarchy). It employs the materialized path pattern which allows operations to be performed efficiently.

Features

There are a few common ways of storing hierarchical data in a database: materialized path, closure tree table, adjacency lists, nested sets, and adjacency list with recursive queries.

Features from Materialized Path

  • Store hierarchy in an easy to understand format. (e.g.: /1/2/3/)
  • Store hierarchy in the original table with no additional tables.
  • Single SQL queries for relations (ancestors, parent, root, children, siblings, descendants)
  • Single query for creating records.
  • Moving/deleting nodes only affect child nodes (rather than updating all nodes in the tree)

Features from Ancestry gem Implementation

  • relations are implemented as scopes
  • STI support
  • Arrangement of subtrees into hashes
  • Multiple strategies for querying materialized_path
  • Multiple strategies for dealing with orphaned records
  • depth caching
  • depth constraints
  • counter caches
  • Multiple strategies for moving nodes
  • Easy migration from parent_id based gems
  • Integrity checking
  • Integrity restoration
  • Most queries use indexes on id or ancestry column. (e.g.: LIKE '#{ancestry}/%')

Since a Btree index has a limitation of 2704 characters for the ancestry column, the maximum depth of an ancestry tree is 900 items at most. If ids are 4 digits long, then the max depth is 540 items.

When using STI all classes are returned from the scopes unless you specify otherwise using where(:type => "ChildClass").

Supported Rails versions

  • Ancestry 2.x supports Rails 4.1 and earlier
  • Ancestry 3.x supports Rails 4.2 and 5.0
  • Ancestry 4.x supports Rails 5.2 through 7.0
  • Ancestry 5.0 supports Rails 6.0 and higher Rails 5.2 with update_strategy=ruby is still being tested in 5.0.

Installation

Follow these steps to apply Ancestry to any ActiveRecord model:

Add to Gemfile

# Gemfile

gem 'ancestry'
$ bundle install

Add ancestry column to your table

$ rails g migration add_[ancestry]_to_[table] ancestry:string:index
class AddAncestryToTable < ActiveRecord::Migration[6.1]
  def change
    change_table(:table) do |t|
      # postgres
      t.string "ancestry", collation: 'C', null: false
      t.index "ancestry"
      # mysql
      t.string "ancestry", collation: 'utf8mb4_bin', null: false
      t.index "ancestry"
    end
  end
end

There are additional options for the columns in Ancestry Database Column and an explanation for opclass and collation.

$ rake db:migrate

Configure ancestry defaults

# config/initializers/ancestry.rb

# use the newer format
Ancestry.default_ancestry_format = :materialized_path2
# Ancestry.default_update_strategy = :sql

Add ancestry to your model

# app/models/[model.rb]

class [Model] < ActiveRecord::Base
   has_ancestry
end

Your model is now a tree!

Organising records into a tree

You can use parent_id and parent to add a node into a tree. They can be set as attributes or passed into methods like new, create, and update.

TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')

Children can be created through the children relation on a node: node.children.create :name => 'Stinky'.

Tree Navigation

The node with the large border is the reference node (the node from which the navigation method is invoked.) The yellow nodes are those returned by the method.

parent root1 ancestors
parent root ancestors
nil for a root node self for a root node root..parent
parent_id root_id ancestor_ids
has_parent? is_root? ancestors?
parent_of? root_of? ancestor_of?
children descendants indirects
children descendants indirects
child_ids descendant_ids indirect_ids
has_children?
child_of? descendant_of? indirect_of?
siblings subtree path
siblings subtree path
includes self self..indirects root..self
sibling_ids subtree_ids path_ids
has_siblings?
sibling_of?(node)

When using STI all classes are returned from the scopes unless you specify otherwise using where(:type => "ChildClass").

1. [other root records are considered siblings]โ†ฉ

has_ancestry options

The has_ancestry method supports the following options:

:ancestry_column       Column name to store ancestry
                       'ancestry' (default)
:ancestry_format       Format for ancestry column (see Ancestry Formats section):
                       :materialized_path   1/2/3, root nodes ancestry=nil (default)
                       :materialized_path2  /1/2/3/, root nodes ancestry=/ (preferred)
:orphan_strategy       How to handle children of a destroyed node:
                       :destroy   All children are destroyed as well (default)
                       :rootify   The children of the destroyed node become root nodes
                       :restrict  An AncestryException is raised if any children exist
                       :adopt     The orphan subtree is added to the parent of the deleted node
                                  If the deleted node is Root, then rootify the orphan subtree
                       :none      skip this logic. (add your own `before_destroy`)
:cache_depth           Cache the depth of each node: (See Depth Cache section)
                       false   Do not cache depth (default)
                       true    Cache depth in 'ancestry_depth'
                       String  Cache depth in the column referenced
:primary_key_format    Regular expression that matches the format of the primary key:
                       '[0-9]+'            integer ids (default)
                       '[-A-Fa-f0-9]{36}'  UUIDs
:touch                 Touch the ancestors of a node when it changes:
                       false  don't invalide nested key-based caches (default)
                       true   touch all ancestors of previous and new parents
:counter_cache         Create counter cache column accessor:
                       false  don't store a counter cache (default)
                       true   store counter cache in `children_count`.
                       String name of column to store counter cache.
:update_strategy       How to update descendants nodes:
                       :ruby  All descendants are updated using the ruby algorithm. (default)
                              This triggers update callbacks for each descendant node
                       :sql   All descendants are updated using a single SQL statement.
                              This strategy does not trigger update callbacks for the descendants.
                              This strategy is available only for PostgreSql implementations

Legacy configuration using acts_as_tree is still available. Ancestry defers to acts_as_tree if that gem is installed.

(Named) Scopes

The navigation methods return scopes instead of records, where possible. Additional ordering, conditions, limits, etc. can be applied and the results can be retrieved, counted, or checked for existence:

node.children.where(:name => 'Mary').exists?
node.subtree.order(:name).limit(10).each { ... }
node.descendants.count

A couple of class-level named scopes are included:

roots                   Root nodes
ancestors_of(node)      Ancestors of node, node can be either a record or an id
children_of(node)       Children of node, node can be either a record or an id
descendants_of(node)    Descendants of node, node can be either a record or an id
indirects_of(node)      Indirect children of node, node can be either a record or an id
subtree_of(node)        Subtree of node, node can be either a record or an id
siblings_of(node)       Siblings of node, node can be either a record or an id

It is possible thanks to some convenient rails magic to create nodes through the children and siblings scopes:

node.children.create
node.siblings.create!
TestNode.children_of(node_id).new
TestNode.siblings_of(node_id).create

Selecting nodes by depth

With depth caching enabled (see has_ancestry options), an additional five named scopes can be used to select nodes by depth:

before_depth(depth)     Return nodes that are less deep than depth (node.depth < depth)
to_depth(depth)         Return nodes up to a certain depth (node.depth <= depth)
at_depth(depth)         Return nodes that are at depth (node.depth == depth)
from_depth(depth)       Return nodes starting from a certain depth (node.depth >= depth)
after_depth(depth)      Return nodes that are deeper than depth (node.depth > depth)

Depth scopes are also available through calls to descendants, descendant_ids, subtree, subtree_ids, path and ancestors (with relative depth). Note that depth constraints cannot be passed to ancestor_ids or path_ids as both relations can be fetched directly from the ancestry column without needing a query. Use ancestors(depth_options).map(&:id) or ancestor_ids.slice(min_depth..max_depth) instead.

node.ancestors(:from_depth => -6, :to_depth => -4)
node.path.from_depth(3).to_depth(4)
node.descendants(:from_depth => 2, :to_depth => 4)
node.subtree.from_depth(10).to_depth(12)

Arrangement

arrange

A subtree can be arranged into nested hashes for easy navigation after database retrieval.

The resulting format is a hash of hashes

{
  #<TreeNode id: 100018, name: "Stinky", ancestry: nil> => {
    #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018"> => {
      #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019"> => {}
    },
    #<TreeNode id: 100021, name: "Squishy", ancestry: "100018"> => {}
  }
}

There are many ways to call arrange:

TreeNode.find_by(:name => 'Crunchy').subtree.arrange
TreeNode.find_by(:name => 'Crunchy').subtree.arrange(:order => :name)

arrange_serializable

If a hash of arrays is preferred, arrange_serializable can be used. The results work well with to_json.

TreeNode.arrange_serializable(:order => :name)
# use an active model serializer
TreeNode.arrange_serializable { |parent, children| MySerializer.new(parent, children: children) }
TreeNode.arrange_serializable do |parent, children|
  {
     my_id: parent.id,
     my_children: children
  }
end

Sorting

The sort_by_ancestry class method: TreeNode.sort_by_ancestry(array_of_nodes) can be used to sort an array of nodes as if traversing in preorder. (Note that since materialized path trees do not support ordering within a rank, the order of siblings is dependant upon their original array order.)

Ancestry Database Column

Collation Indexes

Sorry, using collation or index operator classes makes this a little complicated. The root of the issue is that in order to use indexes, the ancestry column needs to compare strings using ascii rules.

It is well known that LIKE '/1/2/%' will use an index because the wildcard (i.e.: %) is on the right hand side of the LIKE. While that is true for ascii strings, it is not necessarily true for unicode. Since ancestry only uses ascii characters, telling the database this constraint will optimize the LIKE statements.

Collation Sorting

As of 2018, standard unicode collation ignores punctuation for sorting. This ignores the ancestry delimiter (i.e.: /) and returns data in the wrong order. The exception being Postgres on a mac, which ignores proper unicode collation and instead uses ISO-8859-1 ordering (read: ascii sorting).

Using the proper column storage and indexes will ensure that data is returned from the database in the correct order. It will also ensure that developers on Mac or Windows will get the same results as linux production servers, if that is your setup.

Migrating Collation

If you are reading this and want to alter your table to add collation to an existing column, remember to drop existing indexes on the ancestry column and recreate them.

ancestry_format materialized_path and nulls

If you are using the legacy ancestry_format of :materialized_path, then you need to the column to allow nulls. Change the column create accordingly: null: true.

Chances are, you can ignore this section as you most likely want to use :materialized_path2.

Postgres Storage Options

ascii field collation

The currently suggested way to create a postgres field is using 'C' collation:

t.string "ancestry", collation: 'C', null: false
t.index "ancestry"

ascii index

If you need to use a standard collation (e.g.: en_US), then use an ascii index:

t.string "ancestry", null: false
t.index  "ancestry", opclass: :varchar_pattern_ops

This option is mostly there for users who have an existing ancestry column and are more comfortable tweaking indexes rather than altering the ancestry column.

binary column

When the column is binary, the database doesn't convert strings using locales. Rails will convert the strings and send byte arrays to the database. At this time, this option is not suggested. The sql is not as readable, and currently this does not support the :sql update_strategy.

t.binary "ancestry", limit: 3000, null: false
t.index  "ancestry"

You may be able to alter the database to gain some readability:

ALTER DATABASE dbname SET bytea_output to 'escape';

MySQL Storage options

ascii field collation

The currently suggested way to create a MySQL field is using 'utf8mb4_bin' collation:

t.string "ancestry", collation: 'utf8mb4_bin', null: false
t.index "ancestry"

binary collation

Collation of binary acts much the same way as the binary column:

t.string "ancestry", collate: 'binary', limit: 3000, null: false
t.index  "ancestry"

binary column

t.binary "ancestry", limit: 3000, null: false
t.index  "ancestry"

ascii character set

MySQL supports per column character sets. Using a character set of ascii will set this up.

ALTER TABLE table
  ADD COLUMN ancestry VARCHAR(2700) CHARACTER SET ascii;

Ancestry Formats

You can choose from 2 ancestry formats:

  • :materialized_path - legacy format (currently the default for backwards compatibility reasons)
  • :materialized_path2 - newer format. Use this if it is a new column
:materialized_path    1/2/3,  root nodes ancestry=nil
    descendants SQL: ancestry LIKE '1/2/3/%' OR ancestry = '1/2/3'
:materialized_path2  /1/2/3/, root nodes ancestry=/
    descendants SQL: ancestry LIKE '/1/2/3/%'

If you are unsure, choose :materialized_path2. It allows a not NULL column, faster descendant queries, has one less OR statement in the queries, and the path can be formed easily in a database query for added benefits.

There is more discussion in Internals or Migrating ancestry format For migrating from materialized_path to materialized_path2 see Ancestry Column

Migrating Ancestry Format

To migrate from materialized_path to materialized_path2:

klass = YourModel
# set all child nodes
klass.where.not(klass.arel_table[klass.ancestry_column].eq(nil)).update_all("#{klass.ancestry_column} = CONCAT('#{klass.ancestry_delimiter}', #{klass.ancestry_column}, '#{klass.ancestry_delimiter}')")
# set all root nodes
klass.where(klass.arel_table[klass.ancestry_column].eq(nil)).update_all("#{klass.ancestry_column} = '#{klass.ancestry_root}'")

change_column_null klass.table_name, klass.ancestry_column, false

Migrating from plugin that uses parent_id column

It should be relatively simple to migrating from a plugin that uses a parent_id column, (e.g.: awesome_nested_set, better_nested_set, acts_as_nested_set).

When running the installation steps, also remove the old gem from your Gemfile, and remove the old gem's macros from the model.

Then populate the ancestry column from rails console:

Model.build_ancestry_from_parent_ids!
# Model.rebuild_depth_cache!
Model.check_ancestry_integrity!

It is time to run your code. Most tree methods should work fine with ancestry and hopefully your tests only require a few minor tweaks to get up and running.

Once you are happy with how your app is running, remove the old parent_id column:

$ rails g migration remove_parent_id_from_[table]
class RemoveParentIdFromToTable < ActiveRecord::Migration[6.1]
  def change
    remove_column "table", "parent_id", type: :integer
  end
end
$ rake db:migrate

Depth cache

Depth Cache Migration

To add depth_caching to an existing model:

Add column

class AddDepthCacheToTable < ActiveRecord::Migration[6.1]
  def change
    change_table(:table) do |t|
      t.integer "ancestry_depth", default: 0
    end
  end
end

Add ancestry to your model

# app/models/[model.rb]

class [Model] < ActiveRecord::Base
   has_ancestry cache_depth: true
end

Update existing values

Add a custom script or run from rails console. Some use migrations, but that can make the migration suite fragile. The command of interest is:

Model.rebuild_depth_cache!

Running Tests

git clone [email protected]:stefankroes/ancestry.git
cd ancestry
cp test/database.example.yml test/database.yml
bundle
appraisal install
# all tests
appraisal rake test
# single test version (sqlite and rails 5.0)
appraisal sqlite3-ar-50 rake test

Contributing and license

Question? Bug report? Faulty/incomplete documentation? Feature request? Please post an issue on 'http://github.com/stefankroes/ancestry/issues'. Make sure you have read the documentation and you have included tests and documentation with any pull request.

Copyright (c) 2016 Stefan Kroes, released under the MIT license

ancestry's People

Contributors

adammck avatar antstorm avatar brocktimus avatar ctrombley avatar d-m-u avatar deni64k avatar dependabot[bot] avatar dtamai avatar fryguy avatar greis avatar haslinger avatar hectormf avatar hw676018683 avatar kbrock avatar kshnurov avatar kueda avatar ledermann avatar mabusaad avatar mastfish avatar milesto avatar muitocomplicado avatar myxoh avatar petergoldstein avatar stefanh avatar stefankroes avatar stowersjoshua avatar suonlight avatar systho avatar vanderhoorn avatar xsuchy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ancestry's Issues

how to add ancestor for root?

Hj, Stefankroes,
That's a great gem, thanks for all.
*node.children.create
*node.siblings.create

how i can create :

*node.parent.create
Any idea for this session?
^^

Can i use positioned siblings list in ancestry?

Hi,

Can i use numbered list of siblings with ancestry?
Integer position field is a simple solution, but i can't get what do i use as a scope option for the list.

Could you, please, give me a clue?

has_children? performing extra query

I'm iterating through some results that have_ancestry and checking has_children? This seems to be fetching the children even in cases where the ancestry column is blank. Is there anyway to avoid this? If I show 100 instances of a model on a screen I don't want to have to run that extra query per item.

Natural Primary Key Supported?

I'm almost about to find this out myself. Legacy database with a model that has non integer primary keys. Simple strings like "AA-12345" and right now they are not saving due to:

# Validate format of ancestry column value
validates_format_of ancestry_column, :with => /\A[0-9]+(\/[0-9]+)*\Z/, :allow_nil => true

I'm gonna rewrite that to allow natural primary keys, but wanted to know if this topic has come up before?

has_many :through

Hi!
I had comment (with ancestry) and tags.
Given a comment I need to access all the tags of children.
I have

Comment Model:

has_ancestry

has_many :tags

has_many :subtags,
:through => :children,
:source => :tag,
:uniq => true

But doesn't work. Can you fix it?

ancestry column cannot be a mass-assign protected attribute

It seems to me that the ancestry column has to be included in attr_accessible. If it is not, the update_descendants_with_new_ancestry will fail, as it uses update_attributes to set the new ancestry.

Was this done on purpose? I would prefer that my users do not have direct access to that column but use special setters for reordering items, in order to more easily control what and where to they can move stuff.

Support cloning trees

I wrote this little patch to be able to clone trees. Perhaps it would be nice to include this in the gem. If you'd prefer a pull request with some tests, I'll likely be able to get to that at some point:

https://gist.github.com/799014

Just in case anyone wants help using acts_as_list with ancestry

Hi Stefan, couldn't see a wiki for this repo so I'll post it here. I ended up having to add my own methods for move to right/left/child of which took into account ancestry and acts_as_list. I'm just posting them here in case anyone else wants to use them. These were just put in the model iteself.

# Accepts the typical array of ids from a scriptaculous sortable. It is called on the instance being moved
def sort(array_of_ids)
if array_of_ids.first == id.to_s
  move_to_left_of siblings.find(array_of_ids.second)
else
  move_to_right_of siblings.find(array_of_ids[array_of_ids.index(id.to_s) - 1])
end
end

def move_to_child_of(reference_instance)
transaction do
  remove_from_list
  self.update_attributes!(:parent => reference_instance)
  add_to_list_bottom
  save!
end
end

def move_to_left_of(reference_instance)
transaction do
  remove_from_list
  reference_instance.reload # Things have possibly changed in this list
  self.update_attributes!(:parent => reference_instance.parent)
  reference_item_position = reference_instance.position
  increment_positions_on_lower_items(reference_item_position)
  self.update_attribute(:position, reference_item_position)
end
end

def move_to_right_of(reference_instance)
transaction do
  remove_from_list
  reference_instance.reload # Things have possibly changed in this list
  self.update_attributes!(:parent => reference_instance.parent)
  if reference_instance.lower_item
    lower_item_position = reference_instance.lower_item.position
    increment_positions_on_lower_items(lower_item_position)
    self.update_attribute(:position, lower_item_position)
  else
    add_to_list_bottom
    save!
  end
end   
end

Need help populating nested categories?

Does anyone know of an easy way to create dummy nested categories for testing purposes? (Maybe there is a script?) I've been playing with writing a script for the past few hours, but the cosntraints on how to establish the ancestor relationship has made my mind boggle.

Thanks.

Arrange maintaining order

Hi Stefan, Live has been so much easier since switching to ancestry! I'm enjoying the overhead reductions too! :)

Once thing I'm having trouble with is the output of .arrange. I'd expect it to respect the order defined in:

named_scope :ordered_by_ancestry, :order => "ancestry is not null, ancestry, position"

I've added this to my model as you suggested to add the position as part of the order. Doing this query:

parent.descendants(:to_depth => 2)

renders everything in the correct order (only not nested). Calling .arrange on that returns the nested hash but things are out of order. I'm not sure whether this is because ruby hashes don't haven an inherent order? What workarounds are there for this? :)

Looking forward to hearing from you,

Brendon

rails2 compatibility & testing

tests should be ran against Rails 2. The following line breaks Rails 2 because it is using 'and' instead of '&&'

self.rails_3 = defined?(ActiveRecord::VERSION) and ActiveRecord::VERSION::MAJOR >= 3

Create method on siblings or children fails if after_save or before_save callback exists

Hi there

I have run into some strange errors when trying to access the parent within a before_save or after_save callback, and have discovered that strangely it happens when using the create method on children or siblings, but not when using the build method on the same objects.

Here is some simple code to replicate this problem:

Simplified migration code

class CreatePlaces < ActiveRecord::Migration
  def self.up
    create_table :places do |t|
      t.string :name
      t.string :ancestry
    end
    add_index :places, :ancestry
  end

  def self.down
    remove_index :places, :ancestry
    remove_column :places, :ancestry
    drop_table :places
  end
end

Ruby code to replicate this issues

This code has been run with Ruby 1.8.7 and Rails 2.3.4, under the script/console

class Place < ActiveRecord::Base
  validates_presence_of :name
  acts_as_tree

  def after_save() 
     puts "#{parent.id}" if parent 
  end 
end

# Create an orphaned root level parent - works fine
parent = Place.new(:name=>"Parent"); parent.save

# Create a child node for that parent - works fine
child = Place.new(:name=>"Child",:parent=>parent); child.save

# Create a child using the children instance var of parent, using build - works fine
child_from_children = parent.children.build(:name=>"Child created from children instance method"); child_from_children.save

# Now use the dodgy create method of children and it FAILS
child_from_children = parent.children.create(:name=>"Child created from children instance method")

I have tried using before_save or after_save and this problem is exactly the same. However, if I remove the before_save or after_save callbacks, then the create method on children works.

The specific error I receive is:
ActiveRecord::RecordNotFound: Couldn't find Place with ID=17123
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:1586:in find_one' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:1569:infind_from_ids'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:616:in find' from /Library/Ruby/Gems/1.8/gems/ancestry-1.1.4/lib/ancestry/acts_as_tree.rb:329:inparent'
from (irb):5:in after_save' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/callbacks.rb:347:insend'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/callbacks.rb:347:in callback' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/callbacks.rb:251:increate_or_update'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2538:in save_without_validation' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/validations.rb:1078:insave_without_dirty'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/dirty.rb:79:in save_without_transactions' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/transactions.rb:229:insend'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/transactions.rb:229:in with_transaction_returning_status' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/connection_adapters/abstract/database_statements.rb:136:intransaction'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/transactions.rb:182:in transaction' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/transactions.rb:228:inwith_transaction_returning_status'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/transactions.rb:196:in save' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/transactions.rb:208:inrollback_active_record_state!'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/transactions.rb:196:in save' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:723:increate'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/named_scope.rb:181:in send' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/named_scope.rb:181:inmethod_missing'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2143:in with_scope' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/named_scope.rb:113:insend'
from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/named_scope.rb:113:in with_scope' from /Users/matthew/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/named_scope.rb:174:inmethod_missing'

parent/parent_id not returned with uuid as id-column

When working with uuid's the parent method does not deliver the parent object. Problem lies in instance_methods line 69:
def ancestor_ids
read_attribute(self.base_class.ancestry_column).to_s.split('/').map(&:to_i)
end

Obviously to_i is the problem. For now i have overriden the method in my class, which is not so nice

Accessing node's path in before_save or after_save

Hello,

I am using ancestry with friendly_id and attempting to write a full path of the node, with it's name seperated by slashes. I am doing this so I can do a single lookup when using Rails route globbing instead of multiple queries.

So far, path always seems to be empty. I've tried to do an AR find from irb after setting a breakpoint in the before_save method and it seems that all queries are scoped to the current records ancestry field contents.

I am not highly skilled in rails, so I might be missing something, or there might be a way to call AR without the scoping, but I am at a loss.

Hopefully you'd be able to suggest a solution. I love ancestry and it seems to fit will for this project and I'd love to move forward!

Sincere,
Nick

Trouble rendering parent.name

Hi and thanks for ancestry,

Everything is working great in the console but I am getting an undefined method when I try to render this on my index page from within a partial:

"undefined method `name' for nil:NilClass

<%= category.parent.name %>

It does render the parent_id successfully.

<%= category.parent_id %>

In the console I get this, it returns the parent category name:

category = Category.find_by_id(2)

=> <Category id: 2, name: "Male", ancestry: "1">

category.parent.name

=> "Human"

What am I doing wrong here?

Best,

Justin Malone

Callbacks on changes to tree?

I don't see any way to handle callbacks for changes to the ancestry tree, for example if I need to do something when a a node's parent is changed, or a new child is added.

I could hack around it using dirty attributes, but it doesn't seem like that works either.

children seems to be not working

hi,

ancestry is acting very weird on my machine ...

  1. cannot add children on depth > 2
  2. if using :ancestry instead of :parent adding children works fine. getting children still sucks:

my Category.rb has the following code for ancestry:
has_ancestry :orphan_strategy => :rootify, :cache_depth => true

check this gist for details: http://gist.github.com/322644

quoted_table_name when referencing table ID etc...

Hi Stefan,

I was just wondering if instead of just referring to the 'primary_key' column name in your code, whether you should be prepending that with the quoted table name? I've come across a situation where MySQL thought the reference to 'id' was ambiguous because I had a join in the query. I thought I'd ask because my join was a string rather than a rails generated join, and I'm not sure if rails will return your call to primary_key with a quoted_table_name appended if it detects a rails generated join in the query?

Cheers,

Brendon

Pagination with materialized paths

I've been exploring pagination with materialized paths. I'm adding pagination to a threaded comments system that sometimes receives thousands of comments per post. What I'm looking to do is return the page of comments in the order that would appear in the tree, top to bottom, to create a page.

Ancestry's ordered_by_ancestry scope produces an ORDER BY clause like this:

SELECT id, body, ancestry FROM comments ORDER BY (case when ancestry is null then 0 else 1 end), ancestry LIMIT 10;

With this query, you get all root comments first (in no particular order without another condition). I'm assuming this was the intended outcome for the purposes of the arrange method?

To get it sorted in tree order this is the first thing I tried:

SELECT id, body, ancestry FROM comments ORDER BY (case when ancestry is null then id else ancestry end), ancestry LIMIT 10;

This is better but you still end up with comments sorted by tier, so all second-level comments will come before all third-level comments, regardless of where they would appear in the actual tree. Still closer:

SELECT id, body, ancestry FROM comments ORDER BY (case when ancestry is null then id else CONCAT(ancestry,"/",id) end) LIMIT 10;

Adding a comment's own ID to the end of it's ancestry produces the expected result, but it's still not perfect, because sorting strings of integers behaves such that if you have 1, 2, 3, and 21, it would sort 1, 2, 21, 3. One solution to that is to pad the number with zeroes, but another is to translate the number to base 36 like Drupal and at least one Django add-on does.

I'm planning on implementing a system similar to Drupal/Django. Obviously making the ancestry string use base 36 instead of the actual IDs means some of the methods in ancestry would break and need to be modified to work, though that is fine for this use case.

I'm primarily wondering whether this is something that has been raised before. It seems like ancestry is primarily focused on traversing and assembling complete trees. Is that a correct assessment? I suspect that paginating huge trees is a fairly uncommon problem, but still I'm thinking about doing this work with an eye towards creating a gem, so of course I'd like to know if it's something that you think would actually be appropriate for inclusion in ancestry.

Missing method "i18n_key" when running tests with AR 3.0.5 or 3.0.7

% ar=3.0.5 ruby -Ilib test/has_ancestry_test.rb
% ar=3.0.7 ruby -Ilib test/has_ancestry_test.rb

Both give the 3 same errors :

  1) Error:
test_ancestry_column_validation(HasAncestryTreeTest):
NoMethodError: undefined method `i18n_key' for #<struct human="TestNode", underscore="test_node">
  2) Error:
test_integrity_checking(HasAncestryTreeTest):
NoMethodError: undefined method `i18n_key' for #<struct human="TestNode", underscore="test_node">
  3) Error:
test_integrity_restoration(HasAncestryTreeTest):
NoMethodError: undefined method `i18n_key' for #<struct human="TestNode", underscore="test_node">

I presume it's an artefact in the test suite, since it uses a Struct instead of a real ActiveRecord class.

Support MongoDB through Mongoid

As mongodb is gaining popularity among web application developers and is considered the new mysql
I think supporting MongoDB will be a welcome addition to this great gem
Actually, Mongoid, the mongo ORM lib has a very similar interface to active record , so i think it will be fairly easy to make the port
Thanks

Descendants ordered

Hi, sorry to bug you again, but I've run into a bit of a mindblank. With a nested set approach you can call self_and_decendants or descendants and get a result set that is ordered by the left column, thus ordered the way the user wants it. Is it possible to order by 'ancestry, position' and have a similar effect?

I'm trying to display the tree to the user :)

Cheers,

Brendon

arrange method does not work

whenever i call a arrange method
@process.arrange(:order => :created_at)
i got a message 'NoMethodError: undefined method `arrange' for #HrProcess:0xdb96ac'
all required fields are properly record in the table. i can call other methods like children, descendants, etc. but only arrange method has a problem.

it is created like this:

  @process = Factory.create(:hr_process, :name => 'Promotion', :business_rule => @rule)
  @a1 = HrAction.create(:name => 'WIG', :parent => @process)
  @a2 = HrAction.create(:name => 'Step 2', :parent => @a1)
  @a3 = HrAction.create(:name => 'Step 3', :parent => @a1)
  @a4 = HrAction.create(:name => 'Step 4', :parent => @a3, :link => true)
  @a5 = HrAction.create(:name => 'Step 5', :parent => @a3, :link => true)

class BusinessProcess < ActiveRecord::Base
self.table_name = 'process'
has_ancestry :cache_depth => true
end

class HrProcess < BusinessProcess

end

class HrAction < HrProcess

end

Selecting a node, its siblings, and the siblings of its ancestors (including the ancestors)

Hopefully the title says it all. I have a special type of indexbar that shows a drilled down view of a node and its surroundings. Essentially it shows the current node, its siblings surrounding it in order, and then a certain level of its ancestors from itself (e.g. parent, grandparent). It also shows those ancestors siblings surrounding it.

It doesn't show any of the other siblings descendants however and that's where I've come unstuck with trying to generate a query for this. Do you have any hints? I think I might have to resort to firstly fetching the path for a node (to a certain height), and then looping over each of those nodes and fetching their siblings into an array and somehow sorting it.

Hope you can help :D

Brendon

Positioning Tree Elements

Hi there, I was just wondering what you'd recommend for ordering of the tree elements. nested set allows you to order the items inherently but ancestry doesn't seem to allow this. Should I go back to using acts_as_list along with ancestry? (I used to use that and acts_as_tree).

I certainly like having a separate position column and tree column. I've had major corruption issues with awesome_nested_set where I think it can't handle two people updating the tree near the same time. I'm hoping ancestry has built in capability to handle this problem?

Thanks heaps for your time :)

Brendon

subtree_of named scope missing

Seems odd that this kind of scope is there for all of the other options (descendants, children, etc) but not for subtree. Imagine I get passed in an ID through the params and I'd like to fetch the subtree from the node with that ID. Now I'd have to fetch that individual node first and then call subtree on it, which adds up to 2 database queries, where something like Node.subtree_of(15) would only require 1 query.

Output of .arrange

Hi Stefan,
I'm new to rails.
Can you or anybody explain me how to "easy" output the hash created from .arrange?
I have now trying several hours and build some workarrounds - but I'm not happy.
Thanks,
Karsten

arrange not nesting the first level of child nodes

First of all, very nice work - the API and storage model very much "just make sense".

I've encountered an issue where [Model].arrange is not properly nesting the 1st level of child nodes -> they appear at the to level of the hash along with the root nodes. Lower nodes are nested as expected.

I see the same behavior with [Model]find([node_id]).subtree.arrange

I have depth caching applied (and rebuilt), and verifying/restoring integrity checking finds no issues to fix.

I'm using v1.1.0 of the gem - any thoughts?

Scope on #children affecting callbacks

Hey,

I don't know if this is anything that you can do or if it needs to be escalated to the Rails team, but I'm curious to hear your thoughts. I just switched from acts_as_tree and everything is working well except for a callback I made in one of my models called Feedback. I have a two level hierarchy (Feedback and their children, which act as the comments). There was a desire to stamp the parent with the date/time of the last child in order to be able to sort by that value, so I created the following:

  after_create :set_last_commented_at
  def set_last_commented_at
    if parent
      parent.update_attribute(:last_commented_at, Time.now)
    end
  end

The problem is that when I go to create a new child like this:

    children.create(:name => User.current.full_name, :email => User.current.email, :body => body)

The set_last_commented_at callback is in the scope of the "children" method from ancestry, so I get an error like this from the first line of the callback (where it's trying to get the parent):

Couldn't find Feedback with ID=9 [WHERE "feedback"."ancestry" = '9' AND (feedback.deleted_at IS NULL)]

The deleted_at part comes from the acts_as_paranoid gem, but you can see it's trying to get the parent by the ID 9 but it's also limiting to children of 9, which mean it gets nothing

I'm looking to implement this callback in a different way and I've already found a way around it by doing this:

  def set_last_commented_at
    self.class.unscoped do
      if parent
        parent.update_attribute(:last_commented_at, Time.now)
      end
    end
  end

But that took me a while to figure out and I'm sure it will trip up other developers in other parts of the app, so I was wondering if you had any thoughts

undefined method arrange

Using Ruby v1.8.7 on Rails v3.0.7, I'm getting a "undefined method `arrange'" when:

Model.find( :all ).arrange()

I've already tried restarting the rails server.

Any suggestions?

Mongoid support?

Hi, I just came across this gem. I want to try it out, but I'm wondering if it supports Mongoid?

Warnings when ancestry is being loaded

/Users/railsmaniac/.rvm/gems/ruby/1.8.6/gems/ancestry-1.1.0/lib/ancestry/acts_as_tree.rb:137: warning: parenthesize argument(s) for future version

There're three of them: missing parentheses. It's a minor issue, however, i don't like noisy output and i hate code throwing warnings.

Please, fix it. It's way too simple even to write patches or forks.
Thank you.

Inactivity (PLEASE READ)

Hello everyone!

I realize I've not done anything about ancestry in quite some time.

I just released a Rails 3 compatible version and fixed up the test suite.

Since all issues where several months old and none of the pull requests would apply cleanly I removed them all.

If there is anything you would still like to see done on ancestry please create new issue or create a new pull request. I will do my very best to be more actively involved with the gem and release regular updates.

Arrange returning child elements at top level and not nesting within parent

I have a rails 3.0.7 app with categories that are being nested within themselves using ancestry.

Steps to reproduce

Given I have the following categories nested within a category named "Health & Beauty"
Category "Albacore"
Category "Cream"

Calling Category.arrange returns the following nested hash

{
  #<Category id: 37, name: "Albacore", created_at: "2011-07-05 14:27:30", updated_at: "2011-07-05 14:27:30", ancestry: "25">=>{}, 
  #<Category id: 25, name: "Beauty and Hygiene", created_at: "2011-07-01 14:21:00", updated_at: "2011-07-01 14:21:00", ancestry: nil> => 
    {
      #<Category id: 38, name: "Cream", created_at: "2011-07-05 14:27:37", updated_at: "2011-07-05 14:27:37", ancestry: "25">=>{}
    }
} 

When I expect both "Cream" and "Albacore" to be returned within the "Beauty and Hygiene" ordered hash. Also important to note that this behavior only happens if the child is alphabetically before the parent as in the "Cream" category is correctly nested but "Albacore" is not.

rails3.

using ancestry with rails3, there are lots of deprecation warnings, due to Base.named_scope is getting a name change:

DEPRECATION WARNING: Base.named_scope has been deprecated, please use Base.scope instead.

Ancestry overrides acts_as_tree method even when acts_as_tree is installed

I have model (X) using acts_as_tree, I want that model to continue using acts_as_tree. After adding ancestry to my Gemfile (for use in a different model (Y)), ancestry is now being called instead of acts_as_tree in X and failing my tests thusly:

NoMethodError:
undefined method `ancestry' for X

It appears that the following code is not working as expected:

if !respond_to?(:acts_as_tree)
    alias_method :acts_as_tree, :has_ancestry
end

undefined local variable or method `has_ancestry'

I did the bundle install for the Ancestry Gem and did a new migration also for my table as specified , but then I get the above mentioned error.

I bundle installed the latest version of ancetry, did the migrations as well.

nested new forms

With the current configuration it is not possible to offer a form with a new parent and its new children, which is a very annoying issue. This due to following:

def child_ancestry
  # New records cannot have children
  raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?

I am not the only one having this problem: http://stackoverflow.com/questions/6286676/rails-ancestry-nested-form

Thanks for this otherwise great gem.

Sorting categories

Hello Stefan,
nice work on the ancestry gem.

I have a problem.

I have a model called "User" that has a

has_and_belongs_to_many :categories

association.

Also, let's say I have a category tree that looks like this:

+ Node1
++ Node11
++ Node12
++ Node13
++ Node14
+ Node2
+ Node3
+ Node4
++ Node41
+++ Node411
+++ Node412
++ Node42

Now, how do I get those nodes from the database for a particular user but in the order specified above?

I tried doing

@user.categories.all(:order => 'ancestry ASC, name ASC')

But since Node1, Node2, Node3 and Node4 all have the "ancestry" field set to nil, that doesn't work.

It seems like I'm missing something, but can't put my finger on it.

Thanks,
Tomislav

AR 3.0.5 / 3.0.7 + default_scope(order('blah')) + #arrange method are incompatible

When you have a "default_order" directive in your AR class with AR 3.0.5 or 3.0.7, #arrange method doesn't work properly. Everything runs fine with AR 3.0.0.

I wrote a little test if you want to see the issue :

  def test_arrangement_nesting
    AncestryTestDatabase.with_model :extra_columns => {:name => :string} do |model|
      model.send :default_scope, model.order('name')
      node2 = model.create! :name => 'Linux'
      node1 = model.create! :name => 'Debian'
      node1.parent = node2
      node1.save
      #expected: {Linux => Debian}
      #got: {Debian, Linux}
      assert_equal 1, model.arrange.count
    end
  end

To run it:

% ar=3.0.7 ruby -Ilib test/has_ancestry_test.rb -n test_arrangement_nesting
#=> KO
#=>
#  1) Failure:
# test_arrangement_nesting(HasAncestryTreeTest) [test/has_ancestry_test.rb:440]:
# <1> expected but was
# <2>.

% ar=3.0.0 ruby -Ilib test/has_ancestry_test.rb -n test_arrangement_nesting
#=> OK

I suppose it can be a problem in the OrderedHash class or in Arel, so maybe not directly related to AR, I'll have a deeper look at it, but any idea is welcome...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.