Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: 'bundler'
directory: '/'
open-pull-requests-limit: 10
schedule:
interval: 'weekly'
labels:
- "dependencies"
48 changes: 13 additions & 35 deletions .github/workflows/ci.yml → .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
---
name: CI
name: Tests

on:
- push
- pull_request
push:
branches: [ "master" ]
pull_request:
branches: [ "**" ]

jobs:
rspec:
test:
runs-on: ubuntu-20.04

services:
postgres:
image: 'postgres:13'
ports: ['5432:5432']
image: "postgres:13"
ports: ["5432:5432"]
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: closure_tree
Expand All @@ -26,41 +28,17 @@ jobs:
fail-fast: false
matrix:
ruby:
- '3.0'
- '2.7'
- '2.6'
- '2.5'
- "3.2.5"
rails:
- activerecord_6.1
- activerecord_6.0
- activerecord_5.2
- activerecord_5.1
- activerecord_5.0
- activerecord_4.2
- activerecord_edge
- activerecord_7.0
adapter:
- sqlite3
- mysql2
- postgresql
exclude:
- ruby: '2.7'
rails: activerecord_4.2
- ruby: '3.0'
rails: activerecord_4.2
- ruby: '3.0'
rails: activerecord_5.0
- ruby: '3.0'
rails: activerecord_5.1
- ruby: '3.0'
rails: activerecord_5.2
- ruby: '2.5'
rails: activerecord_edge
- ruby: '2.6'
rails: activerecord_edge

steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Setup Ruby
uses: ruby/setup-ruby@v1
Expand All @@ -76,7 +54,7 @@ jobs:
run: |
if [ "${DB_ADAPTER}" = "mysql2" ]; then
sudo systemctl start mysql.service
mysql -u root -proot -e 'create database closure_tree;'
mysql -u root -proot -e "create database closure_tree;"
fi

- name: Bundle
Expand All @@ -95,4 +73,4 @@ jobs:
DB_ADAPTER: ${{ matrix.adapter }}
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }}
run: bin/rake --trace spec:all
run: bin/rake --trace spec:all
15 changes: 15 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ appraise 'activerecord-6.1' do
end
end

appraise 'activerecord-7.0' do
gem 'activerecord', '~> 7.0'
platforms :ruby do
gem 'mysql2'
gem 'pg'
gem 'sqlite3'
end

platforms :jruby do
gem 'activerecord-jdbcmysql-adapter'
gem 'activerecord-jdbcpostgresql-adapter'
gem 'activerecord-jdbcsqlite3-adapter'
end
end

appraise 'activerecord-edge' do
gem 'activerecord', github: 'rails/rails'
platforms :ruby do
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# OpsLevel fork of Closure Tree

[![Overall](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fapp.opslevel.com%2Fapi%2Fservice_level%2F8bksYN3Yj8yRfI1LKJNLpqXqzpXSPdvGAkJKUXqYMIA)](https://app.opslevel.com/services/closure_tree/maturity-report)
[![🔐 Security](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fapp.opslevel.com%2Fapi%2Fservice_level%2F8bksYN3Yj8yRfI1LKJNLpqXqzpXSPdvGAkJKUXqYMIA%2Fsecurity)](https://app.opslevel.com/services/closure_tree/maturity-report)
[![🟢 Reliability](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fapp.opslevel.com%2Fapi%2Fservice_level%2F8bksYN3Yj8yRfI1LKJNLpqXqzpXSPdvGAkJKUXqYMIA%2Freliability)](https://app.opslevel.com/services/closure_tree/maturity-report)
[![🔍 Observability](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fapp.opslevel.com%2Fapi%2Fservice_level%2F8bksYN3Yj8yRfI1LKJNLpqXqzpXSPdvGAkJKUXqYMIA%2Fobservability)](https://app.opslevel.com/services/closure_tree/maturity-report)
[![📈 Quality](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fapp.opslevel.com%2Fapi%2Fservice_level%2F8bksYN3Yj8yRfI1LKJNLpqXqzpXSPdvGAkJKUXqYMIA%2Fquality)](https://app.opslevel.com/services/closure_tree/maturity-report)
[![📋 Ownership](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fapp.opslevel.com%2Fapi%2Fservice_level%2F8bksYN3Yj8yRfI1LKJNLpqXqzpXSPdvGAkJKUXqYMIA%2Fownership)](https://app.opslevel.com/services/closure_tree/maturity-report)
[![🛠 Misc](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fapp.opslevel.com%2Fapi%2Fservice_level%2F8bksYN3Yj8yRfI1LKJNLpqXqzpXSPdvGAkJKUXqYMIA%2Fmisc_2)](https://app.opslevel.com/services/closure_tree/maturity-report)

This fork is based on 7.4.0, the last stable gem version release (from Oct 2021). We then applied a patch from https://github.com/ClosureTree/closure_tree/pull/442, which allows us to support tenancy with determinstically ordered closure trees.

# Closure Tree

### Closure_tree lets your ActiveRecord models act as nodes in a [tree data structure](http://en.wikipedia.org/wiki/Tree_%28data_structure%29)
Expand Down
9 changes: 0 additions & 9 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,3 @@ namespace :spec do
task.pattern = 'spec/generators/*_spec.rb'
end
end

require 'github_changelog_generator/task'
GitHubChangelogGenerator::RakeTask.new :changelog do |config|
config.user = 'ClosureTree'
config.project = 'closure_tree'
config.issues = false
config.future_release = '5.2.0'
config.since_tag = 'v7.3.0'
end
21 changes: 21 additions & 0 deletions gemfiles/activerecord_7.0.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This file was generated by Appraisal

source "https://rubygems.org"

gem "bump", "~> 0.10.0"
gem "github_changelog_generator", "~> 1.16"
gem "activerecord", "~> 7.0"

platforms :ruby do
gem "mysql2"
gem "pg"
gem "sqlite3"
end

platforms :jruby do
gem "activerecord-jdbcmysql-adapter"
gem "activerecord-jdbcpostgresql-adapter"
gem "activerecord-jdbcsqlite3-adapter"
end

gemspec path: "../"
4 changes: 2 additions & 2 deletions lib/closure_tree/finders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def find_or_create_by_path(path, attributes = {})
return found if found

attrs = subpath.shift
_ct.with_advisory_lock do
_ct.with_advisory_lock! do
# shenanigans because children.create is bound to the superclass
# (in the case of polymorphism):
child = self.children.where(attrs).first || begin
Expand Down Expand Up @@ -159,7 +159,7 @@ def find_or_create_by_path(path, attributes = {})
attr_path = _ct.build_ancestry_attr_path(path, attributes)
find_by_path(attr_path) || begin
root_attrs = attr_path.shift
_ct.with_advisory_lock do
_ct.with_advisory_lock! do
# shenanigans because find_or_create can't infer that we want the same class as this:
# Note that roots will already be constrained to this subclass (in the case of polymorphism):
root = roots.where(root_attrs).first || _ct.create!(self, root_attrs)
Expand Down
4 changes: 3 additions & 1 deletion lib/closure_tree/has_closure_tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ def has_closure_tree(options = {})
:dont_order_roots,
:numeric_order,
:touch,
:with_advisory_lock
:with_advisory_lock,
:advisory_lock_timeout_seconds,
:order_belong_to
)

class_attribute :_ct
Expand Down
8 changes: 4 additions & 4 deletions lib/closure_tree/hierarchy_maintenance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def _ct_after_save
end

def _ct_before_destroy
_ct.with_advisory_lock do
_ct.with_advisory_lock! do
delete_hierarchy_references
if _ct.options[:dependent] == :nullify
self.class.find(self.id).children.find_each { |c| c.rebuild! }
Expand All @@ -63,7 +63,7 @@ def _ct_before_destroy
end

def rebuild!(called_by_rebuild = false)
_ct.with_advisory_lock do
_ct.with_advisory_lock! do
delete_hierarchy_references unless (defined? @was_new_record) && @was_new_record
hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
unless root?
Expand All @@ -89,7 +89,7 @@ def rebuild!(called_by_rebuild = false)
end

def delete_hierarchy_references
_ct.with_advisory_lock do
_ct.with_advisory_lock! do
# The crazy double-wrapped sub-subselect works around MySQL's limitation of subselects on the same table that is being mutated.
# It shouldn't affect performance of postgresql.
# See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
Expand All @@ -111,7 +111,7 @@ module ClassMethods
# Rebuilds the hierarchy table based on the parent_id column in the database.
# Note that the hierarchy table will be truncated.
def rebuild!
_ct.with_advisory_lock do
_ct.with_advisory_lock! do
cleanup!
roots.find_each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
end
Expand Down
4 changes: 4 additions & 0 deletions lib/closure_tree/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ def _ct_parent_id
read_attribute(_ct.parent_column_sym)
end

def _ct_belong_to_id
read_attribute(_ct.belong_to_column_sym)
end

def _ct_quoted_parent_id
_ct.quoted_value(_ct_parent_id)
end
Expand Down
8 changes: 4 additions & 4 deletions lib/closure_tree/numeric_deterministic_ordering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ def _ct_reorder_prior_siblings_if_parent_changed
attribute_method = as_5_1 ? :attribute_before_last_save : :attribute_was

was_parent_id = public_send(attribute_method, _ct.parent_column_name)
_ct.reorder_with_parent_id(was_parent_id)
_ct.reorder_with_parent_id(parent_id: was_parent_id, belong_to_name: _ct.belong_to_column_sym, belong_to_id: _ct_belong_to_id)
end
end

def _ct_reorder_siblings(minimum_sort_order_value = nil)
_ct.reorder_with_parent_id(_ct_parent_id, minimum_sort_order_value)
_ct.reorder_with_parent_id(parent_id: _ct_parent_id, minimum_sort_order_value: minimum_sort_order_value, belong_to_name: _ct.belong_to_column_sym, belong_to_id: _ct_belong_to_id)
reload unless destroyed?
end

def _ct_reorder_children(minimum_sort_order_value = nil)
_ct.reorder_with_parent_id(_ct_id, minimum_sort_order_value)
_ct.reorder_with_parent_id(parent_id: _ct_id, minimum_sort_order_value: minimum_sort_order_value)
end

def self_and_descendants_preordered
Expand Down Expand Up @@ -125,7 +125,7 @@ def add_sibling(sibling, add_after = true)
# Make sure self isn't dirty, because we're going to call reload:
save

_ct.with_advisory_lock do
_ct.with_advisory_lock! do
prior_sibling_parent = sibling.parent
reorder_from_value = if prior_sibling_parent == self.parent
[self.order_value, sibling.order_value].compact.min
Expand Down
21 changes: 16 additions & 5 deletions lib/closure_tree/numeric_order_support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,48 @@ def self.adapter_for_connection(connection)
end

module MysqlAdapter
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
def reorder_with_parent_id(parent_id:, minimum_sort_order_value: nil, belong_to_name: nil, belong_to_id: nil)
return if parent_id.nil? && dont_order_roots
min_where = if minimum_sort_order_value
"AND #{quoted_order_column} >= #{minimum_sort_order_value}"
else
""
end
belong_to_scope = if parent_id.nil? && belong_to_id
"AND #{quoted_table_name}.#{belong_to_name} = #{belong_to_id}"
else
""
end
connection.execute 'SET @i = 0'
connection.execute <<-SQL.squish
UPDATE #{quoted_table_name}
SET #{quoted_order_column} = (@i := @i + 1) + #{minimum_sort_order_value.to_i - 1}
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where} #{belong_to_scope}
ORDER BY #{nulls_last_order_by}
SQL
end
end

module PostgreSQLAdapter
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
def reorder_with_parent_id(parent_id:, minimum_sort_order_value: nil, belong_to_name: nil, belong_to_id: nil)
return if parent_id.nil? && dont_order_roots
min_where = if minimum_sort_order_value
"AND #{quoted_order_column} >= #{minimum_sort_order_value}"
else
""
end
belong_to_scope = if parent_id.nil? && belong_to_id
"AND #{quoted_table_name}.#{belong_to_name} = #{belong_to_id}"
else
""
end
connection.execute <<-SQL.squish
UPDATE #{quoted_table_name}
SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i - 1}
FROM (
SELECT #{quoted_id_column_name} AS id, row_number() OVER(ORDER BY #{order_by}) AS seq
FROM #{quoted_table_name}
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where} #{belong_to_scope}
) AS t
WHERE #{quoted_table_name}.#{quoted_id_column_name} = t.id and
#{quoted_table_name}.#{quoted_order_column(false)} is distinct from t.seq + #{minimum_sort_order_value.to_i - 1}
Expand All @@ -57,14 +67,15 @@ def rows_updated(result)
end

module GenericAdapter
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
def reorder_with_parent_id(parent_id:, minimum_sort_order_value: nil, belong_to_name: nil, belong_to_id: nil)
return if parent_id.nil? && dont_order_roots
scope = model_class.
where(parent_column_sym => parent_id).
order(nulls_last_order_by)
if minimum_sort_order_value
scope = scope.where("#{quoted_order_column} >= #{minimum_sort_order_value}")
end
scope = scope.where(belong_to_name => belong_to_id) if belong_to_id
scope.each_with_index do |ea, idx|
ea.update_order_value(idx + minimum_sort_order_value.to_i)
end
Expand Down
5 changes: 3 additions & 2 deletions lib/closure_tree/support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def initialize(model_class, options)
:dependent => :nullify, # or :destroy or :delete_all -- see the README
:name_column => 'name',
:with_advisory_lock => true,
:advisory_lock_timeout_seconds => 15,
:numeric_order => false
}.merge(options)
raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
Expand Down Expand Up @@ -108,9 +109,9 @@ def where_eq(column_name, value)
end
end

def with_advisory_lock(&block)
def with_advisory_lock!(&block)
if options[:with_advisory_lock]
model_class.with_advisory_lock(advisory_lock_name) do
model_class.with_advisory_lock!(advisory_lock_name, advisory_lock_options) do
transaction { yield }
end
else
Expand Down
12 changes: 12 additions & 0 deletions lib/closure_tree/support_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ def advisory_lock_name
Digest::SHA1.hexdigest("ClosureTree::#{base_class.name}")[0..32]
end

def advisory_lock_options
{ timeout_seconds: options[:advisory_lock_timeout_seconds] }.compact
end

def quoted_table_name
connection.quote_table_name(table_name)
end
Expand Down Expand Up @@ -36,6 +40,14 @@ def parent_column_sym
parent_column_name.to_sym
end

def belong_to_column_name
options[:order_belong_to]
end

def belong_to_column_sym
belong_to_column_name&.to_sym
end

def name_column
options[:name_column]
end
Expand Down
Loading