├── .github └── workflows │ ├── ci.yml │ ├── ci_jruby.yml │ ├── ci_truffleruby.yml │ └── release.yml ├── .gitignore ├── .release-please-manifest.json ├── .tool-versions ├── .yardopts ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin ├── appraisal ├── rake └── rspec ├── closure_tree.gemspec ├── gemfiles ├── activerecord_7.1.gemfile ├── activerecord_7.2.gemfile ├── activerecord_8.0.gemfile └── activerecord_edge.gemfile ├── img ├── example.png └── preorder.png ├── lib ├── closure_tree.rb ├── closure_tree │ ├── active_record_support.rb │ ├── configuration.rb │ ├── deterministic_ordering.rb │ ├── digraphs.rb │ ├── finders.rb │ ├── has_closure_tree.rb │ ├── has_closure_tree_root.rb │ ├── hash_tree.rb │ ├── hash_tree_support.rb │ ├── hierarchy_maintenance.rb │ ├── model.rb │ ├── numeric_deterministic_ordering.rb │ ├── numeric_order_support.rb │ ├── support.rb │ ├── support_attributes.rb │ ├── support_flags.rb │ ├── test │ │ └── matcher.rb │ └── version.rb └── generators │ └── closure_tree │ ├── config_generator.rb │ ├── migration_generator.rb │ └── templates │ ├── config.rb │ └── create_hierarchies_table.rb.erb ├── mktree.rb ├── release-please-config.json └── test ├── closure_tree ├── cache_invalidation_test.rb ├── cuisine_type_test.rb ├── generator_test.rb ├── has_closure_tree_root_test.rb ├── hierarchy_maintenance_test.rb ├── label_test.rb ├── matcher_test.rb ├── metal_test.rb ├── model_test.rb ├── namespace_type_test.rb ├── parallel_test.rb ├── pool_test.rb ├── support_test.rb ├── tag_test.rb ├── user_test.rb └── uuid_tag_test.rb ├── support ├── models.rb ├── query_counter.rb ├── schema.rb └── tag_examples.rb └── test_helper.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | services: 17 | mysql: 18 | image: mysql/mysql-server 19 | ports: 20 | - "3306:3306" 21 | env: 22 | MYSQL_ROOT_PASSWORD: root 23 | MYSQL_DATABASE: closure_tree_test 24 | MYSQL_ROOT_HOST: '%' 25 | postgres: 26 | image: 'postgres' 27 | ports: ['5432:5432'] 28 | env: 29 | POSTGRES_PASSWORD: postgres 30 | POSTGRES_DB: closure_tree_test 31 | options: >- 32 | --health-cmd pg_isready 33 | --health-interval 10s 34 | --health-timeout 5s 35 | --health-retries 5 36 | 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | ruby: 41 | - '3.3' 42 | rails: 43 | - activerecord_8.0 44 | - activerecord_7.2 45 | - activerecord_7.1 46 | - activerecord_edge 47 | adapter: 48 | - 'sqlite3:///:memory:' 49 | - mysql2://root:root@0/closure_tree_test 50 | - postgres://closure_tree:closure_tree@0/closure_tree_test 51 | 52 | steps: 53 | - name: Checkout 54 | uses: actions/checkout@v3 55 | 56 | - name: Setup Ruby 57 | uses: ruby/setup-ruby@v1 58 | with: 59 | ruby-version: ${{ matrix.ruby }} 60 | bundler-cache: true 61 | rubygems: latest 62 | env: 63 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile 64 | RAILS_ENV: test 65 | 66 | - name: Test 67 | env: 68 | RAILS_ENV: test 69 | RAILS_VERSION: ${{ matrix.rails }} 70 | DB_ADAPTER: ${{ matrix.adapter }} 71 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile 72 | WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} 73 | run: bin/rake 74 | -------------------------------------------------------------------------------- /.github/workflows/ci_jruby.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI Jruby 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | concurrency: 12 | group: ci-${{ github.head_ref }}-jruby 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | services: 19 | mysql: 20 | image: mysql/mysql-server 21 | ports: 22 | - "3306:3306" 23 | env: 24 | MYSQL_ROOT_PASSWORD: root 25 | MYSQL_DATABASE: closure_tree_test 26 | MYSQL_ROOT_HOST: '%' 27 | postgres: 28 | image: 'postgres' 29 | ports: ['5432:5432'] 30 | env: 31 | POSTGRES_PASSWORD: postgres 32 | POSTGRES_DB: closure_tree_test 33 | options: >- 34 | --health-cmd pg_isready 35 | --health-interval 10s 36 | --health-timeout 5s 37 | --health-retries 5 38 | 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | rails: 43 | - activerecord_7.1 44 | adapter: 45 | - 'sqlite3:///:memory:' 46 | - mysql2://root:root@0/closure_tree_test 47 | - postgres://closure_tree:closure_tree@0/closure_tree_test 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v3 51 | 52 | - name: Setup Ruby 53 | uses: ruby/setup-ruby@v1 54 | with: 55 | ruby-version: jruby 56 | bundler-cache: true 57 | rubygems: latest 58 | env: 59 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile 60 | RAILS_ENV: test 61 | 62 | - name: Test 63 | env: 64 | RAILS_ENV: test 65 | RAILS_VERSION: ${{ matrix.rails }} 66 | DB_ADAPTER: ${{ matrix.adapter }} 67 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile 68 | WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} 69 | run: bin/rake 70 | -------------------------------------------------------------------------------- /.github/workflows/ci_truffleruby.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI Truffleruby 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | concurrency: 12 | group: ci-${{ github.head_ref }}-truffleruby 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | services: 19 | mysql: 20 | image: mysql/mysql-server 21 | ports: 22 | - "3306:3306" 23 | env: 24 | MYSQL_ROOT_PASSWORD: root 25 | MYSQL_DATABASE: closure_tree_test 26 | MYSQL_ROOT_HOST: '%' 27 | postgres: 28 | image: 'postgres' 29 | ports: ['5432:5432'] 30 | env: 31 | POSTGRES_PASSWORD: postgres 32 | POSTGRES_DB: closure_tree_test 33 | options: >- 34 | --health-cmd pg_isready 35 | --health-interval 10s 36 | --health-timeout 5s 37 | --health-retries 5 38 | 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | ruby: 43 | - truffleruby 44 | rails: 45 | - activerecord_7.1 46 | adapter: 47 | - 'sqlite3:///:memory:' 48 | - mysql2://root:root@0/closure_tree_test 49 | - postgres://closure_tree:closure_tree@0/closure_tree_test 50 | 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v3 54 | 55 | - name: Setup Ruby 56 | uses: ruby/setup-ruby@v1 57 | with: 58 | ruby-version: truffleruby 59 | bundler-cache: true 60 | rubygems: latest 61 | env: 62 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile 63 | RAILS_ENV: test 64 | 65 | - name: Test 66 | env: 67 | RAILS_ENV: test 68 | RAILS_VERSION: ${{ matrix.rails }} 69 | DB_ADAPTER: ${{ matrix.adapter }} 70 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile 71 | WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} 72 | run: bin/rake 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | jobs: 14 | release-please: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: googleapis/release-please-action@v4 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | .history/ 3 | .idea/ 4 | pkg/ 5 | rdoc/ 6 | doc/ 7 | *.sqlite3.db 8 | *.log 9 | tmp/ 10 | .DS_Store 11 | .yardoc/ 12 | .rvmrc 13 | *.lock 14 | .ruby-* 15 | *.iml 16 | .env 17 | coverage/ 18 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | {".":"8.0.0"} 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.4.4 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --readme README.md 2 | --charset utf-8 3 | 'lib/**/*.rb' 4 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise 'activerecord-7.1' do 4 | gem 'activerecord', '~> 7.1.0' 5 | gem 'railties' 6 | 7 | platforms :ruby, :truffleruby do 8 | gem 'mysql2' 9 | gem 'pg' 10 | gem 'sqlite3', '< 2.0' 11 | end 12 | 13 | platforms :jruby do 14 | gem 'activerecord-jdbcmysql-adapter' 15 | gem 'activerecord-jdbcpostgresql-adapter' 16 | gem 'activerecord-jdbcsqlite3-adapter' 17 | end 18 | end 19 | 20 | appraise 'activerecord-7.2' do 21 | gem 'activerecord', '~> 7.2.0' 22 | gem 'railties' 23 | 24 | platforms :ruby do 25 | gem 'mysql2' 26 | gem 'pg' 27 | gem 'sqlite3' 28 | end 29 | end 30 | 31 | appraise 'activerecord-8.0' do 32 | gem 'activerecord', '~> 8.0.0' 33 | gem 'railties' 34 | 35 | platforms :ruby do 36 | gem 'mysql2' 37 | gem 'pg' 38 | gem 'sqlite3' 39 | end 40 | end 41 | 42 | appraise 'activerecord-edge' do 43 | gem 'activerecord', github: 'rails/rails' 44 | gem 'railties', github: 'rails/rails' 45 | 46 | platforms :ruby do 47 | gem 'mysql2' 48 | gem 'pg' 49 | gem 'sqlite3' 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 8.0.0 4 | 5 | - Drop support to EOL ruby and rails 6 | - Reference ancestor_hierarchies in depth instead of ancestors to avoid n+1 7 | 8 | ## [7.4.0](https://github.com/ClosureTree/closure_tree/tree/7.4.0) 9 | 10 | [Full Changelog](https://github.com/ClosureTree/closure_tree/compare/v7.3.0...7.4.0) 11 | 12 | - fix: hierarchy model with namespace should inherit from the superclass of basic model [\#384](https://github.com/ClosureTree/closure_tree/pull/384) ([shawndodo](https://github.com/shawndodo)) 13 | - Add with\_descendant to readme [\#381](https://github.com/ClosureTree/closure_tree/pull/381) ([mattvague](https://github.com/mattvague)) 14 | 15 | ### 7.3.0 16 | - Ruby 3.0 support 17 | 18 | ### 7.2.0 19 | - Ruby 2.7 support 20 | - Ordering raw SQL argument wrapped with Arel.sql 21 | 22 | ### 7.1.0 23 | Closure Tree is now tested against Rails 6.0 24 | - Directly require core_ext for String#strip_heredoc[PR 350](https://github.com/ClosureTree/closure_tree/pull/350) 25 | - Call Module#module_parent instead of deprecated #parent[PR 354](https://github.com/ClosureTree/closure_tree/pull/354) 26 | 27 | ### 7.0.0 28 | Closure Tree is now tested against Rails 5.2 29 | 30 | - Postpone configuration (database introspection)[PR 264](https://github.com/ClosureTree/closure_tree/pull/264) 31 | - Fix "tree.find_by_path([])" [PR 288](https://github.com/ClosureTree/closure_tree/pull/288) 32 | - Fixed generator specs and added migration version [PR 292](https://github.com/ClosureTree/closure_tree/pull/292) 33 | - Eliminate deprecation warnings in ActiveRecord 5.2 [PR 296](https://github.com/ClosureTree/closure_tree/pull/296) 34 | - When using 'oracle_enhanced', remove 'AS' on the table_name alias. [PR 298](https://github.com/ClosureTree/closure_tree/pull/298) 35 | - README update [PR 301](https://github.com/ClosureTree/closure_tree/pull/301) 36 | - Add `with_descendant` finder [PR 302](https://github.com/ClosureTree/closure_tree/pull/302) 37 | - Fix pg version for rails prior 5.1 [PR 303](https://github.com/ClosureTree/closure_tree/pull/303) 38 | - Test on Rails 5.2 & fix mysql for older Rails [PR 304](https://github.com/ClosureTree/closure_tree/pull/304) 39 | - Test with ActiveRecord 5.2.0 [PR 307](https://github.com/ClosureTree/closure_tree/pull/307) 40 | - README update [PR 310](https://github.com/ClosureTree/closure_tree/pull/310) 41 | - FactoryBot linter failing for a model that uses closure_tree [PR 311](https://github.com/ClosureTree/closure_tree/pull/311) 42 | - Added dont_order_roots option [PR 312](https://github.com/ClosureTree/closure_tree/pull/312) 43 | - Added instance methods to determine the relationship between 2 nodes [PR 314](https://github.com/ClosureTree/closure_tree/pull/314) 44 | - Add an instance method to check the relationship between 2 nodes: #family_of? [PR 319](https://github.com/ClosureTree/closure_tree/pull/319) 45 | - Remove options restrictions on has_closure_tree_root [PR 321](https://github.com/ClosureTree/closure_tree/pull/321) 46 | - Fix uninitialized variable warnings [PR 323](https://github.com/ClosureTree/closure_tree/pull/323) 47 | 48 | ### 6.6.0 49 | 50 | Closure Tree is now tested against Rails 5.1, and just passed 50 contributors and 51 | 1000 stars on github! 🎉! 52 | 53 | Note that Closure Tree has moved to a new "Closure Tree" github organization. 54 | **Future gem releases will be done by other contributors.** 55 | 56 | * ActiveSupport 5.1 deprecation warnings were addressed in [PR 57 | 262](https://github.com/ClosureTree/closure_tree/pull/262). Thanks, [Charles 58 | Pence](https://github.com/cpence)! 59 | * ActiveSupport 5.1 test failures were fixed in 60 | [PR 280](https://github.com/ClosureTree/closure_tree/pull/280). 61 | Thanks, [Bobby Uhlenbrock](https://github.com/uhlenbrock)! 62 | * A database connection was leaked at startup, fixed in [PR 63 | 263](https://github.com/ClosureTree/closure_tree/pull/263). Thanks, [Andrey 64 | Novikov](https://github.com/Envek)! 65 | 66 | ### 6.5.0 67 | 68 | * Merged [PR 258](https://github.com/ClosureTree/closure_tree/pull/258) which 69 | improves hierarchy maintenance with large trees. Thanks, [Nikolay 70 | Bekirov](https://github.com/nbekirov)! 71 | ### 6.4.0 72 | 73 | * Merged [PR 236](https://github.com/ClosureTree/closure_tree/pull/236) which adds documentation for `has_closure_tree_root`. 74 | * Added ruby 2.4 and dropped Rails 4.1 from the build matrix. 75 | 76 | ### 6.3.0 77 | 78 | * `prepend_child` [handles invalid children properly now](https://github.com/ClosureTree/closure_tree/issues/249). 79 | Thanks [Amit Saxena](https://github.com/amitsaxena)! 80 | * Dropped official support for jruby and ruby 2.0 (no code was changed) 81 | 82 | ### 6.2.0 83 | 84 | * Fix for [MySQL lock lengths](https://github.com/ClosureTree/closure_tree/issues/231). 85 | Thanks to [Liam](https://github.com/hut8)! 86 | * [Tom Smyth](https://github.com/hooverlunch) added [eager tree loading](https://github.com/ClosureTree/closure_tree/pull/232) 87 | * Merged [PR 200](https://github.com/ClosureTree/closure_tree/pull/200) which may or may not add support to SQLServer 2008 (but this is not a supported RDBMS). 88 | 89 | ### 6.1.0 90 | 91 | * Added official support for ActiveRecord 5.0! Thanks to [Abdelkader Boudih](https://github.com/seuros), 92 | [Jay Fredlund](https://github.com/jayfredlund), Veselin Stoyanov, and 93 | [Aaron Russell](https://github.com/aaronrussell) for all the PRs. 94 | * Add `database_less` configuration to not raise an error during build step when 95 | database is unavailable which is a common case in some PaaS like 96 | (Heroku, Catalyze, ..., etc). 97 | 98 | ### 6.0.0 99 | 100 | * [Andrew Kumanyaev](https://github.com/zzet) *dramatically* improved mutation performance on large trees. 101 | Thanks for the PR! 102 | * [Martin Schmidt](https://github.com/martin-schmidt) discovered and fixed build problems due to new versions 103 | of mysql2 and ammeter which broke Travis builds. Thanks for the PR! 104 | * [Fabien MICHEL](https://github.com/fabien-michel) updated the README with another example. Thanks for the PR! 105 | 106 | ### 6.0.0.alpha,beta,gamma 107 | 108 | * Dropped support for versions of Rails 3.2 and 4.0 (which are no longer supported). 109 | * Dropped support for Ruby 1.9 and JRuby 1.9 (which are no longer supported). 110 | * Added support for `.hash_tree` from `.parent` and `.children`. 111 | Addresses [PR146](https://github.com/ClosureTree/closure_tree/pull/146). 112 | Thanks for reporting this and the breaking test, [Mike](https://github.com/mkralla11)! 113 | 114 | ### 5.2.0 115 | 116 | * [Eduardo Turiño](https://github.com/eturino) renamed `acts_as_tree` to `has_closure_tree`. We'll 117 | keep both annotations around for the forseeable future, but I think not name-colliding by default is 118 | strictly better. (Thanks for both the suggestion and PR!) 119 | * [Ryan Selk](https://github.com/rselk) made several enhancements to the migration generation (thanks!). 120 | * [ruok5](https://github.com/ruok5) updated the README to clarify a heirarchy maintenance usecase. Thanks! 121 | * Made migrations error with a helpful message if the target didn't have the `has_closure_tree` or 122 | `acts_as_tree` annotation. This addresses [issue 131](https://github.com/ClosureTree/closure_tree/issues/131). 123 | 124 | ### 5.1.1 125 | 126 | * Fixed bug in `rails g closure_tree:migration` (introduced by me, not by seuros!) 127 | that was reported and fixed by [Rich Kuo](https://github.com/richkuo). Thanks! 128 | 129 | ### 5.1.0 130 | 131 | * [Abdelkader Boudih](https://github.com/seuros) added a database generator 132 | for the hierarchies table. Thanks! 133 | * [Jason Weathered](https://github.com/jasoncodes) fixed [issue #117](https://github.com/ClosureTree/closure_tree/pull/117) 134 | with the preordered traversal code that assumed the primary key column was called `id`. Thanks! 135 | 136 | ### 5.0.0 137 | 138 | #### Breaking API changes 139 | 140 | * `find_by_path` and `find_or_create_by_path` now takes either an array of strings 141 | or an array of attribute hashes, which can include the inheritance column for STI support. 142 | * Removed the extraneous `base_class` `acts_as_tree` option—it needlessly duplicated ActiveRecord's method. 143 | * Removed the unused `name` `acts_as_tree` option. 144 | 145 | #### Improvements and bugfixes 146 | 147 | * Cleaned up the inheritance support methods to delegate correctly to ActiveRecord 148 | * Fixed a query generation error when ancestor paths exceeded 50 items. 149 | * Documented the `.touch` option 150 | 151 | ### 4.6.3 152 | 153 | * More goodness from [Abdelkader Boudih](https://github.com/seuros), including rspec 3 support. 154 | 155 | ### 4.6.2 156 | 157 | * Pulled in [106](https://github.com/ClosureTree/closure_tree/pull/106) which fixed a bug introduced 158 | in 4.6.0 which broke if the numeric ordering column wasn't named 'sort_order'. Tests have been 159 | added. Thanks for the fix, [Fission Xuiptz](https://github.com/fissionxuiptz)! 160 | 161 | ### 4.6.1 162 | 163 | * Address [issue 60](https://github.com/ClosureTree/closure_tree/issues/60) (use `.empty?` rather 164 | than `.nil?`—thanks for the suggestion, [Leonel Galán](https://github.com/leonelgalan), 165 | [Doug Mayer](https://github.com/doxavore) and [Samnang Chhun](https://github.com/samnang)! 166 | 167 | ### 4.6.0 168 | 169 | * Deterministically ordered trees are guaranteed to have a sort_order now. 170 | 171 | **This may be a breaking change if you're expecting sort_order to be nullable.** 172 | 173 | Many thanks to [David Schmidt](https://github.com/inetdavid) for raising and 174 | working on the issue! 175 | 176 | * Added ```append_child``` and ```prepend_child``` 177 | 178 | * All raw SQL is now ```strip_heredoc```'ed 179 | 180 | ### 4.5.0 181 | 182 | * Merged a bunch of great changes from [Abdelkader Boudih](https://github.com/seuros), 183 | including a change to use [appraisal](https://github.com/thoughtbot/appraisal) 184 | * Added Travis builds for Rails 4.1.1 and Ruby 2.1.2 185 | * Dropped support for Rails 3.1, as it is no longer receiving security patches. 186 | See http://rubyonrails.org/security/ for more information. 187 | 188 | ### 4.4.0 189 | 190 | * Added ```.self_and_descendant_ids``` and ```.self_and_ancestors_ids``` from [PR92](https://github.com/ClosureTree/closure_tree/pull/92). 191 | Thanks, [Kir Shatrov](https://github.com/kirs)! 192 | 193 | * Dropped support for Rails 3.0. 194 | 195 | ### 4.3.0 196 | 197 | * Use [foreigner](https://github.com/matthuhiggins/foreigner) to prove that 198 | things are inserted and deleted without violating foreign key constraints 199 | 200 | * Added Rails 4.1.0.rc2 as a Travis CI build target 201 | 202 | ### 4.2.9 203 | 204 | * Support for Heroku's cray assets:precompile hack for Rails 4. 205 | Addresses [issue 78](https://github.com/ClosureTree/closure_tree/issues/78). 206 | Thanks for the assist, [Alex Bowman](https://github.com/axlekb). 207 | 208 | ### 4.2.8 209 | 210 | * More massaging for Rails 4 and ```attr_accessible``` support 211 | 212 | ### 4.2.7 213 | 214 | * ```self_and_ancestors``` and ```ancestry_hierarchy``` are reloaded 215 | when nodes are reparented. Addresses [issue 68](https://github.com/ClosureTree/closure_tree/issues/68). 216 | Thanks for the assist, [Ivan Stana](https://github.com/istana). 217 | 218 | ### 4.2.6 219 | 220 | * Explicitly added MIT licensing to the gemspec. 221 | 222 | ### 4.2.5 223 | 224 | * Fix for potential deadlock from ```delete_hierarchy_references``` not being called within an 225 | advisory lock. Thanks, [Armando Guereca](https://github.com/aguereca), for finding that! 226 | 227 | * Sped up find_or_create_by_path to skip cycle detection validation. 228 | A node whose ancestry was 200-deep took 20 seconds to create (!!), and now takes < 1 second. 229 | 230 | * Fixed issue with MySQL that prevented nodes > 60 levels deep from being created 231 | 232 | ### 4.2.4 233 | 234 | * Support for ```root?```, ```child?```, and proper parent-child associations 235 | when both the parent and the child are not persisted. Addresses [issue 64](https://github.com/ClosureTree/closure_tree/issues/64). 236 | Thanks for the help, [Gabriel Mazetto](https://github.com/brodock)! 237 | 238 | ### 4.2.3 239 | 240 | * Fixed ```attr_accessible?``` error introduced in 4.2.2 ([issue 66](https://github.com/ClosureTree/closure_tree/issues/66)). 241 | * Switched to use new WithAdvisoryLock::DatabaseAdapterSupport (in v0.0.9) to add Postgis support 242 | 243 | ### 4.2.2 244 | 245 | * Support attr_accessible and strong_attributes even if you're on Rails 4 246 | 247 | ### 4.2.1 248 | 249 | * Deleting from NumericDeterministicOrdering doesn't create sort order gaps anymore. 250 | 251 | ### 4.2.0 252 | 253 | * Added ```with_ancestor(*ancestors)```. Thanks for the idea, [Matt](https://github.com/mgornick)! 254 | * Applied [Leonel Galan](https://github.com/leonelgalan)'s fix for Strong Attribute support 255 | * ```find_or_create_by``` now uses passed-in attributes as both selection and creation criteria. 256 | Thanks for the help, [Judd Blair](https://github.com/juddblair)! 257 | **Please note that this changes prior behavior—test your code with this new version!** 258 | * ```ct_advisory_lock``` was moved into the ```_ct``` support class, to reduce model method pollution 259 | * Moved a bunch of code into more focused piles of module mixins 260 | 261 | ### 4.1.0 262 | 263 | * Added support for Rails 4.0.0.rc1 and Ruby 2.0.0 (while maintaining backward compatibility with Rails 3, BOOYA) 264 | * Added ```#to_dot_digraph```, suitable for Graphviz rendering 265 | 266 | ### 4.0.1 267 | 268 | * Numeric, deterministically ordered siblings will always be [0..#{self_and_siblings.count}] 269 | (previously, the sort order might use negative values, which broke the preordering). 270 | Resolves [issue 49](https://github.com/ClosureTree/closure_tree/issues/49). Thanks for the help, 271 | [Leonel Galan](https://github.com/leonelgalan), [Juan Hoyos](https://github.com/elhoyos), and 272 | [Michael Elfassy](https://github.com/elfassy)! 273 | 274 | * The ```order``` option can be a symbol now. Resolves [issue 46](https://github.com/ClosureTree/closure_tree/issues/46). 275 | 276 | ### 4.0.0 277 | 278 | * Moved all of closure_tree's implementation-detail methods into a ```ClosureTree::Support``` 279 | instance, which removes almost all of the namespace pollution in your models that wasn't 280 | for normal consumption. If you were using any of these methods, they're now available through 281 | the "_ct" class and instance member. 282 | 283 | *This change may break consumers*, so I incremented the major version number, even though no new 284 | functionality was released. 285 | 286 | ### 3.10.2 287 | 288 | * Prevent faulty SQL statement when ```#siblings``` is called on an unsaved records. 289 | Resolves [issue 52](https://github.com/ClosureTree/closure_tree/pull/52). Perfect pull 290 | request by [Gary Greyling](https://github.com/garygreyling). 291 | 292 | * The ```.roots``` class method now correctly respects the ```:order``` option. 293 | Resolves [issue 53](https://github.com/ClosureTree/closure_tree/issues/53). 294 | Thanks for finding this, [Brendon Muir](https://github.com/brendon)! 295 | 296 | ### 3.10.1 297 | 298 | * Multipart constant names like "Admin::PageHierarchy" are now supported. 299 | Resolves [issue 47](https://github.com/ClosureTree/closure_tree/issues/47). 300 | Thanks for the perfect pull request, [Simon Menke](https://github.com/fd)! 301 | 302 | * Committing transactions involving large numbers of hierarchy model classes was very slow due 303 | to hash collisions in the hierarchy class. A better hash implementation addressed 304 | [issue 48](https://github.com/ClosureTree/closure_tree/issues/48). 305 | Thanks, [Joel Turkel](https://github.com/jturkel)! 306 | 307 | ### 3.10.0 308 | 309 | * Added ```#roots_and_descendants_preordered```. 310 | Thanks for the suggestion, [Leonel Galan](https://github.com/leonelgalan)! 311 | 312 | ### 3.9.0 313 | 314 | * Added ```.child_ids```. 315 | * Removed ```dependent => destroy``` on the descendant_hierarchy and ancestor_hierarchy collections 316 | (they were a mistake). 317 | * Clarified documentation for creation and child associations. 318 | Because ```Tag.create!(:parent => ...)``` requires a ```.reload```, I removed it as an example. 319 | 320 | All three of these improvements were suggested by Andrew Bromwich. Thanks! 321 | 322 | ### 3.8.2 323 | 324 | * find_by_path uses 1 SELECT now. BOOM. 325 | 326 | ### 3.8.1 327 | 328 | * Double-check locking for find_or_create_by_path 329 | 330 | ### 3.8.0 331 | 332 | * Support for preordered descendants. This requires a numeric sort order column. 333 | Resolves [feature request 38](https://github.com/ClosureTree/closure_tree/issues/38). 334 | * Moved modules from ```acts_as_tree``` into separate files 335 | 336 | ### 3.7.3 337 | 338 | Due to MySQL's inability to lock rows properly, I've switched to advisory_locks for 339 | all write paths. This will prevent deadlocks, addressing 340 | [issue 41](https://github.com/ClosureTree/closure_tree/issues/41). 341 | 342 | ### 3.7.2 343 | 344 | * Support for UUID primary keys. Addresses 345 | [issue 40](https://github.com/ClosureTree/closure_tree/issues/40). Thanks for the pull request, 346 | [Julien](https://github.com/calexicoz)! 347 | 348 | ### 3.7.1 349 | 350 | * Moved requires into ActiveSupport.on_load 351 | * Added ```require 'with_advisory_lock'``` 352 | 353 | ### 3.7.0 354 | 355 | **Thread safety!** 356 | * [Advisory locks](https://github.com/ClosureTree/with_advisory_lock) were 357 | integrated with the class-level ```find_or_create_by_path``` and ```rebuild!```. 358 | * Pessimistic locking is used by the instance-level ```find_or_create_by_path```. 359 | 360 | ### 3.6.9 361 | 362 | * [Don Morrison](https://github.com/elskwid) massaged the [#hash_tree](#nested-hashes) query to 363 | be more efficient, and found a bug in ```hash_tree```'s query that resulted in duplicate rows, 364 | wasting time on the ruby side. 365 | 366 | ### 3.6.7 367 | 368 | * Added workaround for ActiveRecord::Observer usage pre-db-creation. Addresses 369 | [issue 32](https://github.com/ClosureTree/closure_tree/issues/32). 370 | Thanks, [Don Morrison](https://github.com/elskwid)! 371 | 372 | ### 3.6.6 373 | 374 | * Added support for Rails 4's [strong parameter](https://github.com/rails/strong_parameters). 375 | Thanks, [James Miller](https://github.com/bensie)! 376 | 377 | ### 3.6.5 378 | 379 | * Use ```quote_table_name``` instead of ```quote_column_name```. Addresses 380 | [issue 29](https://github.com/ClosureTree/closure_tree/issues/29). Thanks, 381 | [Marcello Barnaba](https://github.com/vjt)! 382 | 383 | ### 3.6.4 384 | 385 | * Use ```.pluck``` when available for ```.ids_from```. Addresses 386 | [issue 26](https://github.com/ClosureTree/closure_tree/issues/26). Thanks, 387 | [Chris Sturgill](https://github.com/sturgill)! 388 | 389 | ### 3.6.3 390 | 391 | * Fixed [issue 24](https://github.com/ClosureTree/closure_tree/issues/24), which optimized ```#hash_tree``` 392 | for roots. Thanks, [Saverio Trioni](https://github.com/rewritten)! 393 | 394 | ### 3.6.2 395 | 396 | * Fixed [issue 23](https://github.com/ClosureTree/closure_tree/issues/23), which added support for ```#siblings``` 397 | when sort_order wasn't specified. Thanks, [Gary Greyling](https://github.com/garygreyling)! 398 | 399 | ### 3.6.1 400 | 401 | * Fixed [issue 20](https://github.com/ClosureTree/closure_tree/issues/20), which affected 402 | deterministic ordering when siblings where different STI classes. Thanks, [edwinramirez](https://github.com/edwinramirez)! 403 | 404 | ### 3.6.0 405 | 406 | Added support for: 407 | * ```:hierarchy_class_name``` as an option 408 | * ActiveRecord::Base.table_name_prefix 409 | * ActiveRecord::Base.table_name_suffix 410 | 411 | This addresses [issue 21](https://github.com/ClosureTree/closure_tree/issues/21). Thanks, [Judd Blair](https://github.com/juddblair)! 412 | 413 | ### 3.5.2 414 | 415 | * Added ```find_all_by_generation``` 416 | for [feature request 17](https://github.com/ClosureTree/closure_tree/issues/17). 417 | 418 | ### 3.4.2 419 | 420 | * Fixed [issue 18](https://github.com/ClosureTree/closure_tree/issues/18), which affected 421 | append_node/prepend_node ordering when the first node didn't have an explicit order_by value 422 | 423 | ### 3.4.1 424 | 425 | * Reverted .gemspec mistake that changed add_development_dependency to add_runtime_dependency 426 | 427 | ### 3.4.0 428 | 429 | Fixed [issue 15](https://github.com/ClosureTree/closure_tree/issues/15): 430 | * "parent" is now attr_accessible, which adds support for constructor-provided parents. 431 | * updated readme accordingly 432 | 433 | ### 3.3.2 434 | 435 | * Merged calebphillips' patch for a more efficient leaves query 436 | 437 | ### 3.3.1 438 | 439 | * Added support for partially-unsaved hierarchies [issue 13](https://github.com/ClosureTree/closure_tree/issues/13): 440 | ``` 441 | a = Tag.new(name: "a") 442 | b = Tag.new(name: "b") 443 | a.children << b 444 | a.save 445 | ``` 446 | 447 | ### 3.3.0 448 | 449 | * Added [```hash_tree```](#nested-hashes). 450 | 451 | ### 3.2.1 452 | 453 | * Added ```ancestor_ids```, ```descendant_ids```, and ```sibling_ids``` 454 | * Added example spec to solve [issue 9](https://github.com/ClosureTree/closure_tree/issues/9) 455 | 456 | ### 3.2.0 457 | 458 | * Added support for deterministic ordering of nodes. 459 | 460 | ### 3.1.0 461 | 462 | * Switched to using ```has_many :though``` rather than ```has_and_belongs_to_many``` 463 | 464 | ### 3.0.4 465 | 466 | * Merged [pull request](https://github.com/ClosureTree/closure_tree/pull/8) to fix ```.siblings``` and ```.self_and_siblings``` 467 | (Thanks, [eljojo](https://github.com/eljojo)!) 468 | 469 | ### 3.0.3 470 | 471 | * Added support for ActiveRecord's whitelist_attributes 472 | (Make sure you read [the Rails Security Guide](http://guides.rubyonrails.org/security.html), and 473 | enable ```config.active_record.whitelist_attributes``` in your ```config/application.rb``` ASAP!) 474 | 475 | ### 3.0.2 476 | 477 | * Fix for ancestry-loop detection (performed by a validation, not through raising an exception in before_save) 478 | 479 | ### 3.0.1 480 | 481 | * Support 3.2.0's fickle deprecation of InstanceMethods (Thanks, [jheiss](https://github.com/ClosureTree/closure_tree/pull/5))! 482 | 483 | ### 3.0.0 484 | 485 | * Support for polymorphic trees 486 | * ```find_by_path``` and ```find_or_create_by_path``` signatures changed to support constructor attributes 487 | * tested against Rails 3.1.3 488 | 489 | ### 2.0.0 490 | 491 | * Had to increment the major version, as rebuild! will need to be called by prior consumers to support the new ```leaves``` class and instance methods. 492 | * Tag deletion is supported now along with ```:dependent => :destroy``` and ```:dependent => :delete_all``` 493 | * Switched from default rails plugin directory structure to rspec 494 | * Support for running specs under different database engines: ```export DB ; for DB in sqlite3 mysql postgresql ; do rake ; done``` 495 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'with_advisory_lock', github: 'closuretree/with_advisory_lock' -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2016 Matthew McEachen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Closure Tree 2 | 3 | ### Closure_tree lets your ActiveRecord models act as nodes in a [tree data structure](http://en.wikipedia.org/wiki/Tree_%28data_structure%29) 4 | 5 | Common applications include modeling hierarchical data, like tags, threaded comments, page graphs in CMSes, 6 | and tracking user referrals. 7 | 8 | [![CI](https://github.com/ClosureTree/closure_tree/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/ClosureTree/closure_tree/actions/workflows/ci.yml) 9 | [![Gem Version](https://badge.fury.io/rb/closure_tree.svg)](https://badge.fury.io/rb/closure_tree) 10 | 11 | Dramatically more performant than 12 | [ancestry](https://github.com/stefankroes/ancestry) and 13 | [acts_as_tree](https://github.com/amerine/acts_as_tree), and even more 14 | awesome than [awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set/), 15 | closure_tree has some great features: 16 | 17 | * __Best-in-class select performance__: 18 | * Fetch your whole ancestor lineage in 1 SELECT. 19 | * Grab all your descendants in 1 SELECT. 20 | * Get all your siblings in 1 SELECT. 21 | * Fetch all [descendants as a nested hash](#nested-hashes) in 1 SELECT. 22 | * [Find a node by ancestry path](#find_or_create_by_path) in 1 SELECT. 23 | * __Best-in-class mutation performance__: 24 | * 2 SQL INSERTs on node creation 25 | * 3 SQL INSERT/UPDATEs on node reparenting 26 | * __Support for [concurrency](#concurrency)__ (using [with_advisory_lock](https://github.com/ClosureTree/with_advisory_lock)) 27 | * __Tested against ActiveRecord 7.1+ with Ruby 3.3+__ 28 | * Support for reparenting children (and all their descendants) 29 | * Support for [single-table inheritance (STI)](#sti) within the hierarchy 30 | * ```find_or_create_by_path``` for [building out heterogeneous hierarchies quickly and conveniently](#find_or_create_by_path) 31 | * Support for [deterministic ordering](#deterministic-ordering) 32 | * Support for [preordered](http://en.wikipedia.org/wiki/Tree_traversal#Pre-order) traversal of descendants 33 | * Support for rendering trees in [DOT format](http://en.wikipedia.org/wiki/DOT_(graph_description_language)), using [Graphviz](http://www.graphviz.org/) 34 | * Excellent [test coverage](#testing) in a comprehensive variety of environments 35 | 36 | See [Bill Karwin](http://karwin.blogspot.com/)'s excellent 37 | [Models for hierarchical data presentation](http://www.slideshare.net/billkarwin/models-for-hierarchical-data) 38 | for a description of different tree storage algorithms. 39 | 40 | ## Table of Contents 41 | 42 | - [Installation](#installation) 43 | - [Warning](#warning) 44 | - [Usage](#usage) 45 | - [Accessing Data](#accessing-data) 46 | - [Polymorphic hierarchies with STI](#polymorphic-hierarchies-with-sti) 47 | - [Deterministic ordering](#deterministic-ordering) 48 | - [Concurrency](#concurrency) 49 | - [FAQ](#faq) 50 | - [Testing](#testing) 51 | - [Change log](#change-log) 52 | 53 | ## Installation 54 | 55 | Note that closure_tree only supports ActiveRecord 7.1 and later, and has test coverage for MySQL, PostgreSQL, and SQLite. 56 | 57 | 1. Add `gem 'closure_tree'` to your Gemfile 58 | 59 | 2. Run `bundle install` 60 | 61 | 3. Add `has_closure_tree` (or `acts_as_tree`, which is an alias of the same method) to your hierarchical model: 62 | 63 | ```ruby 64 | class Tag < ActiveRecord::Base 65 | has_closure_tree 66 | end 67 | 68 | class AnotherTag < ActiveRecord::Base 69 | acts_as_tree 70 | end 71 | ``` 72 | 73 | Make sure you check out the [large number of options](#available-options) that `has_closure_tree` accepts. 74 | 75 | **IMPORTANT: Make sure you add `has_closure_tree` _after_ `attr_accessible` and 76 | `self.table_name =` lines in your model.** 77 | 78 | If you're already using other hierarchical gems, like `ancestry` or `acts_as_tree`, please refer 79 | to the [warning section](#warning)! 80 | 81 | 4. Add a migration to add a `parent_id` column to the hierarchical model. 82 | You may want to also [add a column for deterministic ordering of children](#deterministic-ordering), but that's optional. 83 | 84 | ```ruby 85 | class AddParentIdToTag < ActiveRecord::Migration 86 | def change 87 | add_column :tags, :parent_id, :integer 88 | end 89 | end 90 | ``` 91 | 92 | The column must be nullable. Root nodes have a `NULL` `parent_id`. 93 | 94 | 5. Run `rails g closure_tree:migration tag` (and replace `tag` with your model name) 95 | to create the closure tree table for your model. 96 | 97 | By default the table name will be the model's table name, followed by 98 | "_hierarchies". Note that by calling ```has_closure_tree```, a "virtual model" (in this case, ```TagHierarchy```) 99 | will be created dynamically. You don't need to create it. 100 | 101 | 6. Run `rake db:migrate` 102 | 103 | 7. If you're migrating from another system where your model already has a 104 | `parent_id` column, run `Tag.rebuild!` and your 105 | `tag_hierarchies` table will be truncated and rebuilt. 106 | 107 | If you're starting from scratch you don't need to call `rebuild!`. 108 | 109 | NOTE: Run `rails g closure_tree:config` to create an initializer with extra 110 | configurations. (Optional) 111 | 112 | ## Warning 113 | 114 | As stated above, using multiple hierarchy gems (like `ancestry` or `nested set`) on the same model 115 | will most likely result in pain, suffering, hair loss, tooth decay, heel-related ailments, and gingivitis. 116 | Assume things will break. 117 | 118 | ## Usage 119 | 120 | ### Creation 121 | 122 | Create a root node: 123 | 124 | ```ruby 125 | grandparent = Tag.create(name: 'Grandparent') 126 | ``` 127 | 128 | Child nodes are created by appending to the children collection: 129 | 130 | ```ruby 131 | parent = grandparent.children.create(name: 'Parent') 132 | ``` 133 | 134 | Or by appending to the children collection: 135 | 136 | ```ruby 137 | child2 = Tag.new(name: 'Second Child') 138 | parent.children << child2 139 | ``` 140 | 141 | Or by calling the "add_child" method: 142 | 143 | ```ruby 144 | child3 = Tag.new(name: 'Third Child') 145 | parent.add_child child3 146 | ``` 147 | 148 | Or by setting the parent on the child : 149 | 150 | ```ruby 151 | Tag.create(name: 'Fourth Child', parent: parent) 152 | ``` 153 | 154 | Then: 155 | 156 | ```ruby 157 | grandparent.self_and_descendants.collect(&:name) 158 | => ["Grandparent", "Parent", "First Child", "Second Child", "Third Child", "Fourth Child"] 159 | 160 | child1.ancestry_path 161 | => ["Grandparent", "Parent", "First Child"] 162 | ``` 163 | 164 | ### find_or_create_by_path 165 | 166 | You can `find` as well as `find_or_create` by "ancestry paths". 167 | 168 | If you provide an array of strings to these methods, they reference the `name` column in your 169 | model, which can be overridden with the `:name_column` option provided to `has_closure_tree`. 170 | 171 | ```ruby 172 | child = Tag.find_or_create_by_path(%w[grandparent parent child]) 173 | ``` 174 | 175 | As of v5.0.0, `find_or_create_by_path` can also take an array of attribute hashes: 176 | 177 | ```ruby 178 | child = Tag.find_or_create_by_path([ 179 | {name: 'Grandparent', title: 'Sr.'}, 180 | {name: 'Parent', title: 'Mrs.'}, 181 | {name: 'Child', title: 'Jr.'} 182 | ]) 183 | ``` 184 | 185 | If you're using STI, The attribute hashes can contain the `sti_name` and things work as expected: 186 | 187 | ```ruby 188 | child = Label.find_or_create_by_path([ 189 | {type: 'DateLabel', name: '2014'}, 190 | {type: 'DateLabel', name: 'August'}, 191 | {type: 'DateLabel', name: '5'}, 192 | {type: 'EventLabel', name: 'Visit the Getty Center'} 193 | ]) 194 | ``` 195 | 196 | ### Moving nodes around the tree 197 | 198 | Nodes can be moved around to other parents, and closure_tree moves the node's descendancy to the 199 | new parent for you: 200 | 201 | ```ruby 202 | d = Tag.find_or_create_by_path %w[a b c d] 203 | h = Tag.find_or_create_by_path %w[e f g h] 204 | e = h.root 205 | d.add_child(e) # "d.children << e" would work too, of course 206 | h.ancestry_path 207 | => ["a", "b", "c", "d", "e", "f", "g", "h"] 208 | ``` 209 | 210 | When it is more convenient to simply change the `parent_id` of a node directly (for example, when dealing with a form `