closure_tree 7.0.0 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +98 -0
- data/.gitignore +2 -0
- data/.rspec +1 -1
- data/Appraisals +33 -3
- data/CHANGELOG.md +19 -0
- data/Gemfile +4 -0
- data/README.md +6 -8
- data/Rakefile +16 -10
- data/_config.yml +1 -0
- data/bin/appraisal +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/closure_tree.gemspec +6 -2
- data/lib/closure_tree/finders.rb +17 -4
- data/lib/closure_tree/has_closure_tree_root.rb +1 -1
- data/lib/closure_tree/hash_tree_support.rb +3 -3
- data/lib/closure_tree/hierarchy_maintenance.rb +20 -3
- data/lib/closure_tree/model.rb +11 -15
- data/lib/closure_tree/numeric_deterministic_ordering.rb +2 -2
- data/lib/closure_tree/numeric_order_support.rb +2 -2
- data/lib/closure_tree/support.rb +13 -11
- data/lib/closure_tree/support_attributes.rb +1 -1
- data/lib/closure_tree/version.rb +1 -1
- metadata +53 -8
- data/.travis.yml +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4ac0bb16bca6ba730b542b35639b067ad113e2c7010265cae6e0192425e1b82
|
4
|
+
data.tar.gz: 03004e7033f76268a1e96b3ee4a0c5d9066a4c716caab80c6082ddb60404b598
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5eac3ad625f051cc1e5fa62165076119d1cda689b619aeb6e83c9a32bbd33f343817c867cfd97abc26d32c56346eb833d39e747ec1d60ae9abf018e79a762442
|
7
|
+
data.tar.gz: 6f01c8b3b6ef3382340d0743a87e34243e9283bb64d3cefec1a79a7fb23c670f8da02e0125f7497e8afa834077aa75c60830988f14b2c7e095a6f7fe3ac8e02b
|
@@ -0,0 +1,98 @@
|
|
1
|
+
---
|
2
|
+
name: CI
|
3
|
+
|
4
|
+
on:
|
5
|
+
- push
|
6
|
+
- pull_request
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
rspec:
|
10
|
+
runs-on: ubuntu-20.04
|
11
|
+
|
12
|
+
services:
|
13
|
+
postgres:
|
14
|
+
image: 'postgres:13'
|
15
|
+
ports: ['5432:5432']
|
16
|
+
env:
|
17
|
+
POSTGRES_PASSWORD: postgres
|
18
|
+
POSTGRES_DB: closure_tree
|
19
|
+
options: >-
|
20
|
+
--health-cmd pg_isready
|
21
|
+
--health-interval 10s
|
22
|
+
--health-timeout 5s
|
23
|
+
--health-retries 5
|
24
|
+
|
25
|
+
strategy:
|
26
|
+
fail-fast: false
|
27
|
+
matrix:
|
28
|
+
ruby:
|
29
|
+
- '3.0'
|
30
|
+
- '2.7'
|
31
|
+
- '2.6'
|
32
|
+
- '2.5'
|
33
|
+
rails:
|
34
|
+
- activerecord_6.1
|
35
|
+
- activerecord_6.0
|
36
|
+
- activerecord_5.2
|
37
|
+
- activerecord_5.1
|
38
|
+
- activerecord_5.0
|
39
|
+
- activerecord_4.2
|
40
|
+
- activerecord_edge
|
41
|
+
adapter:
|
42
|
+
- sqlite3
|
43
|
+
- mysql2
|
44
|
+
- postgresql
|
45
|
+
exclude:
|
46
|
+
- ruby: '2.7'
|
47
|
+
rails: activerecord_4.2
|
48
|
+
- ruby: '3.0'
|
49
|
+
rails: activerecord_4.2
|
50
|
+
- ruby: '3.0'
|
51
|
+
rails: activerecord_5.0
|
52
|
+
- ruby: '3.0'
|
53
|
+
rails: activerecord_5.1
|
54
|
+
- ruby: '3.0'
|
55
|
+
rails: activerecord_5.2
|
56
|
+
- ruby: '2.5'
|
57
|
+
rails: activerecord_edge
|
58
|
+
- ruby: '2.6'
|
59
|
+
rails: activerecord_edge
|
60
|
+
|
61
|
+
steps:
|
62
|
+
- name: Checkout
|
63
|
+
uses: actions/checkout@v2
|
64
|
+
|
65
|
+
- name: Setup Ruby
|
66
|
+
uses: ruby/setup-ruby@v1
|
67
|
+
with:
|
68
|
+
ruby-version: ${{ matrix.ruby }}
|
69
|
+
|
70
|
+
- name: Set DB Adapter
|
71
|
+
env:
|
72
|
+
RAILS_VERSION: ${{ matrix.rails }}
|
73
|
+
DB_ADAPTER: ${{ matrix.adapter }}
|
74
|
+
|
75
|
+
# See: https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md#mysql
|
76
|
+
run: |
|
77
|
+
if [ "${DB_ADAPTER}" = "mysql2" ]; then
|
78
|
+
sudo systemctl start mysql.service
|
79
|
+
mysql -u root -proot -e 'create database closure_tree;'
|
80
|
+
fi
|
81
|
+
|
82
|
+
- name: Bundle
|
83
|
+
env:
|
84
|
+
RAILS_VERSION: ${{ matrix.rails }}
|
85
|
+
DB_ADAPTER: ${{ matrix.adapter }}
|
86
|
+
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
|
87
|
+
run: |
|
88
|
+
gem install bundler
|
89
|
+
bundle config path vendor/bundle
|
90
|
+
bundle install --jobs 4 --retry 3
|
91
|
+
|
92
|
+
- name: RSpec
|
93
|
+
env:
|
94
|
+
RAILS_VERSION: ${{ matrix.rails }}
|
95
|
+
DB_ADAPTER: ${{ matrix.adapter }}
|
96
|
+
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
|
97
|
+
WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }}
|
98
|
+
run: bin/rake --trace spec:all
|
data/.gitignore
CHANGED
data/.rspec
CHANGED
@@ -1 +1 @@
|
|
1
|
-
--
|
1
|
+
--color
|
data/Appraisals
CHANGED
@@ -4,7 +4,7 @@ appraise 'activerecord-4.2' do
|
|
4
4
|
platforms :ruby do
|
5
5
|
gem 'mysql2', "< 0.5"
|
6
6
|
gem 'pg', "~> 0.21"
|
7
|
-
gem 'sqlite3'
|
7
|
+
gem 'sqlite3', '~> 1.3.13'
|
8
8
|
end
|
9
9
|
|
10
10
|
platforms :jruby do
|
@@ -19,7 +19,7 @@ appraise 'activerecord-5.0' do
|
|
19
19
|
platforms :ruby do
|
20
20
|
gem 'mysql2'
|
21
21
|
gem 'pg'
|
22
|
-
gem 'sqlite3'
|
22
|
+
gem 'sqlite3', '~> 1.3.13'
|
23
23
|
end
|
24
24
|
|
25
25
|
platforms :jruby do
|
@@ -34,7 +34,7 @@ appraise 'activerecord-5.1' do
|
|
34
34
|
platforms :ruby do
|
35
35
|
gem 'mysql2'
|
36
36
|
gem 'pg'
|
37
|
-
gem 'sqlite3'
|
37
|
+
gem 'sqlite3', '~> 1.3.13'
|
38
38
|
end
|
39
39
|
|
40
40
|
platforms :jruby do
|
@@ -59,6 +59,36 @@ appraise 'activerecord-5.2' do
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
|
+
appraise 'activerecord-6.0' do
|
63
|
+
gem 'activerecord', '~> 6.0.0'
|
64
|
+
platforms :ruby do
|
65
|
+
gem 'mysql2'
|
66
|
+
gem 'pg'
|
67
|
+
gem 'sqlite3'
|
68
|
+
end
|
69
|
+
|
70
|
+
platforms :jruby do
|
71
|
+
gem 'activerecord-jdbcmysql-adapter'
|
72
|
+
gem 'activerecord-jdbcpostgresql-adapter'
|
73
|
+
gem 'activerecord-jdbcsqlite3-adapter'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
appraise 'activerecord-6.1' do
|
78
|
+
gem 'activerecord', '~> 6.1.0'
|
79
|
+
platforms :ruby do
|
80
|
+
gem 'mysql2'
|
81
|
+
gem 'pg'
|
82
|
+
gem 'sqlite3'
|
83
|
+
end
|
84
|
+
|
85
|
+
platforms :jruby do
|
86
|
+
gem 'activerecord-jdbcmysql-adapter'
|
87
|
+
gem 'activerecord-jdbcpostgresql-adapter'
|
88
|
+
gem 'activerecord-jdbcsqlite3-adapter'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
62
92
|
appraise 'activerecord-edge' do
|
63
93
|
gem 'activerecord', github: 'rails/rails'
|
64
94
|
platforms :ruby do
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [7.4.0](https://github.com/ClosureTree/closure_tree/tree/7.4.0)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/ClosureTree/closure_tree/compare/v7.3.0...7.4.0)
|
6
|
+
|
7
|
+
- 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))
|
8
|
+
- Add with\_descendant to readme [\#381](https://github.com/ClosureTree/closure_tree/pull/381) ([mattvague](https://github.com/mattvague))
|
9
|
+
|
10
|
+
### 7.3.0
|
11
|
+
- Ruby 3.0 support
|
12
|
+
|
13
|
+
### 7.2.0
|
14
|
+
- Ruby 2.7 support
|
15
|
+
- Ordering raw SQL argument wrapped with Arel.sql
|
16
|
+
|
17
|
+
### 7.1.0
|
18
|
+
Closure Tree is now tested against Rails 6.0
|
19
|
+
- Directly require core_ext for String#strip_heredoc[PR 350](https://github.com/ClosureTree/closure_tree/pull/350)
|
20
|
+
- Call Module#module_parent instead of deprecated #parent[PR 354](https://github.com/ClosureTree/closure_tree/pull/354)
|
21
|
+
|
3
22
|
### 7.0.0
|
4
23
|
Closure Tree is now tested against Rails 5.2
|
5
24
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# Closure Tree
|
2
2
|
|
3
|
-
__Important: please [vote on the future of ClosureTree](https://github.com/ClosureTree/closure_tree/issues/277)!__
|
4
|
-
|
5
3
|
### Closure_tree lets your ActiveRecord models act as nodes in a [tree data structure](http://en.wikipedia.org/wiki/Tree_%28data_structure%29)
|
6
4
|
|
7
5
|
Common applications include modeling hierarchical data, like tags, threaded comments, page graphs in CMSes,
|
@@ -10,7 +8,6 @@ and tracking user referrals.
|
|
10
8
|
[](https://gitter.im/closure_tree/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
11
9
|
[](http://travis-ci.org/ClosureTree/closure_tree)
|
12
10
|
[](https://badge.fury.io/rb/closure_tree)
|
13
|
-
[](https://gemnasium.com/github.com/ClosureTree/closure_tree)
|
14
11
|
|
15
12
|
Dramatically more performant than
|
16
13
|
[ancestry](https://github.com/stefankroes/ancestry) and
|
@@ -28,7 +25,7 @@ closure_tree has some great features:
|
|
28
25
|
* 2 SQL INSERTs on node creation
|
29
26
|
* 3 SQL INSERT/UPDATEs on node reparenting
|
30
27
|
* __Support for [concurrency](#concurrency)__ (using [with_advisory_lock](https://github.com/ClosureTree/with_advisory_lock))
|
31
|
-
* __Tested against ActiveRecord 4.2, 5.0,
|
28
|
+
* __Tested against ActiveRecord 4.2, 5.0, 5.1, 5.2 and 6.0 with Ruby 2.5 and 2.6__
|
32
29
|
* Support for reparenting children (and all their descendants)
|
33
30
|
* Support for [single-table inheritance (STI)](#sti) within the hierarchy
|
34
31
|
* ```find_or_create_by_path``` for [building out heterogeneous hierarchies quickly and conveniently](#find_or_create_by_path)
|
@@ -340,8 +337,9 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
|
|
340
337
|
* ```Tag.find_by_path(path, attributes)``` returns the node whose name path is ```path```. See (#find_or_create_by_path).
|
341
338
|
* ```Tag.find_or_create_by_path(path, attributes)``` returns the node whose name path is ```path```, and will create the node if it doesn't exist already.See (#find_or_create_by_path).
|
342
339
|
* ```Tag.find_all_by_generation(generation_level)``` returns the descendant nodes who are ```generation_level``` away from a root. ```Tag.find_all_by_generation(0)``` is equivalent to ```Tag.roots```.
|
343
|
-
* ```Tag.with_ancestor(ancestors)``` scopes to all descendants whose
|
344
|
-
|
340
|
+
* ```Tag.with_ancestor(ancestors)``` scopes to all descendants whose ancestors(s) is/are in the given list.
|
341
|
+
* ```Tag.with_descendant(ancestors)``` scopes to all ancestors whose descendant(s) is/are in the given list.
|
342
|
+
* ```Tag.lowest_common_ancestor(descendants)``` finds the lowest common ancestor of the descendants.
|
345
343
|
### Instance methods
|
346
344
|
|
347
345
|
* ```tag.root``` returns the root for this node
|
@@ -665,8 +663,8 @@ end
|
|
665
663
|
|
666
664
|
Closure tree is [tested under every valid combination](http://travis-ci.org/#!/ClosureTree/closure_tree) of
|
667
665
|
|
668
|
-
* Ruby 2.
|
669
|
-
* ActiveRecord 4.2, 5.
|
666
|
+
* Ruby 2.5, 2.6 and 2.7
|
667
|
+
* ActiveRecord 4.2, 5.x and 6.0
|
670
668
|
* PostgreSQL, MySQL, and SQLite. Concurrency tests are only run with MySQL and PostgreSQL.
|
671
669
|
|
672
670
|
Assuming you're using [rbenv](https://github.com/sstephenson/rbenv), you can use ```tests.sh``` to
|
data/Rakefile
CHANGED
@@ -1,23 +1,20 @@
|
|
1
|
-
|
2
|
-
require 'bundler/setup'
|
3
|
-
rescue LoadError
|
4
|
-
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
-
end
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
7
|
-
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
8
5
|
|
9
|
-
require "rspec/core/rake_task"
|
10
6
|
RSpec::Core::RakeTask.new(:spec) do |task|
|
11
|
-
task.pattern = 'spec/*_spec.rb'
|
7
|
+
task.pattern = 'spec/closure_tree/*_spec.rb'
|
12
8
|
end
|
13
9
|
|
14
|
-
task :
|
10
|
+
task default: :spec
|
15
11
|
|
16
12
|
namespace :spec do
|
17
13
|
desc 'Run all spec variants'
|
18
14
|
task :all do
|
19
|
-
rake = '
|
15
|
+
rake = 'bin/rake'
|
20
16
|
fail unless system("#{rake} spec:generators")
|
17
|
+
|
21
18
|
[['', ''], ['db_prefix_', ''], ['', '_db_suffix'], ['abc_', '_123']].each do |prefix, suffix|
|
22
19
|
env = "DB_PREFIX=#{prefix} DB_SUFFIX=#{suffix}"
|
23
20
|
fail unless system("#{rake} spec #{env}")
|
@@ -29,3 +26,12 @@ namespace :spec do
|
|
29
26
|
task.pattern = 'spec/generators/*_spec.rb'
|
30
27
|
end
|
31
28
|
end
|
29
|
+
|
30
|
+
require 'github_changelog_generator/task'
|
31
|
+
GitHubChangelogGenerator::RakeTask.new :changelog do |config|
|
32
|
+
config.user = 'ClosureTree'
|
33
|
+
config.project = 'closure_tree'
|
34
|
+
config.issues = false
|
35
|
+
config.future_release = '5.2.0'
|
36
|
+
config.since_tag = 'v7.3.0'
|
37
|
+
end
|
data/_config.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
theme: jekyll-theme-leap-day
|
data/bin/appraisal
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'appraisal' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("appraisal", "appraisal")
|
data/bin/rake
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rspec' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/closure_tree.gemspec
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/closure_tree/version'
|
3
4
|
|
4
5
|
Gem::Specification.new do |gem|
|
5
6
|
gem.name = 'closure_tree'
|
@@ -26,8 +27,11 @@ Gem::Specification.new do |gem|
|
|
26
27
|
gem.add_development_dependency 'database_cleaner'
|
27
28
|
gem.add_development_dependency 'generator_spec'
|
28
29
|
gem.add_development_dependency 'parallel'
|
30
|
+
gem.add_development_dependency 'pg'
|
29
31
|
gem.add_development_dependency 'rspec-instafail'
|
30
32
|
gem.add_development_dependency 'rspec-rails'
|
33
|
+
gem.add_development_dependency 'sqlite3'
|
34
|
+
gem.add_development_dependency 'simplecov'
|
31
35
|
gem.add_development_dependency 'timecop'
|
32
36
|
# gem.add_development_dependency 'byebug'
|
33
37
|
# gem.add_development_dependency 'ruby-prof' # <- don't need this normally.
|
data/lib/closure_tree/finders.rb
CHANGED
@@ -34,7 +34,7 @@ module ClosureTree
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def find_all_by_generation(generation_level)
|
37
|
-
s = _ct.base_class.joins(<<-SQL.
|
37
|
+
s = _ct.base_class.joins(<<-SQL.squish)
|
38
38
|
INNER JOIN (
|
39
39
|
SELECT descendant_id
|
40
40
|
FROM #{_ct.quoted_hierarchy_table_name}
|
@@ -70,7 +70,7 @@ module ClosureTree
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def leaves
|
73
|
-
s = joins(<<-SQL.
|
73
|
+
s = joins(<<-SQL.squish)
|
74
74
|
INNER JOIN (
|
75
75
|
SELECT ancestor_id
|
76
76
|
FROM #{_ct.quoted_hierarchy_table_name}
|
@@ -99,8 +99,21 @@ module ClosureTree
|
|
99
99
|
_ct.scope_with_order(scope)
|
100
100
|
end
|
101
101
|
|
102
|
+
def lowest_common_ancestor(*descendants)
|
103
|
+
descendants = descendants.first if descendants.length == 1 && descendants.first.respond_to?(:each)
|
104
|
+
ancestor_id = hierarchy_class
|
105
|
+
.where(descendant_id: descendants)
|
106
|
+
.group(:ancestor_id)
|
107
|
+
.having("COUNT(ancestor_id) = #{descendants.count}")
|
108
|
+
.order(Arel.sql('MIN(generations) ASC'))
|
109
|
+
.limit(1)
|
110
|
+
.pluck(:ancestor_id).first
|
111
|
+
|
112
|
+
find_by(primary_key => ancestor_id) if ancestor_id
|
113
|
+
end
|
114
|
+
|
102
115
|
def find_all_by_generation(generation_level)
|
103
|
-
s = joins(<<-SQL.
|
116
|
+
s = joins(<<-SQL.squish)
|
104
117
|
INNER JOIN (
|
105
118
|
SELECT #{primary_key} as root_id
|
106
119
|
FROM #{_ct.quoted_table_name}
|
@@ -130,7 +143,7 @@ module ClosureTree
|
|
130
143
|
last_joined_table = _ct.table_name
|
131
144
|
path.reverse.each_with_index do |ea, idx|
|
132
145
|
next_joined_table = "p#{idx}"
|
133
|
-
scope = scope.joins(<<-SQL.
|
146
|
+
scope = scope.joins(<<-SQL.squish)
|
134
147
|
INNER JOIN #{_ct.quoted_table_name} #{ _ct.t_alias_keyword } #{next_joined_table}
|
135
148
|
ON #{next_joined_table}.#{_ct.quoted_id_column_name} =
|
136
149
|
#{connection.quote_table_name(last_joined_table)}.#{_ct.quoted_parent_column_name}
|
@@ -8,7 +8,7 @@ module ClosureTree
|
|
8
8
|
options[:class_name] ||= assoc_name.to_s.sub(/\Aroot_/, "").classify
|
9
9
|
options[:foreign_key] ||= self.name.underscore << "_id"
|
10
10
|
|
11
|
-
has_one assoc_name, -> { where(parent: nil) }, options
|
11
|
+
has_one assoc_name, -> { where(parent: nil) }, **options
|
12
12
|
|
13
13
|
# Fetches the association, eager loading all children and given associations
|
14
14
|
define_method("#{assoc_name}_including_tree") do |*args|
|
@@ -4,7 +4,7 @@ module ClosureTree
|
|
4
4
|
# Deepest generation, within limit, for each descendant
|
5
5
|
# NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
|
6
6
|
having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
|
7
|
-
generation_depth = <<-SQL.
|
7
|
+
generation_depth = <<-SQL.squish
|
8
8
|
INNER JOIN (
|
9
9
|
SELECT descendant_id, MAX(generations) as depth
|
10
10
|
FROM #{quoted_hierarchy_table_name}
|
@@ -14,7 +14,7 @@ module ClosureTree
|
|
14
14
|
ON #{quoted_table_name}.#{model_class.primary_key} = generation_depth.descendant_id
|
15
15
|
SQL
|
16
16
|
scope_with_order(scope.joins(generation_depth), 'generation_depth.depth')
|
17
|
-
|
17
|
+
end
|
18
18
|
|
19
19
|
def hash_tree(tree_scope, limit_depth = nil)
|
20
20
|
limited_scope = limit_depth ? tree_scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}") : tree_scope
|
@@ -33,4 +33,4 @@ module ClosureTree
|
|
33
33
|
tree
|
34
34
|
end
|
35
35
|
end
|
36
|
-
end
|
36
|
+
end
|
@@ -67,7 +67,7 @@ module ClosureTree
|
|
67
67
|
delete_hierarchy_references unless (defined? @was_new_record) && @was_new_record
|
68
68
|
hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
|
69
69
|
unless root?
|
70
|
-
_ct.connection.execute <<-SQL.
|
70
|
+
_ct.connection.execute <<-SQL.squish
|
71
71
|
INSERT INTO #{_ct.quoted_hierarchy_table_name}
|
72
72
|
(ancestor_id, descendant_id, generations)
|
73
73
|
SELECT x.ancestor_id, #{_ct.quote(_ct_id)}, x.generations + 1
|
@@ -94,7 +94,7 @@ module ClosureTree
|
|
94
94
|
# It shouldn't affect performance of postgresql.
|
95
95
|
# See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
|
96
96
|
# Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
|
97
|
-
_ct.connection.execute <<-SQL.
|
97
|
+
_ct.connection.execute <<-SQL.squish
|
98
98
|
DELETE FROM #{_ct.quoted_hierarchy_table_name}
|
99
99
|
WHERE descendant_id IN (
|
100
100
|
SELECT DISTINCT descendant_id
|
@@ -112,11 +112,28 @@ module ClosureTree
|
|
112
112
|
# Note that the hierarchy table will be truncated.
|
113
113
|
def rebuild!
|
114
114
|
_ct.with_advisory_lock do
|
115
|
-
|
115
|
+
cleanup!
|
116
116
|
roots.find_each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
|
117
117
|
end
|
118
118
|
nil
|
119
119
|
end
|
120
|
+
|
121
|
+
def cleanup!
|
122
|
+
hierarchy_table = hierarchy_class.arel_table
|
123
|
+
|
124
|
+
[:descendant_id, :ancestor_id].each do |foreign_key|
|
125
|
+
alias_name = foreign_key.to_s.split('_').first + "s"
|
126
|
+
alias_table = Arel::Table.new(table_name).alias(alias_name)
|
127
|
+
arel_join = hierarchy_table.join(alias_table, Arel::Nodes::OuterJoin)
|
128
|
+
.on(alias_table[primary_key].eq(hierarchy_table[foreign_key]))
|
129
|
+
.join_sources
|
130
|
+
|
131
|
+
lonely_childs = hierarchy_class.joins(arel_join).where(alias_table[primary_key].eq(nil))
|
132
|
+
ids = lonely_childs.pluck(foreign_key)
|
133
|
+
|
134
|
+
hierarchy_class.where(hierarchy_table[foreign_key].in(ids)).delete_all
|
135
|
+
end
|
136
|
+
end
|
120
137
|
end
|
121
138
|
end
|
122
139
|
end
|
data/lib/closure_tree/model.rb
CHANGED
@@ -6,7 +6,7 @@ module ClosureTree
|
|
6
6
|
|
7
7
|
included do
|
8
8
|
|
9
|
-
belongs_to :parent, nil,
|
9
|
+
belongs_to :parent, nil, **_ct.belongs_to_with_optional_option(
|
10
10
|
class_name: _ct.model_class.to_s,
|
11
11
|
foreign_key: _ct.parent_column_name,
|
12
12
|
inverse_of: :children,
|
@@ -15,11 +15,11 @@ module ClosureTree
|
|
15
15
|
|
16
16
|
order_by_generations = -> { Arel.sql("#{_ct.quoted_hierarchy_table_name}.generations ASC") }
|
17
17
|
|
18
|
-
has_many :children, *_ct.
|
18
|
+
has_many :children, *_ct.has_many_order_with_option, **{
|
19
19
|
class_name: _ct.model_class.to_s,
|
20
20
|
foreign_key: _ct.parent_column_name,
|
21
21
|
dependent: _ct.options[:dependent],
|
22
|
-
inverse_of: :parent
|
22
|
+
inverse_of: :parent } do
|
23
23
|
# We have to redefine hash_tree because the activerecord relation is already scoped to parent_id.
|
24
24
|
def hash_tree(options = {})
|
25
25
|
# we want limit_depth + 1 because we don't do self_and_descendants.
|
@@ -28,25 +28,21 @@ module ClosureTree
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
-
has_many :ancestor_hierarchies, *_ct.
|
31
|
+
has_many :ancestor_hierarchies, *_ct.has_many_order_without_option(order_by_generations),
|
32
32
|
class_name: _ct.hierarchy_class_name,
|
33
|
-
foreign_key: 'descendant_id'
|
34
|
-
order: order_by_generations)
|
33
|
+
foreign_key: 'descendant_id'
|
35
34
|
|
36
|
-
has_many :self_and_ancestors, *_ct.
|
35
|
+
has_many :self_and_ancestors, *_ct.has_many_order_without_option(order_by_generations),
|
37
36
|
through: :ancestor_hierarchies,
|
38
|
-
source: :ancestor
|
39
|
-
order: order_by_generations)
|
37
|
+
source: :ancestor
|
40
38
|
|
41
|
-
has_many :descendant_hierarchies, *_ct.
|
39
|
+
has_many :descendant_hierarchies, *_ct.has_many_order_without_option(order_by_generations),
|
42
40
|
class_name: _ct.hierarchy_class_name,
|
43
|
-
foreign_key: 'ancestor_id'
|
44
|
-
order: order_by_generations)
|
41
|
+
foreign_key: 'ancestor_id'
|
45
42
|
|
46
|
-
has_many :self_and_descendants, *_ct.
|
43
|
+
has_many :self_and_descendants, *_ct.has_many_order_with_option(order_by_generations),
|
47
44
|
through: :descendant_hierarchies,
|
48
|
-
source: :descendant
|
49
|
-
order: order_by_generations)
|
45
|
+
source: :descendant
|
50
46
|
end
|
51
47
|
|
52
48
|
# Delegate to the Support instance on the class:
|
@@ -51,7 +51,7 @@ module ClosureTree
|
|
51
51
|
|
52
52
|
# If node is nil, order the whole tree.
|
53
53
|
def _ct_sum_order_by(node = nil)
|
54
|
-
stats_sql = <<-SQL.
|
54
|
+
stats_sql = <<-SQL.squish
|
55
55
|
SELECT
|
56
56
|
count(*) as total_descendants,
|
57
57
|
max(generations) as max_depth
|
@@ -74,7 +74,7 @@ module ClosureTree
|
|
74
74
|
raise ClosureTree::RootOrderingDisabledError.new("Root ordering is disabled on this model")
|
75
75
|
end
|
76
76
|
|
77
|
-
join_sql = <<-SQL.
|
77
|
+
join_sql = <<-SQL.squish
|
78
78
|
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
|
79
79
|
ON anc_hier.descendant_id = #{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}
|
80
80
|
JOIN #{_ct.quoted_table_name} anc
|
@@ -21,7 +21,7 @@ module ClosureTree
|
|
21
21
|
""
|
22
22
|
end
|
23
23
|
connection.execute 'SET @i = 0'
|
24
|
-
connection.execute <<-SQL.
|
24
|
+
connection.execute <<-SQL.squish
|
25
25
|
UPDATE #{quoted_table_name}
|
26
26
|
SET #{quoted_order_column} = (@i := @i + 1) + #{minimum_sort_order_value.to_i - 1}
|
27
27
|
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}
|
@@ -38,7 +38,7 @@ module ClosureTree
|
|
38
38
|
else
|
39
39
|
""
|
40
40
|
end
|
41
|
-
connection.execute <<-SQL.
|
41
|
+
connection.execute <<-SQL.squish
|
42
42
|
UPDATE #{quoted_table_name}
|
43
43
|
SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i - 1}
|
44
44
|
FROM (
|
data/lib/closure_tree/support.rb
CHANGED
@@ -32,13 +32,15 @@ module ClosureTree
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def hierarchy_class_for_model
|
35
|
-
|
35
|
+
parent_class = ActiveSupport::VERSION::MAJOR >= 6 ? model_class.module_parent : model_class.parent
|
36
|
+
hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(model_class.superclass))
|
36
37
|
use_attr_accessible = use_attr_accessible?
|
37
38
|
include_forbidden_attributes_protection = include_forbidden_attributes_protection?
|
38
|
-
|
39
|
+
model_class_name = model_class.to_s
|
40
|
+
hierarchy_class.class_eval do
|
39
41
|
include ActiveModel::ForbiddenAttributesProtection if include_forbidden_attributes_protection
|
40
|
-
belongs_to :ancestor, :
|
41
|
-
belongs_to :descendant, :
|
42
|
+
belongs_to :ancestor, class_name: model_class_name
|
43
|
+
belongs_to :descendant, class_name: model_class_name
|
42
44
|
attr_accessible :ancestor, :descendant, :generations if use_attr_accessible
|
43
45
|
def ==(other)
|
44
46
|
self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
|
@@ -47,7 +49,7 @@ module ClosureTree
|
|
47
49
|
def hash
|
48
50
|
ancestor_id.hash << 31 ^ descendant_id.hash
|
49
51
|
end
|
50
|
-
|
52
|
+
end
|
51
53
|
hierarchy_class.table_name = hierarchy_table_name
|
52
54
|
hierarchy_class
|
53
55
|
end
|
@@ -78,20 +80,20 @@ module ClosureTree
|
|
78
80
|
end
|
79
81
|
|
80
82
|
def belongs_to_with_optional_option(opts)
|
81
|
-
|
83
|
+
ActiveRecord::VERSION::MAJOR < 5 ? opts.except(:optional) : opts
|
82
84
|
end
|
83
85
|
|
84
86
|
# lambda-ize the order, but don't apply the default order_option
|
85
|
-
def
|
86
|
-
[lambda { order(
|
87
|
+
def has_many_order_without_option(order_by_opt)
|
88
|
+
[lambda { order(order_by_opt.call) }]
|
87
89
|
end
|
88
90
|
|
89
|
-
def
|
90
|
-
order_options = [
|
91
|
+
def has_many_order_with_option(order_by_opt=nil)
|
92
|
+
order_options = [order_by_opt, order_by].compact
|
91
93
|
[lambda {
|
92
94
|
order_options = order_options.map { |o| o.is_a?(Proc) ? o.call : o }
|
93
95
|
order(order_options)
|
94
|
-
}
|
96
|
+
}]
|
95
97
|
end
|
96
98
|
|
97
99
|
def ids_from(scope)
|
data/lib/closure_tree/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: closure_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 7.
|
4
|
+
version: 7.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew McEachen
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-10-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pg
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: rspec-instafail
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,6 +136,34 @@ dependencies:
|
|
122
136
|
- - ">="
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: sqlite3
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: simplecov
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
125
167
|
- !ruby/object:Gem::Dependency
|
126
168
|
name: timecop
|
127
169
|
requirement: !ruby/object:Gem::Requirement
|
@@ -143,9 +185,9 @@ executables: []
|
|
143
185
|
extensions: []
|
144
186
|
extra_rdoc_files: []
|
145
187
|
files:
|
188
|
+
- ".github/workflows/ci.yml"
|
146
189
|
- ".gitignore"
|
147
190
|
- ".rspec"
|
148
|
-
- ".travis.yml"
|
149
191
|
- ".yardopts"
|
150
192
|
- Appraisals
|
151
193
|
- CHANGELOG.md
|
@@ -153,6 +195,10 @@ files:
|
|
153
195
|
- MIT-LICENSE
|
154
196
|
- README.md
|
155
197
|
- Rakefile
|
198
|
+
- _config.yml
|
199
|
+
- bin/appraisal
|
200
|
+
- bin/rake
|
201
|
+
- bin/rspec
|
156
202
|
- closure_tree.gemspec
|
157
203
|
- lib/closure_tree.rb
|
158
204
|
- lib/closure_tree/active_record_support.rb
|
@@ -183,7 +229,7 @@ homepage: http://mceachen.github.io/closure_tree/
|
|
183
229
|
licenses:
|
184
230
|
- MIT
|
185
231
|
metadata: {}
|
186
|
-
post_install_message:
|
232
|
+
post_install_message:
|
187
233
|
rdoc_options: []
|
188
234
|
require_paths:
|
189
235
|
- lib
|
@@ -198,9 +244,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
198
244
|
- !ruby/object:Gem::Version
|
199
245
|
version: '0'
|
200
246
|
requirements: []
|
201
|
-
|
202
|
-
|
203
|
-
signing_key:
|
247
|
+
rubygems_version: 3.0.3
|
248
|
+
signing_key:
|
204
249
|
specification_version: 4
|
205
250
|
summary: Easily and efficiently make your ActiveRecord model support hierarchies
|
206
251
|
test_files: []
|
data/.travis.yml
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
cache: bundler
|
2
|
-
sudo: false
|
3
|
-
language: ruby
|
4
|
-
rvm:
|
5
|
-
- 2.6.0
|
6
|
-
- 2.5.1
|
7
|
-
- 2.4.4
|
8
|
-
- 2.3.6
|
9
|
-
- 2.2.10
|
10
|
-
|
11
|
-
gemfile:
|
12
|
-
- gemfiles/activerecord_5.2.gemfile
|
13
|
-
- gemfiles/activerecord_5.1.gemfile
|
14
|
-
- gemfiles/activerecord_5.0.gemfile
|
15
|
-
- gemfiles/activerecord_4.2.gemfile
|
16
|
-
- gemfiles/activerecord_edge.gemfile
|
17
|
-
|
18
|
-
env:
|
19
|
-
- DB=sqlite
|
20
|
-
- DB=mysql
|
21
|
-
- DB=postgresql
|
22
|
-
|
23
|
-
script: WITH_ADVISORY_LOCK_PREFIX=$TRAVIS_JOB_ID bundle exec rake --trace spec:all
|
24
|
-
|
25
|
-
matrix:
|
26
|
-
allow_failures:
|
27
|
-
- gemfile: gemfiles/activerecord_edge.gemfile
|
28
|
-
- rvm: jruby-head
|
29
|
-
- rvm: rbx
|
30
|
-
- rvm: 2.6.0
|