acts_as_nested_interval 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|