closure_tree 7.0.0 → 7.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aad24f6acfed9da72a06c877919ef7a349680abb951a185307590b42484582e0
4
- data.tar.gz: 543f9482cd9497d99174358b6d342caa6c860608acc51f5989e5d780c1747f58
3
+ metadata.gz: 7465d4d3d8e4188cc0d64ffdc93e31df1f943ea3685d0aa36c1c3980065625bb
4
+ data.tar.gz: a48c396aa59ea4890ce2700d2dd340659485923714296df792698c5f97414c3c
5
5
  SHA512:
6
- metadata.gz: 69c034fe0ee0278b143758381412ca43663da441bd35a81b67d516d58defc5f19b79d83a28a9fea18984cb105b757b7028c9898cab34eaae98fccb56e7b0d71e
7
- data.tar.gz: 53346203044b6eb82b784bc715105b6a2a5029d8ab8c48665d1bb02bf667bff643ba0768c6dfa6ec476b03b7f5567e28846c8b6b2c7b17344f2d17227ca1e24c
6
+ metadata.gz: 25e8e406ea7dee123d49f02060339b49bf1272adf07c2bcb261a442c396c766fd0e646cc916a4dce08b832ffe2225bdb00f604d39f1a301825f26ad3f839595f
7
+ data.tar.gz: 5709b20dcd1d2fc78da96c7f9d884ee58c680beb5180c583396052038acacf9d81adfb5b7718160e05624db5027bc7f3286610b4d3a1cbfd6497672455b89432
@@ -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.rc1')
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.rc1
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-09-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
@@ -194,13 +240,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
194
240
  version: 2.0.0
195
241
  required_rubygems_version: !ruby/object:Gem::Requirement
196
242
  requirements:
197
- - - ">="
243
+ - - ">"
198
244
  - !ruby/object:Gem::Version
199
- version: '0'
245
+ version: 1.3.1
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