ancestry

repository·master·Indexed 26 days ago

https://github.com/stefankroes/ancestry

A Ruby on Rails gem that implements the materialized path pattern to manage hierarchical tree structures within ActiveRecord models. It enables efficient single-query reads of ancestors, descendants, and siblings without requiring extra tables. Features include support for multiple ancestry formats (materialized_path, ltree), depth caching, subtree arrangement into hashes or serializable formats, and tools for migrating from parent_id based plugins.

Tokens
7.9K
Snippets
23
Records
59
Agent score
86%

What's inside ancestry

  1. Add ancestry column to a database table

    master

    Generate a migration to add the ancestry column to your table. Use the t.ancestry helper in your migration to automatically create the column with the correct type, collation, and indexes. You can pass options to this helper for cached columns and ancestry formats.

    class AddAncestryToTable < ActiveRecord::Migration[7.0]
      def change
        change_table(:table) do |t|
          t.ancestry
          # t.ancestry format: :materialized_path3, cache_depth: true, parent: true, counter_cache: true
        end
      end
    end
  2. Run the Ancestry test suite

    master

    To run the tests locally, clone the repository, set up the database configuration, and use appraisal to run tests across different Rails versions.

    git clone git@github.com: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
  3. Rebuild Cached Columns

    master

    If you perform bulk imports or direct SQL updates that bypass ActiveRecord callbacks, you must manually rebuild your cached columns to ensure data integrity.

    Model.rebuild_depth_cache!          # depth cache
    Model.rebuild_parent_id_cache!      # parent_id cache
    Model.rebuild_root_id_cache!       # root_id cache
    Model.rebuild_counter_cache!        # counter cache
    
    # Faster SQL alternatives:
    Model.rebuild_depth_cache_sql!
    Model.rebuild_parent_id_cache_sql!
    Model.rebuild_root_id_cache_sql!
  4. Migrate from a parent_id based plugin to Ancestry

    master

    If you are migrating from a plugin that uses a parent_id column (such as awesome_nested_set, better_nested_set, or acts_as_nested_set), follow these steps:

    1. Cleanup: Remove the old gem from your Gemfile and remove its macros from your model.
    2. Data Migration: Populate the ancestry column using the provided model methods via the Rails console.
    3. Verification: Run your application and tests to ensure tree methods work as expected.
    4. Database Cleanup: Once verified, remove the old parent_id column from your database using a Rails migration.

    To populate the ancestry data, use:

    Model.build_ancestry_from_parent_ids!
    # Model.rebuild_depth_cache! # Uncomment if using depth cache
    Model.check_ancestry_integrity!
    Model.build_ancestry_from_parent_ids!
    # Model.rebuild_depth_cache!
    Model.check_ancestry_integrity!
  5. Remove parent_id column after Ancestry migration

    master

    After successfully migrating your data to the ancestry column and verifying your application, you can remove the legacy parent_id column using a Rails migration.

    Generate the migration:

    $ rails g migration remove_parent_id_from_[table]

    Edit the migration file to remove the column:

    class RemoveParentIdFromToTable < ActiveRecord::Migration[6.1]
      def change
        remove_column "table", "parent_id", type: :integer
      end
    end

    Run the migration:

    $ rake db:migrate
    $ rails g migration remove_parent_id_from_[table]
    $ rake db:migrate
  6. Migrate from :materialized_path to :materialized_path3

    master

    To upgrade from the legacy :materialized_path (where root is nil) to the recommended :materialized_path3 (where root is an empty string and paths have trailing delimiters), run the following updates:

    1. Append the delimiter to existing paths.
    2. Convert nil root nodes to empty strings.
    3. Change the column to NOT NULL.
    klass = YourModel
    # Append delimiter: "1/2/3" → "1/2/3/"
    klass.where.not(ancestry: nil).update_all("ancestry = CONCAT(ancestry, '/')")
    # Convert root nodes: nil → ""
    klass.where(ancestry: nil).update_all("ancestry = ''")
    change_column_null klass.table_name, :ancestry, false
  7. Migrate from :materialized_path2 to :materialized_path3

    master

    To migrate from :materialized_path2 (root is /) to :materialized_path3 (root is ""), strip the leading delimiter and convert the root node.

    klass = YourModel
    # Strip leading delimiter: "/1/2/3/" → "1/2/3/"
    klass.where.not(ancestry: '/').update_all("ancestry = SUBSTRING(ancestry, 2)")
    # Convert root nodes: "/" → ""
    klass.where(ancestry: '/').update_all("ancestry = ''")
  8. Understand the MaterializedPath3 format

    master

    The MaterializedPath3 format stores ancestry as a string of IDs followed by a trailing delimiter (e.g., 1/2/3/).

    Key characteristics:

    • Root nodes: Represented by an empty string ("").
    • Trailing delimiter: Every path ends with a /. This ensures that split('/') returns clean ID segments without empty trailing elements.
    • Depth: The depth of a node is equivalent to the number of delimiters in the string.
  9. Use Ancestry::Ltree for PostgreSQL ltree support

    master

    The Ancestry::Ltree class provides a materialized path implementation specifically designed for PostgreSQL's ltree extension. It stores ancestry as a dot-separated path of IDs (e.g., 1.2.3).

    Key characteristics:

    • Root representation: The root node is represented by an empty string ("").
    • Delimiter: Uses a dot (.) to separate IDs.
    • Storage format: Ancestry is stored as grandparent_id.parent_id.
  10. Configure Parent and Root Caching

    master

    Caching parent_id and root_id enables ActiveRecord associations like belongs_to :parent, has_many :children, and belongs_to :root.

    Important Notes:

    • Virtual Columns: Requires Rails 7.2+ for SQLite and Rails 7.0+ for PostgreSQL/MySQL.
    • MySQL Limitation: root: :virtual is NOT supported on MySQL because generated columns cannot reference auto-increment columns. Use root: true on MySQL instead.
    • Root Callback: root: true requires an extra UPDATE after creating root nodes because the root_id is only available after INSERT.
    # Parent Cache
    has_ancestry parent: :virtual    # recommended — database-generated parent_id
    has_ancestry parent: true        # callback-maintained parent_id
    
    # Root Cache
    has_ancestry root: :virtual     # database-generated root_id (PostgreSQL, SQLite)
    has_ancestry root: true         # callback-maintained root_id
  11. Configure Primary Key Format

    master

    Specify the format of your primary keys. Note that the ancestry column itself is always a string (e.g., "uuid1/uuid2"), even if your primary keys are UUIDs. :ltree and :array formats are NOT compatible with UUID primary keys.

    has_ancestry primary_key_format: :integer   # default
    has_ancestry primary_key_format: :uuid      # UUIDs
    has_ancestry primary_key_format: :string    # alphanumeric string ids
    
    # Custom regex support:
    has_ancestry primary_key_format: '[a-z0-9]{8}'