acts_as_nested_interval 0.0.1
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.
- data/MIT-LICENSE +20 -0
- data/README.md +171 -0
- data/Rakefile +38 -0
- data/lib/acts_as_nested_interval/core_ext/integer.rb +21 -0
- data/lib/acts_as_nested_interval/version.rb +3 -0
- data/lib/acts_as_nested_interval.rb +293 -0
- data/lib/tasks/acts_as_nested_interval_tasks.rake +4 -0
- data/test/acts_as_nested_interval_test.rb +184 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/region.rb +3 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config/application.rb +56 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/migrate/20120302143528_create_regions.rb +15 -0
- data/test/dummy/db/schema.rb +28 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +129 -0
- data/test/dummy/log/test.log +13942 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/dummy/test/fixtures/regions.yml +7 -0
- data/test/dummy/test/unit/region_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +154 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
# ActsAsNestedInterval
|
2
|
+
|
3
|
+
## About
|
4
|
+
|
5
|
+
* Pythonic's acts_as_nested_interval updated to Rails 3 and gemified.
|
6
|
+
* This: https://github.com/clyfe/acts_as_nested_interval
|
7
|
+
* Original: https://github.com/pythonic/acts_as_nested_interval
|
8
|
+
* Acknowledgement: http://arxiv.org/html/cs.DB/0401014 by Vadim Tropashko.
|
9
|
+
|
10
|
+
This act implements a nested-interval tree. You can find all descendants or all
|
11
|
+
ancestors with just one select query. You can insert and delete records without
|
12
|
+
a full table update.
|
13
|
+
|
14
|
+
## Install
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
# add to Gemfile
|
18
|
+
gem 'acts_as_nested_interval'
|
19
|
+
```
|
20
|
+
|
21
|
+
```sh
|
22
|
+
# install
|
23
|
+
bundle install
|
24
|
+
```
|
25
|
+
|
26
|
+
* requires a `parent_id` foreign key column, and `lftp` and `lftq` integer columns.
|
27
|
+
* if your database does not support stored procedures then you also need `rgtp` and `rgtq` integer columns
|
28
|
+
* if your database does not support functional indexes then you also need a `rgt` float column
|
29
|
+
* the `lft` float column is optional
|
30
|
+
|
31
|
+
Example:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
create_table :regions do |t|
|
35
|
+
t.integer :parent_id
|
36
|
+
t.integer :lftp, :null => false
|
37
|
+
t.integer :lftq, :null => false
|
38
|
+
t.integer :rgtp, :null => false
|
39
|
+
t.integer :rgtq, :null => false
|
40
|
+
t.float :lft, :null => false
|
41
|
+
t.float :rgt, :null => false
|
42
|
+
t.string :name, :null => false
|
43
|
+
end
|
44
|
+
add_index :regions, :parent_id
|
45
|
+
add_index :regions, :lftp
|
46
|
+
add_index :regions, :lftq
|
47
|
+
add_index :regions, :lft
|
48
|
+
add_index :regions, :rgt
|
49
|
+
```
|
50
|
+
|
51
|
+
## Usage
|
52
|
+
|
53
|
+
The size of the tree is limited by the precision of the integer and floating
|
54
|
+
point data types in the database.
|
55
|
+
|
56
|
+
This act provides these named scopes:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
Region.roots # returns roots of tree.
|
60
|
+
Region.preorder # returns records for preorder traversal.
|
61
|
+
```
|
62
|
+
|
63
|
+
This act provides these instance methods:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
Region.parent # returns parent of record.
|
67
|
+
Region.children # returns children of record.
|
68
|
+
Region.ancestors # returns scoped ancestors of record.
|
69
|
+
Region.descendants # returns scoped descendants of record.
|
70
|
+
Region.depth # returns depth of record.
|
71
|
+
```
|
72
|
+
|
73
|
+
Example:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class Region < ActiveRecord::Base
|
77
|
+
acts_as_nested_interval
|
78
|
+
end
|
79
|
+
|
80
|
+
earth = Region.create :name => "Earth"
|
81
|
+
oceania = Region.create :name => "Oceania", :parent => earth
|
82
|
+
australia = Region.create :name => "Australia", :parent => oceania
|
83
|
+
new_zealand = Region.new :name => "New Zealand"
|
84
|
+
oceania.children << new_zealand
|
85
|
+
earth.descendants # => [oceania, australia, new_zealand]
|
86
|
+
earth.children # => [oceania]
|
87
|
+
oceania.children # => [australia, new_zealand]
|
88
|
+
oceania.depth # => 1
|
89
|
+
australia.parent # => oceania
|
90
|
+
new_zealand.ancestors # => [earth, oceania]
|
91
|
+
Region.roots # => [earth]
|
92
|
+
```
|
93
|
+
|
94
|
+
## How it works
|
95
|
+
|
96
|
+
The `mediant` of two rationals is the rational with the sum of the two
|
97
|
+
numerators for the numerator, and the sum of the two denominators for the
|
98
|
+
denominator (where the denominators are positive). The mediant is numerically
|
99
|
+
between the two rationals.
|
100
|
+
Example: `3 / 5` is the mediant of `1 / 2` and `2 / 3`, and `1 / 2 < 3 / 5 < 2 / 3`.
|
101
|
+
|
102
|
+
Each record `covers` a half-open interval `(lftp / lftq, rgtp / rgtq]`. The tree
|
103
|
+
root covers `(0 / 1, 1 / 1]`. The first child of a record covers interval
|
104
|
+
`(mediant{lftp / lftq, rgtp / rgtq}, rgtp / rgtq]`; the next child covers the interval
|
105
|
+
`(mediant{lftp / lftq, mediant{lftp / lftq, rgtp / rgtq}}, mediant{lftp / lftq, rgtp / rgtq}]`.
|
106
|
+
|
107
|
+
With this construction each lftp and lftq are relatively prime and the identity
|
108
|
+
`lftq * rgtp = 1 + lftp * rgtq holds`.
|
109
|
+
|
110
|
+
Example:
|
111
|
+
|
112
|
+
0/1 1/2 3/5 2/3 1/1
|
113
|
+
earth (-----------------------------------------------------------]
|
114
|
+
oceania (-----------------------------]
|
115
|
+
australia (-------------------]
|
116
|
+
new zealand (---]
|
117
|
+
|
118
|
+
The descendants of a record are those records that cover subintervals of the
|
119
|
+
interval covered by the record, and the ancestors are those records that cover
|
120
|
+
superintervals.
|
121
|
+
|
122
|
+
Only the left end of an interval needs to be stored, since the right end can be
|
123
|
+
calculated (with special exceptions) using the above identity:
|
124
|
+
|
125
|
+
rgtp := x
|
126
|
+
rgtq := (x * lftq - 1) / lftp
|
127
|
+
|
128
|
+
where x is the inverse of lftq modulo lftp.
|
129
|
+
|
130
|
+
Similarly, the left end of the interval covered by the parent of a record can
|
131
|
+
be calculated using the above identity:
|
132
|
+
|
133
|
+
lftp := (x * lftp - 1) / lftq
|
134
|
+
lftq := x
|
135
|
+
|
136
|
+
where x is the inverse of lftp modulo lftq.
|
137
|
+
|
138
|
+
## Moving nodes
|
139
|
+
|
140
|
+
To move a record from old.lftp, old.lftq to new.lftp, new.lftq, apply this
|
141
|
+
linear transform to lftp, lftq of all descendants:
|
142
|
+
|
143
|
+
lftp := (old.lftq * new.rgtp - old.rgtq * new.lftp) * lftp
|
144
|
+
+ (old.rgtp * new.lftp - old.lftp * new.rgtp) * lftq
|
145
|
+
lftq := (old.lftq * new.rgtq - old.rgtq * new.lftq) * lftp
|
146
|
+
+ (old.rgtp * new.lftq - old.lftp * new.rgtq) * lftq
|
147
|
+
|
148
|
+
You should acquire a table lock before moving a record.
|
149
|
+
|
150
|
+
Example:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
pacific = Region.create :name => "Pacific", :parent => earth
|
154
|
+
oceania.parent = pacific
|
155
|
+
oceania.save!
|
156
|
+
```
|
157
|
+
|
158
|
+
## Migrating from acts_as_tree
|
159
|
+
|
160
|
+
If you come from acts_as_tree or another system where you only have a parent_id,
|
161
|
+
to rebuild the intervals based on `acts_as_nested_set`, after you migrated the DB
|
162
|
+
and created the columns required by `acts_as_nested_set` run:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
Region.rebuild_nested_interval_tree!
|
166
|
+
```
|
167
|
+
|
168
|
+
NOTE! About `rebuild_nested_interval_tree!`:
|
169
|
+
* zeroes all your tree intervals before recomputing them!
|
170
|
+
* does a lot of N+1 queries of type `record.parent` and not only.
|
171
|
+
This might change once the AR identity_map is finished.
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'ActsAsNestedInterval'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
Bundler::GemHelper.install_tasks
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
|
30
|
+
Rake::TestTask.new(:test) do |t|
|
31
|
+
t.libs << 'lib'
|
32
|
+
t.libs << 'test'
|
33
|
+
t.pattern = 'test/**/*_test.rb'
|
34
|
+
t.verbose = false
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
task :default => :test
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright (c) 2007, 2008 Pythonic Pty Ltd
|
2
|
+
# http://www.pythonic.com.au/
|
3
|
+
|
4
|
+
class Integer
|
5
|
+
# Returns modular multiplicative inverse.
|
6
|
+
# Examples:
|
7
|
+
# 2.inverse(7) # => 4
|
8
|
+
# 4.inverse(7) # => 2
|
9
|
+
def inverse(m)
|
10
|
+
u, v = m, self
|
11
|
+
x, y = 0, 1
|
12
|
+
while v != 0
|
13
|
+
q, r = u.divmod(v)
|
14
|
+
x, y = y, x - q * y
|
15
|
+
u, v = v, r
|
16
|
+
end
|
17
|
+
if u.abs == 1
|
18
|
+
x < 0 ? x + m : x
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# Copyright (c) 2007, 2008 Pythonic Pty Ltd
|
2
|
+
# http://www.pythonic.com.au/
|
3
|
+
|
4
|
+
# Copyright (c) 2012 Nicolae Claudius
|
5
|
+
# https://github.com/clyfe
|
6
|
+
|
7
|
+
require 'acts_as_nested_interval/version'
|
8
|
+
require 'acts_as_nested_interval/core_ext/integer'
|
9
|
+
|
10
|
+
module ActsAsNestedInterval
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
# This act implements a nested-interval tree. You can find all descendants
|
14
|
+
# or all ancestors with just one select query. You can insert and delete
|
15
|
+
# records without a full table update.
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
# The +options+ hash can include:
|
19
|
+
# * <tt>:foreign_key</tt> -- the self-reference foreign key column name (default :parent_id).
|
20
|
+
# * <tt>:scope_columns</tt> -- an array of columns to scope independent trees.
|
21
|
+
# * <tt>:lft_index</tt> -- whether to use functional index for lft (default false).
|
22
|
+
# * <tt>:virtual_root</tt> -- whether to compute root's interval as in an upper root (default false)
|
23
|
+
def acts_as_nested_interval(options = {})
|
24
|
+
cattr_accessor :nested_interval_foreign_key
|
25
|
+
cattr_accessor :nested_interval_scope_columns
|
26
|
+
cattr_accessor :nested_interval_lft_index
|
27
|
+
|
28
|
+
cattr_accessor :virtual_root
|
29
|
+
self.virtual_root = !!options[:virtual_root]
|
30
|
+
|
31
|
+
self.nested_interval_foreign_key = options[:foreign_key] || :parent_id
|
32
|
+
self.nested_interval_scope_columns = Array(options[:scope_columns])
|
33
|
+
self.nested_interval_lft_index = options[:lft_index]
|
34
|
+
|
35
|
+
belongs_to :parent, :class_name => name, :foreign_key => nested_interval_foreign_key
|
36
|
+
has_many :children, :class_name => name, :foreign_key => nested_interval_foreign_key, :dependent => :destroy
|
37
|
+
scope :roots, where(nested_interval_foreign_key => nil)
|
38
|
+
|
39
|
+
if columns_hash["rgt"]
|
40
|
+
scope :preorder, order('rgt DESC, lftp ASC')
|
41
|
+
elsif columns_hash["rgtp"] && columns_hash["rgtq"]
|
42
|
+
scope :preorder, order('1.0 * rgtp / rgtq DESC, lftp ASC')
|
43
|
+
else
|
44
|
+
scope :preorder, order('nested_interval_rgt(lftp, lftq) DESC, lftp ASC')
|
45
|
+
end
|
46
|
+
|
47
|
+
class_eval do
|
48
|
+
include ActsAsNestedInterval::NodeInstanceMethods
|
49
|
+
|
50
|
+
# TODO make into before filters
|
51
|
+
before_create :create_nested_interval
|
52
|
+
before_destroy :destroy_nested_interval
|
53
|
+
before_update :update_nested_interval
|
54
|
+
|
55
|
+
if columns_hash["lft"]
|
56
|
+
def descendants
|
57
|
+
quoted_table_name = self.class.quoted_table_name
|
58
|
+
nested_interval_scope.where <<-SQL
|
59
|
+
#{lftp} < #{quoted_table_name}.lftp AND
|
60
|
+
#{quoted_table_name}.lft BETWEEN #{1.0 * lftp / lftq} AND #{1.0 * rgtp / rgtq}
|
61
|
+
SQL
|
62
|
+
end
|
63
|
+
elsif nested_interval_lft_index
|
64
|
+
def descendants
|
65
|
+
quoted_table_name = self.class.quoted_table_name
|
66
|
+
nested_interval_scope.where <<-SQL
|
67
|
+
#{lftp} < #{quoted_table_name}.lftp AND
|
68
|
+
1.0 * #{quoted_table_name}.lftp / #{quoted_table_name}.lftq BETWEEN
|
69
|
+
#{1.0 * lftp / lftq} AND
|
70
|
+
#{1.0 * rgtp / rgtq}
|
71
|
+
SQL
|
72
|
+
end
|
73
|
+
elsif connection.adapter_name == "MySQL"
|
74
|
+
def descendants
|
75
|
+
quoted_table_name = self.class.quoted_table_name
|
76
|
+
nested_interval_scope.where <<-SQL
|
77
|
+
( #{quoted_table_name}.lftp != #{rgtp} OR
|
78
|
+
#{quoted_table_name}.lftq != #{rgtq}
|
79
|
+
) AND
|
80
|
+
#{quoted_table_name}.lftp BETWEEN
|
81
|
+
1 + #{quoted_table_name}.lftq * #{lftp} DIV #{lftq} AND
|
82
|
+
#{quoted_table_name}.lftq * #{rgtp} DIV #{rgtq}
|
83
|
+
SQL
|
84
|
+
end
|
85
|
+
else
|
86
|
+
def descendants
|
87
|
+
quoted_table_name = self.class.quoted_table_name
|
88
|
+
nested_interval_scope.where <<-SQL
|
89
|
+
( #{quoted_table_name}.lftp != #{rgtp} OR
|
90
|
+
#{quoted_table_name}.lftq != #{rgtq}
|
91
|
+
) AND
|
92
|
+
#{quoted_table_name}.lftp BETWEEN
|
93
|
+
1 + #{quoted_table_name}.lftq * CAST(#{lftp} AS BIGINT) / #{lftq} AND
|
94
|
+
#{quoted_table_name}.lftq * CAST(#{rgtp} AS BIGINT) / #{rgtq}
|
95
|
+
SQL
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.rebuild_nested_interval_tree!
|
100
|
+
skip_callback :update, :before, :update_nested_interval
|
101
|
+
skip_callback :update, :before, :sync_childre
|
102
|
+
scope :roots, where(nested_interval_foreign_key => nil).where("#{quoted_table_name}.lftq > 0")
|
103
|
+
update_all(:lftp => 0, :lftq => 0)
|
104
|
+
update_all(:rgtp => 0) if columns_hash["rgtp"]
|
105
|
+
update_all(:rgtq => 0) if columns_hash["rgtq"]
|
106
|
+
update_all(:lft => 0) if columns_hash["lft"]
|
107
|
+
update_all(:rgt => 0) if columns_hash["rgt"]
|
108
|
+
clear_cache!
|
109
|
+
|
110
|
+
scoped.find_each do |d|
|
111
|
+
begin
|
112
|
+
d.create_nested_interval
|
113
|
+
unless d.save
|
114
|
+
puts "WARNING #{d.name} did not save because #{d.errors.inspect}"
|
115
|
+
end
|
116
|
+
rescue => e
|
117
|
+
puts "WARNING #{d.name} exception because #{e.message}"
|
118
|
+
puts "WARNING lftq: #{d.lftq}, llftp: #{d.lftp}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
set_callback :update, :before, :update_nested_interval
|
123
|
+
set_callback :update, :before, :sync_childre
|
124
|
+
scope :roots, where(nested_interval_foreign_key => nil)
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
module NodeInstanceMethods
|
133
|
+
def set_nested_interval(lftp, lftq)
|
134
|
+
self.lftp, self.lftq = lftp, lftq
|
135
|
+
self.rgtp = rgtp if has_attribute?(:rgtp)
|
136
|
+
self.rgtq = rgtq if has_attribute?(:rgtq)
|
137
|
+
self.lft = lft if has_attribute?(:lft)
|
138
|
+
self.rgt = rgt if has_attribute?(:rgt)
|
139
|
+
end
|
140
|
+
|
141
|
+
def set_nested_interval_for_top
|
142
|
+
if self.class.virtual_root
|
143
|
+
set_nested_interval *next_root_lft
|
144
|
+
else
|
145
|
+
set_nested_interval 0, 1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Creates record.
|
150
|
+
def create_nested_interval
|
151
|
+
if read_attribute(nested_interval_foreign_key).nil?
|
152
|
+
set_nested_interval_for_top
|
153
|
+
else
|
154
|
+
set_nested_interval *parent.lock!.next_child_lft
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Destroys record.
|
159
|
+
def destroy_nested_interval
|
160
|
+
lock! rescue nil
|
161
|
+
end
|
162
|
+
|
163
|
+
def nested_interval_scope
|
164
|
+
conditions = {}
|
165
|
+
nested_interval_scope_columns.each do |column_name|
|
166
|
+
conditions[column_name] = send(column_name)
|
167
|
+
end
|
168
|
+
self.class.where conditions
|
169
|
+
end
|
170
|
+
|
171
|
+
# Updates record, updating descendants if parent association updated,
|
172
|
+
# in which case caller should first acquire table lock.
|
173
|
+
def update_nested_interval
|
174
|
+
if read_attribute(nested_interval_foreign_key).nil?
|
175
|
+
set_nested_interval_for_top
|
176
|
+
elsif !association(:parent).updated?
|
177
|
+
db_self = self.class.find(id, :lock => true)
|
178
|
+
write_attribute(nested_interval_foreign_key, db_self.read_attribute(nested_interval_foreign_key))
|
179
|
+
set_nested_interval db_self.lftp, db_self.lftq
|
180
|
+
else # move
|
181
|
+
# No locking in this case -- caller should have acquired table lock.
|
182
|
+
update_nested_interval_move
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def update_nested_interval_move
|
187
|
+
db_self = self.class.find(id)
|
188
|
+
db_parent = self.class.find(read_attribute(nested_interval_foreign_key))
|
189
|
+
if db_self.ancestor_of?(db_parent)
|
190
|
+
errors.add nested_interval_foreign_key, "is descendant"
|
191
|
+
raise ActiveRecord::RecordInvalid, self
|
192
|
+
end
|
193
|
+
|
194
|
+
set_nested_interval *parent.next_child_lft
|
195
|
+
mysql_tmp = "@" if ["MySQL", "Mysql2"].include?(connection.adapter_name)
|
196
|
+
cpp = db_self.lftq * rgtp - db_self.rgtq * lftp
|
197
|
+
cpq = db_self.rgtp * lftp - db_self.lftp * rgtp
|
198
|
+
cqp = db_self.lftq * rgtq - db_self.rgtq * lftq
|
199
|
+
cqq = db_self.rgtp * lftq - db_self.lftp * rgtq
|
200
|
+
|
201
|
+
db_descendants = db_self.descendants
|
202
|
+
|
203
|
+
if has_attribute?(:rgtp) && has_attribute?(:rgtq)
|
204
|
+
db_descendants.update_all %(
|
205
|
+
rgtp = #{cpp} * rgtp + #{cpq} * rgtq,
|
206
|
+
rgtq = #{cqp} * #{mysql_tmp}rgtp + #{cqq} * rgtq
|
207
|
+
), mysql_tmp && %(@rgtp := rgtp)
|
208
|
+
db_descendants.update_all "rgt = 1.0 * rgtp / rgtq" if has_attribute?(:rgt)
|
209
|
+
end
|
210
|
+
|
211
|
+
db_descendants.update_all %(
|
212
|
+
lftp = #{cpp} * lftp + #{cpq} * lftq,
|
213
|
+
lftq = #{cqp} * #{mysql_tmp}lftp + #{cqq} * lftq
|
214
|
+
), mysql_tmp && %(@lftp := lftp)
|
215
|
+
|
216
|
+
db_descendants.update_all %(lft = 1.0 * lftp / lftq) if has_attribute?(:lft)
|
217
|
+
end
|
218
|
+
|
219
|
+
def ancestor_of?(node)
|
220
|
+
node.lftp == lftp && node.lftq == lftq ||
|
221
|
+
node.lftp > node.lftq * lftp / lftq &&
|
222
|
+
node.lftp <= node.lftq * rgtp / rgtq &&
|
223
|
+
(node.lftp != rgtp || node.lftq != rgtq)
|
224
|
+
end
|
225
|
+
|
226
|
+
def ancestors
|
227
|
+
sqls = ["NULL"]
|
228
|
+
p, q = lftp, lftq
|
229
|
+
while p != 0
|
230
|
+
x = p.inverse(q)
|
231
|
+
p, q = (x * p - 1) / q, x
|
232
|
+
sqls << "lftq = #{q} AND lftp = #{p}"
|
233
|
+
end
|
234
|
+
nested_interval_scope.where(sqls * ' OR ')
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns depth by counting ancestors up to 0 / 1.
|
238
|
+
def depth
|
239
|
+
n = 0
|
240
|
+
p, q = lftp, lftq
|
241
|
+
while p != 0
|
242
|
+
x = p.inverse(q)
|
243
|
+
p, q = (x * p - 1) / q, x
|
244
|
+
n += 1
|
245
|
+
end
|
246
|
+
n
|
247
|
+
end
|
248
|
+
|
249
|
+
def lft; 1.0 * lftp / lftq end
|
250
|
+
def rgt; 1.0 * rgtp / rgtq end
|
251
|
+
|
252
|
+
# Returns numerator of right end of interval.
|
253
|
+
def rgtp
|
254
|
+
case lftp
|
255
|
+
when 0 then 1
|
256
|
+
when 1 then 1
|
257
|
+
else lftq.inverse(lftp)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns denominator of right end of interval.
|
262
|
+
def rgtq
|
263
|
+
case lftp
|
264
|
+
when 0 then 1
|
265
|
+
when 1 then lftq - 1
|
266
|
+
else (lftq.inverse(lftp) * lftq - 1) / lftp
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Returns left end of interval for next child.
|
271
|
+
def next_child_lft
|
272
|
+
if child = children.order('lftq DESC').first
|
273
|
+
return lftp + child.lftp, lftq + child.lftq
|
274
|
+
else
|
275
|
+
return lftp + rgtp, lftq + rgtq
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Returns left end of interval for next root.
|
280
|
+
def next_root_lft
|
281
|
+
vr = self.class.new # a virtual root
|
282
|
+
vr.set_nested_interval 0, 1
|
283
|
+
if child = nested_interval_scope.roots.order('lftq DESC').first
|
284
|
+
return vr.lftp + child.lftp, vr.lftq + child.lftq
|
285
|
+
else
|
286
|
+
return vr.lftp + vr.rgtp, vr.lftq + vr.rgtq
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
ActiveRecord::Base.send :include, ActsAsNestedInterval
|
293
|
+
|