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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aad24f6acfed9da72a06c877919ef7a349680abb951a185307590b42484582e0
4
- data.tar.gz: 543f9482cd9497d99174358b6d342caa6c860608acc51f5989e5d780c1747f58
3
+ metadata.gz: e4ac0bb16bca6ba730b542b35639b067ad113e2c7010265cae6e0192425e1b82
4
+ data.tar.gz: 03004e7033f76268a1e96b3ee4a0c5d9066a4c716caab80c6082ddb60404b598
5
5
  SHA512:
6
- metadata.gz: 69c034fe0ee0278b143758381412ca43663da441bd35a81b67d516d58defc5f19b79d83a28a9fea18984cb105b757b7028c9898cab34eaae98fccb56e7b0d71e
7
- data.tar.gz: 53346203044b6eb82b784bc715105b6a2a5029d8ab8c48665d1bb02bf667bff643ba0768c6dfa6ec476b03b7f5567e28846c8b6b2c7b17344f2d17227ca1e24c
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
@@ -13,3 +13,5 @@ tmp/
13
13
  *.lock
14
14
  tmp/
15
15
  .ruby-*
16
+ *.iml
17
+ coverage/
data/.rspec CHANGED
@@ -1 +1 @@
1
- --format documentation --color --order random
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
@@ -1,3 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+
6
+ gem "bump", "~> 0.10.0"
7
+ gem "github_changelog_generator", "~> 1.16"
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
  [![Join the chat at https://gitter.im/closure_tree/Lobby](https://badges.gitter.im/closure_tree/Lobby.svg)](https://gitter.im/closure_tree/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
11
9
  [![Build Status](https://api.travis-ci.org/ClosureTree/closure_tree.svg?branch=master)](http://travis-ci.org/ClosureTree/closure_tree)
12
10
  [![Gem Version](https://badge.fury.io/rb/closure_tree.svg)](https://badge.fury.io/rb/closure_tree)
13
- [![Dependency Status](https://gemnasium.com/badges/github.com/ClosureTree/closure_tree.svg)](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, and 5.1, with Ruby 2.2 and 2.3__
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 ancestor is in the given list.
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.2, 2.3
669
- * ActiveRecord 4.2, 5.0, and 5.1
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
- begin
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
- Bundler::GemHelper.install_tasks
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 :default => :spec
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 = 'bundle exec 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
- $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
- require 'closure_tree/version'
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.
@@ -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.strip_heredoc)
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.strip_heredoc)
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.strip_heredoc)
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.strip_heredoc)
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.strip_heredoc
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
- end
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.strip_heredoc
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.strip_heredoc
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
- hierarchy_class.delete_all # not destroy_all -- we just want a simple truncate.
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
@@ -6,7 +6,7 @@ module ClosureTree
6
6
 
7
7
  included do
8
8
 
9
- belongs_to :parent, nil, *_ct.belongs_to_with_optional_option(
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.has_many_with_order_option(
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) do
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.has_many_without_order_option(
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.has_many_without_order_option(
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.has_many_without_order_option(
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.has_many_with_order_option(
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.strip_heredoc
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.strip_heredoc
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.strip_heredoc
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.strip_heredoc
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 (
@@ -32,13 +32,15 @@ module ClosureTree
32
32
  end
33
33
 
34
34
  def hierarchy_class_for_model
35
- hierarchy_class = model_class.parent.const_set(short_hierarchy_class_name, Class.new(ActiveRecord::Base))
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
- hierarchy_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
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, :class_name => "#{model_class}"
41
- belongs_to :descendant, :class_name => "#{model_class}"
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
- RUBY
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
- [ActiveRecord::VERSION::MAJOR < 5 ? opts.except(:optional) : opts]
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 has_many_without_order_option(opts)
86
- [lambda { order(opts[:order].call) }, opts.except(: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 has_many_with_order_option(opts)
90
- order_options = [opts[:order], order_by].compact
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
- }, opts.except(:order)]
96
+ }]
95
97
  end
96
98
 
97
99
  def ids_from(scope)
@@ -80,7 +80,7 @@ module ClosureTree
80
80
  end
81
81
 
82
82
  def nulls_last_order_by
83
- "-#{quoted_order_column} #{order_by_order(reverse = true)}"
83
+ Arel.sql "-#{quoted_order_column} #{order_by_order(true)}"
84
84
  end
85
85
 
86
86
  def order_by_order(reverse = false)
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = Gem::Version.new('7.0.0')
2
+ VERSION = Gem::Version.new('7.4.0')
3
3
  end
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.0.0
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: 2018-09-23 00:00:00.000000000 Z
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
- rubyforge_project:
202
- rubygems_version: 2.7.6
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