{"id":13462913,"url":"https://github.com/stefankroes/ancestry","last_synced_at":"2025-05-12T05:32:42.601Z","repository":{"id":694990,"uuid":"339567","full_name":"stefankroes/ancestry","owner":"stefankroes","description":"Organise ActiveRecord model into a tree structure","archived":false,"fork":false,"pushed_at":"2025-02-12T16:10:42.000Z","size":9585,"stargazers_count":3793,"open_issues_count":60,"forks_count":464,"subscribers_count":52,"default_branch":"master","last_synced_at":"2025-05-08T18:02:28.094Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stefankroes.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2009-10-16T13:37:09.000Z","updated_at":"2025-05-08T15:43:20.000Z","dependencies_parsed_at":"2023-07-05T15:03:24.877Z","dependency_job_id":"0876b7e1-3323-433d-982e-72fab9b5dd95","html_url":"https://github.com/stefankroes/ancestry","commit_stats":{"total_commits":526,"total_committers":126,"mean_commits":4.174603174603175,"dds":0.5779467680608366,"last_synced_commit":"e1c0f355a55c587b3f14b7c8c39060b85f2c8ddc"},"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefankroes%2Fancestry","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefankroes%2Fancestry/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefankroes%2Fancestry/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefankroes%2Fancestry/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stefankroes","download_url":"https://codeload.github.com/stefankroes/ancestry/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253124216,"owners_count":21857614,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T13:00:40.810Z","updated_at":"2025-05-08T18:03:13.311Z","avatar_url":"https://github.com/stefankroes.png","language":"Ruby","funding_links":[],"categories":["Active Record Plugins","Ruby","Gems","ORM/ODM Extensions","ActiveRecord"],"sub_categories":["Active Record Nesting","Attributes Management","ActiveRecord"],"readme":"[![Gitter](https://badges.gitter.im/Join+Chat.svg)](https://gitter.im/stefankroes/ancestry?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge)\n\n# Ancestry\n\n## Overview\n\nAncestry is a gem that allows rails ActiveRecord models to be organized as\na tree structure (or hierarchy). It employs the materialized path pattern\nwhich allows operations to be performed efficiently.\n\n# Features\n\nThere are a few common ways of storing hierarchical data in a database:\nmaterialized path, closure tree table, adjacency lists, nested sets, and adjacency list with recursive queries.\n\n## Features from Materialized Path\n\n- Store hierarchy in an easy to understand format. (e.g.: `/1/2/3/`)\n- Store hierarchy in the original table with no additional tables.\n- Single SQL queries for relations (`ancestors`, `parent`, `root`, `children`, `siblings`, `descendants`)\n- Single query for creating records.\n- Moving/deleting nodes only affect child nodes (rather than updating all nodes in the tree)\n\n## Features from Ancestry gem Implementation\n\n- relations are implemented as `scopes`\n- `STI` support\n- Arrangement of subtrees into hashes\n- Multiple strategies for querying materialized_path\n- Multiple strategies for dealing with orphaned records\n- depth caching\n- depth constraints\n- counter caches\n- Multiple strategies for moving nodes\n- Easy migration from `parent_id` based gems\n- Integrity checking\n- Integrity restoration\n- Most queries use indexes on `id` or `ancestry` column. (e.g.: `LIKE '#{ancestry}/%'`)\n\nSince a Btree index has a limitation of 2704 characters for the `ancestry` column,\nthe maximum depth of an ancestry tree is 900 items at most. If ids are 4 digits long,\nthen the max depth is 540 items.\n\nWhen using `STI` all classes are returned from the scopes unless you specify otherwise using `where(:type =\u003e \"ChildClass\")`.\n\n## Supported Rails versions\n\n- Ancestry 2.x supports Rails 4.1 and earlier\n- Ancestry 3.x supports Rails 4.2 and 5.0\n- Ancestry 4.x supports Rails 5.2 through 7.0\n- Ancestry 5.0 supports Rails 6.0 and higher  \n  Rails 5.2 with `update_strategy=ruby` is still being tested in 5.0.\n\n# Installation\n\nFollow these steps to apply Ancestry to any ActiveRecord model:\n\n## Add to Gemfile\n\n```ruby\n# Gemfile\n\ngem 'ancestry'\n```\n\n```bash\n$ bundle install\n```\n\n## Add ancestry column to your table\n\n```bash\n$ rails g migration add_[ancestry]_to_[table] ancestry:string:index\n```\n\n```ruby\nclass AddAncestryToTable \u003c ActiveRecord::Migration[6.1]\n  def change\n    change_table(:table) do |t|\n      # postgres\n      t.string \"ancestry\", collation: 'C', null: false\n      t.index \"ancestry\"\n      # mysql\n      t.string \"ancestry\", collation: 'utf8mb4_bin', null: false\n      t.index \"ancestry\"\n    end\n  end\nend\n```\n\nThere are additional options for the columns in [Ancestry Database Column](#ancestry-database-column) and\nan explanation for `opclass` and `collation`.\n\n```bash\n$ rake db:migrate\n```\n\n## Configure ancestry defaults\n\n```ruby\n# config/initializers/ancestry.rb\n\n# use the newer format\nAncestry.default_ancestry_format = :materialized_path2\n# Ancestry.default_update_strategy = :sql\n```\n\n## Add ancestry to your model\n\n```ruby\n# app/models/[model.rb]\n\nclass [Model] \u003c ActiveRecord::Base\n   has_ancestry\nend\n```\n\nYour model is now a tree!\n\n# Organising records into a tree\n\nYou can use `parent_id` and `parent` to add a node into a tree. They can be\nset as attributes or passed into methods like `new`, `create`, and `update`.\n\n```ruby\nTreeNode.create! :name =\u003e 'Stinky', :parent =\u003e TreeNode.create!(:name =\u003e 'Squeeky')\n```\n\nChildren can be created through the children relation on a node: `node.children.create :name =\u003e 'Stinky'`.\n\n# Tree Navigation\n\nThe node with the large border is the reference node (the node from which the navigation method is invoked.)\nThe yellow nodes are those returned by the method.\n\n|                               |                                                     |                                 |\n|:-:                            |:-:                                                  |:-:                              |\n|**parent**                     |**root**\u003csup\u003e\u003ca href=\"#fn1\" id=\"ref1\"\u003e1\u003c/a\u003e\u003c/sup\u003e    |**ancestors**                    |\n|![parent](/img/parent.png)     |![root](/img/root.png)                               |![ancestors](/img/ancestors.png) |\n| nil for a root node           |self for a root node                                 |root..parent                     |\n| `parent_id`                   |`root_id`                                            |`ancestor_ids`                   |\n| `has_parent?`                 |`is_root?`                                           |`ancestors?`                     |\n|`parent_of?`                   |`root_of?`                                           |`ancestor_of?`                   |\n|**children**                   |**descendants**                                      |**indirects**                    |\n|![children](/img/children.png) |![descendants](/img/descendants.png)                 |![indirects](/img/indirects.png) |\n| `child_ids`                   |`descendant_ids`                                     |`indirect_ids`                   |\n| `has_children?`               |                                                     |                                 |\n| `child_of?`                   |`descendant_of?`                                     |`indirect_of?`                   |\n|**siblings**                   |**subtree**                                          |**path**                         |\n|![siblings](/img/siblings.png) |![subtree](/img/subtree.png)                         |![path](/img/path.png)           |\n| includes self                 |self..indirects                                      |root..self                       |\n|`sibling_ids`                  |`subtree_ids`                                        |`path_ids`                       |\n|`has_siblings?`                |                                                     |                                 |\n|`sibling_of?(node)`            |`in_subtree_of?`                                     |                                 |\n\nWhen using `STI` all classes are returned from the scopes unless you specify otherwise using `where(:type =\u003e \"ChildClass\")`.\n\n\u003csup id=\"fn1\"\u003e1. [other root records are considered siblings]\u003ca href=\"#ref1\" title=\"Jump back to footnote 1.\"\u003e↩\u003c/a\u003e\u003c/sup\u003e\n\n# has_ancestry options\n\nThe `has_ancestry` method supports the following options:\n\n    :ancestry_column       Column name to store ancestry\n                           'ancestry' (default)\n    :ancestry_format       Format for ancestry column (see Ancestry Formats section):\n                           :materialized_path   1/2/3, root nodes ancestry=nil (default)\n                           :materialized_path2  /1/2/3/, root nodes ancestry=/ (preferred)\n    :orphan_strategy       How to handle children of a destroyed node:\n                           :destroy   All children are destroyed as well (default)\n                           :rootify   The children of the destroyed node become root nodes\n                           :restrict  An AncestryException is raised if any children exist\n                           :adopt     The orphan subtree is added to the parent of the deleted node\n                                      If the deleted node is Root, then rootify the orphan subtree\n                           :none      skip this logic. (add your own `before_destroy`)\n    :cache_depth           Cache the depth of each node: (See Depth Cache section)\n                           false   Do not cache depth (default)\n                           true    Cache depth in 'ancestry_depth'\n                           String  Cache depth in the column referenced\n    :primary_key_format    Regular expression that matches the format of the primary key:\n                           '[0-9]+'            integer ids (default)\n                           '[-A-Fa-f0-9]{36}'  UUIDs\n    :touch                 Touch the ancestors of a node when it changes:\n                           false  don't invalid nested key-based caches (default)\n                           true   touch all ancestors of previous and new parents\n    :counter_cache         Create counter cache column accessor:\n                           false  don't store a counter cache (default)\n                           true   store counter cache in `children_count`.\n                           String name of column to store counter cache.\n    :update_strategy       How to update descendants nodes:\n                           :ruby  All descendants are updated using the ruby algorithm. (default)\n                                  This triggers update callbacks for each descendant node\n                           :sql   All descendants are updated using a single SQL statement.\n                                  This strategy does not trigger update callbacks for the descendants.\n                                  This strategy is available only for PostgreSql implementations\n\nLegacy configuration using `acts_as_tree` is still available. Ancestry defers to `acts_as_tree` if that gem is installed.\n\n# (Named) Scopes\n\nThe navigation methods return scopes instead of records, where possible. Additional ordering,\nconditions, limits, etc. can be applied and the results can be retrieved, counted, or checked for existence:\n\n```ruby\nnode.children.where(:name =\u003e 'Mary').exists?\nnode.subtree.order(:name).limit(10).each { ... }\nnode.descendants.count\n```\n\nA couple of class-level named scopes are included:\n\n    roots                   Root nodes\n    ancestors_of(node)      Ancestors of node, node can be either a record or an id\n    children_of(node)       Children of node, node can be either a record or an id\n    descendants_of(node)    Descendants of node, node can be either a record or an id\n    indirects_of(node)      Indirect children of node, node can be either a record or an id\n    subtree_of(node)        Subtree of node, node can be either a record or an id\n    siblings_of(node)       Siblings of node, node can be either a record or an id\n\nIt is possible thanks to some convenient rails magic to create nodes through the children and siblings scopes:\n\n    node.children.create\n    node.siblings.create!\n    TestNode.children_of(node_id).new\n    TestNode.siblings_of(node_id).create\n\n# Selecting nodes by depth\n\nWith depth caching enabled (see [has_ancestry options](#has_ancestry-options)), an additional five named\nscopes can be used to select nodes by depth:\n\n    before_depth(depth)     Return nodes that are less deep than depth (node.depth \u003c depth)\n    to_depth(depth)         Return nodes up to a certain depth (node.depth \u003c= depth)\n    at_depth(depth)         Return nodes that are at depth (node.depth == depth)\n    from_depth(depth)       Return nodes starting from a certain depth (node.depth \u003e= depth)\n    after_depth(depth)      Return nodes that are deeper than depth (node.depth \u003e depth)\n\nDepth scopes are also available through calls to `descendants`,\n`descendant_ids`, `subtree`, `subtree_ids`, `path` and `ancestors` (with relative depth).\nNote that depth constraints cannot be passed to `ancestor_ids` or `path_ids` as both relations\ncan be fetched directly from the ancestry column without needing a query. Use\n`ancestors(depth_options).map(\u0026:id)` or `ancestor_ids.slice(min_depth..max_depth)` instead.\n\n    node.ancestors(:from_depth =\u003e -6, :to_depth =\u003e -4)\n    node.path.from_depth(3).to_depth(4)\n    node.descendants(:from_depth =\u003e 2, :to_depth =\u003e 4)\n    node.subtree.from_depth(10).to_depth(12)\n\n# Arrangement\n\n## `arrange`\n\nA subtree can be arranged into nested hashes for easy navigation after database retrieval.\n\nThe resulting format is a hash of hashes\n\n```ruby\n{\n  #\u003cTreeNode id: 100018, name: \"Stinky\", ancestry: nil\u003e =\u003e {\n    #\u003cTreeNode id: 100019, name: \"Crunchy\", ancestry: \"100018\"\u003e =\u003e {\n      #\u003cTreeNode id: 100020, name: \"Squeeky\", ancestry: \"100018/100019\"\u003e =\u003e {}\n    },\n    #\u003cTreeNode id: 100021, name: \"Squishy\", ancestry: \"100018\"\u003e =\u003e {}\n  }\n}\n```\n\nThere are many ways to call `arrange`:\n\n```ruby\nTreeNode.find_by(:name =\u003e 'Crunchy').subtree.arrange\nTreeNode.find_by(:name =\u003e 'Crunchy').subtree.arrange(:order =\u003e :name)\n```\n\n## `arrange_serializable`\n\nIf a hash of arrays is preferred, `arrange_serializable` can be used. The results\nwork well with `to_json`.\n\n```ruby\nTreeNode.arrange_serializable(:order =\u003e :name)\n# use an active model serializer\nTreeNode.arrange_serializable { |parent, children| MySerializer.new(parent, children: children) }\nTreeNode.arrange_serializable do |parent, children|\n  {\n     my_id: parent.id,\n     my_children: children\n  }\nend\n```\n\n# Sorting\n\nThe `sort_by_ancestry` class method: `TreeNode.sort_by_ancestry(array_of_nodes)` can be used\nto sort an array of nodes as if traversing in preorder. (Note that since materialized path\ntrees do not support ordering within a rank, the order of siblings is\ndependant upon their original array order.)\n\n\n# Ancestry Database Column\n\n## Collation Indexes\n\nSorry, using collation or index operator classes makes this a little complicated. The\nroot of the issue is that in order to use indexes, the ancestry column needs to\ncompare strings using ascii rules.\n\nIt is well known that `LIKE '/1/2/%'` will use an index because the wildcard (i.e.: `%`)\nis on the right hand side of the `LIKE`. While that is true for ascii strings, it is not\nnecessarily true for unicode. Since ancestry only uses ascii characters, telling the database\nthis constraint will optimize the `LIKE` statements.\n\n## Collation Sorting\n\nAs of 2018, standard unicode collation ignores punctuation for sorting. This ignores\nthe ancestry delimiter (i.e.: `/`) and returns data in the wrong order. The exception\nbeing Postgres on a mac, which ignores proper unicode collation and instead uses\nISO-8859-1 ordering (read: ascii sorting).\n\nUsing the proper column storage and indexes will ensure that data is returned from the\ndatabase in the correct order. It will also ensure that developers on Mac or Windows will\nget the same results as linux production servers, if that is your setup.\n\n## Migrating Collation\n\nIf you are reading this and want to alter your table to add collation to an existing column,\nremember to drop existing indexes on the `ancestry` column and recreate them.\n\n## ancestry_format materialized_path and nulls\n\nIf you are using the legacy `ancestry_format` of `:materialized_path`, then you need to the\ncolumn to allow `nulls`. Change the column create accordingly: `null: true`.\n\nChances are, you can ignore this section as you most likely want to use `:materialized_path2`.\n\n## Postgres Storage Options\n\n### ascii field collation\n\nThe currently suggested way to create a postgres field is using `'C'` collation:\n\n```ruby\nt.string \"ancestry\", collation: 'C', null: false\nt.index \"ancestry\"\n```\n\n### ascii index\n\nIf you need to use a standard collation (e.g.: `en_US`), then use an ascii index:\n\n```ruby\nt.string \"ancestry\", null: false\nt.index  \"ancestry\", opclass: :varchar_pattern_ops\n```\n\nThis option is mostly there for users who have an existing ancestry column and are more\ncomfortable tweaking indexes rather than altering the ancestry column.\n\n### binary column\n\nWhen the column is binary, the database doesn't convert strings using locales.\nRails will convert the strings and send byte arrays to the database.\nAt this time, this option is not suggested. The sql is not as readable, and currently\nthis does not support the `:sql` update_strategy.\n\n```ruby\nt.binary \"ancestry\", limit: 3000, null: false\nt.index  \"ancestry\"\n```\nYou may be able to alter the database to gain some readability:\n\n```SQL\nALTER DATABASE dbname SET bytea_output to 'escape';\n```\n\n## MySQL Storage options\n\n### ascii field collation\n\nThe currently suggested way to create a MySQL field is using `'utf8mb4_bin'` collation:\n\n```ruby\nt.string \"ancestry\", collation: 'utf8mb4_bin', null: false\nt.index \"ancestry\"\n```\n\n### binary collation\n\nCollation of `binary` acts much the same way as the `binary` column:\n\n```ruby\nt.string \"ancestry\", collate: 'binary', limit: 3000, null: false\nt.index  \"ancestry\"\n```\n\n### binary column\n\n```ruby\nt.binary \"ancestry\", limit: 3000, null: false\nt.index  \"ancestry\"\n```\n\n### ascii character set\n\nMySQL supports per column character sets. Using a character set of `ascii` will\nset this up.\n\n```SQL\nALTER TABLE table\n  ADD COLUMN ancestry VARCHAR(2700) CHARACTER SET ascii;\n```\n\n# Ancestry Formats\n\nYou can choose from 2 ancestry formats:\n\n- `:materialized_path` - legacy format (currently the default for backwards compatibility reasons)\n- `:materialized_path2` - newer format. Use this if it is a new column\n\n```\n:materialized_path    1/2/3,  root nodes ancestry=nil\n    descendants SQL: ancestry LIKE '1/2/3/%' OR ancestry = '1/2/3'\n:materialized_path2  /1/2/3/, root nodes ancestry=/\n    descendants SQL: ancestry LIKE '/1/2/3/%'\n```\n\nIf you are unsure, choose `:materialized_path2`. It allows a not NULL column,\nfaster descendant queries, has one less `OR` statement in the queries, and\nthe path can be formed easily in a database query for added benefits.\n\nThere is more discussion in [Internals](#internals) or [Migrating ancestry format](#migrate-ancestry-format)\nFor migrating from `materialized_path` to `materialized_path2` see [Ancestry Column](#ancestry-column)\n\n## Migrating Ancestry Format\n\nTo migrate from `materialized_path` to `materialized_path2`:\n\n```ruby\nklass = YourModel\n# set all child nodes\nklass.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}')\")\n# set all root nodes\nklass.where(klass.arel_table[klass.ancestry_column].eq(nil)).update_all(\"#{klass.ancestry_column} = '#{klass.ancestry_root}'\")\n\nchange_column_null klass.table_name, klass.ancestry_column, false\n```\n\n# Migrating from plugin that uses parent_id column\n\nIt should be relatively simple to migrating from a plugin that uses a `parent_id`\ncolumn, (e.g.: `awesome_nested_set`, `better_nested_set`, `acts_as_nested_set`).\n\nWhen running the installation steps, also remove the old gem from your `Gemfile`,\nand remove the old gem's macros from the model.\n\nThen populate the `ancestry` column from rails console:\n\n```ruby\nModel.build_ancestry_from_parent_ids!\n# Model.rebuild_depth_cache!\nModel.check_ancestry_integrity!\n```\n\nIt is time to run your code. Most tree methods should work fine with ancestry\nand hopefully your tests only require a few minor tweaks to get up and running.\n\nOnce you are happy with how your app is running, remove the old `parent_id` column:\n\n```bash\n$ rails g migration remove_parent_id_from_[table]\n```\n\n```ruby\nclass RemoveParentIdFromToTable \u003c ActiveRecord::Migration[6.1]\n  def change\n    remove_column \"table\", \"parent_id\", type: :integer\n  end\nend\n```\n\n```bash\n$ rake db:migrate\n```\n\n# Depth cache\n\n## Depth Cache Migration\n\nTo add depth_caching to an existing model:\n\n## Add column\n\n```ruby\nclass AddDepthCacheToTable \u003c ActiveRecord::Migration[6.1]\n  def change\n    change_table(:table) do |t|\n      t.integer \"ancestry_depth\", default: 0\n    end\n  end\nend\n```\n\n## Add ancestry to your model\n\n```ruby\n# app/models/[model.rb]\n\nclass [Model] \u003c ActiveRecord::Base\n   has_ancestry cache_depth: true\nend\n```\n\n## Update existing values\n\nAdd a custom script or run from rails console.\nSome use migrations, but that can make the migration suite fragile. The command of interest is:\n\n```ruby\nModel.rebuild_depth_cache!\n```\n\n# Running Tests\n\n```bash\ngit clone git@github.com:stefankroes/ancestry.git\ncd ancestry\ncp test/database.example.yml test/database.yml\nbundle\nappraisal install\n# all tests\nappraisal rake test\n# single test version (sqlite and rails 5.0)\nappraisal sqlite3-ar-50 rake test\n```\n\n# Contributing and license\n\nQuestion? Bug report? Faulty/incomplete documentation? Feature request? Please\npost an issue on 'http://github.com/stefankroes/ancestry/issues'. Make sure\nyou have read the documentation and you have included tests and documentation\nwith any pull request.\n\nCopyright (c) 2016 Stefan Kroes, released under the MIT license\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstefankroes%2Fancestry","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstefankroes%2Fancestry","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstefankroes%2Fancestry/lists"}