ltree_hierarchy 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +8 -0
- data/Gemfile.lock +35 -0
- data/MIT-LICENSE +20 -0
- data/README.md +54 -0
- data/Rakefile +12 -0
- data/lib/ltree_hierarchy.rb +4 -0
- data/lib/ltree_hierarchy/hierarchy.rb +190 -0
- data/lib/ltree_hierarchy/version.rb +5 -0
- metadata +103 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
ltree_hierarchy (0.0.4)
|
5
|
+
activerecord (= 3.2)
|
6
|
+
pg
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (3.2.0)
|
12
|
+
activesupport (= 3.2.0)
|
13
|
+
builder (~> 3.0.0)
|
14
|
+
activerecord (3.2.0)
|
15
|
+
activemodel (= 3.2.0)
|
16
|
+
activesupport (= 3.2.0)
|
17
|
+
arel (~> 3.0.0)
|
18
|
+
tzinfo (~> 0.3.29)
|
19
|
+
activesupport (3.2.0)
|
20
|
+
i18n (~> 0.6)
|
21
|
+
multi_json (~> 1.0)
|
22
|
+
arel (3.0.2)
|
23
|
+
builder (3.0.4)
|
24
|
+
i18n (0.6.1)
|
25
|
+
multi_json (1.5.0)
|
26
|
+
pg (0.14.1)
|
27
|
+
rake (10.0.3)
|
28
|
+
tzinfo (0.3.35)
|
29
|
+
|
30
|
+
PLATFORMS
|
31
|
+
ruby
|
32
|
+
|
33
|
+
DEPENDENCIES
|
34
|
+
ltree_hierarchy!
|
35
|
+
rake
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Rob Worley
|
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,54 @@
|
|
1
|
+
# ltree_hierarchy
|
2
|
+
|
3
|
+
A simplistic gem that allows ActiveRecord models to be organized in a tree or hierarchy. It uses a materialized path implementation based around PostgreSQL's [ltree](http://www.postgresql.org/docs/current/static/ltree.html) data type, associated functions and operators.
|
4
|
+
|
5
|
+
[![Build Status](https://api.travis-ci.org/robworley/ltree_hierarchy.png)](https://travis-ci.org/robworley/ltree_hierarchy)
|
6
|
+
|
7
|
+
## Why might you want to use it?
|
8
|
+
|
9
|
+
- You want to be able to construct optimized hierarchical queries with ease, both from Ruby AND raw SQL.
|
10
|
+
- You want to be able to compose complex arel expressions from pre-defined building blocks.
|
11
|
+
- You prefer PostgreSQL over other relational DBs.
|
12
|
+
|
13
|
+
## Getting started
|
14
|
+
|
15
|
+
Follow these steps to apply to any ActiveRecord model:
|
16
|
+
|
17
|
+
1. Install
|
18
|
+
- Add to Gemfile: **gem 'ltree_hierarchy', :git => 'git://github.com/robworley/ltree_hierarchy.git'**
|
19
|
+
- Install required gems: **bundle install**
|
20
|
+
2. Add parent_id (integer) and path (ltree) columns to your table.
|
21
|
+
3. Add ltree hierarchy to your model
|
22
|
+
- Add to app/models/[model].rb: has_ltree_hierarchy
|
23
|
+
|
24
|
+
## Organizing records into a tree
|
25
|
+
|
26
|
+
Set the parent association or parent_id:
|
27
|
+
|
28
|
+
Node.create! :name => 'New York', :parent => Node.create!(:name => 'USA')
|
29
|
+
|
30
|
+
## Navigating the tree
|
31
|
+
|
32
|
+
The usual basic tree stuff. Use the following methods on any model instance:
|
33
|
+
|
34
|
+
- parent
|
35
|
+
- ancestors
|
36
|
+
- self_and_ancestors
|
37
|
+
- siblings
|
38
|
+
- self_and_siblings
|
39
|
+
- children
|
40
|
+
- self_and_children
|
41
|
+
- descendents
|
42
|
+
- self_and_descendents
|
43
|
+
- leaves
|
44
|
+
|
45
|
+
Useful class methods:
|
46
|
+
|
47
|
+
- roots
|
48
|
+
- leaves
|
49
|
+
- at_depth(n)
|
50
|
+
- lowest_common_ancestors(scope)
|
51
|
+
|
52
|
+
## TODO
|
53
|
+
|
54
|
+
- Better error message for circular references. Don't neglect i18n.
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
desc 'Default: run unit tests.'
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
desc 'Test the ltree_hierarchy plugin.'
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.pattern = 'test/**/*_test.rb'
|
11
|
+
t.verbose = true
|
12
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module Ltree
|
2
|
+
module Hierarchy
|
3
|
+
def has_ltree_hierarchy(options = {})
|
4
|
+
options = {
|
5
|
+
:fragment => :id,
|
6
|
+
:parent_fragment => :parent_id,
|
7
|
+
:path => :path
|
8
|
+
}.merge(options)
|
9
|
+
|
10
|
+
options.assert_valid_keys(:fragment, :parent_fragment, :path)
|
11
|
+
|
12
|
+
cattr_accessor :ltree_fragment_column, :ltree_parent_fragment_column, :ltree_path_column
|
13
|
+
|
14
|
+
self.ltree_fragment_column = options[:fragment]
|
15
|
+
self.ltree_parent_fragment_column = options[:parent_fragment]
|
16
|
+
self.ltree_path_column = options[:path]
|
17
|
+
|
18
|
+
belongs_to :parent, :class_name => self.name, :foreign_key => self.ltree_parent_fragment_column
|
19
|
+
|
20
|
+
validate :prevent_circular_paths, :if => :ltree_parent_fragment_changed?
|
21
|
+
|
22
|
+
after_create :commit_path
|
23
|
+
before_update :assign_path, :cascade_path_change, :if => :ltree_parent_fragment_changed?
|
24
|
+
|
25
|
+
include InstanceMethods
|
26
|
+
end
|
27
|
+
|
28
|
+
def roots
|
29
|
+
where(self.ltree_parent_fragment_column => nil)
|
30
|
+
end
|
31
|
+
|
32
|
+
def at_depth(depth)
|
33
|
+
where(["nlevel(#{ltree_path_column}) = ?", depth])
|
34
|
+
end
|
35
|
+
|
36
|
+
def leaves
|
37
|
+
subquery = select("DISTINCT #{ltree_parent_fragment_column}")
|
38
|
+
where("#{ltree_fragment_column} NOT IN(#{subquery.to_sql})")
|
39
|
+
end
|
40
|
+
|
41
|
+
def lowest_common_ancestor_paths(paths)
|
42
|
+
sql = if paths.respond_to?(:to_sql)
|
43
|
+
"SELECT lca(array(#{paths.to_sql}))"
|
44
|
+
else
|
45
|
+
return [] if paths.empty?
|
46
|
+
safe_paths = paths.map { |p| "#{connection.quote(p)}::ltree" }
|
47
|
+
"SELECT lca(ARRAY[#{safe_paths.join(', ')}])"
|
48
|
+
end
|
49
|
+
connection.select_values(sql)
|
50
|
+
end
|
51
|
+
|
52
|
+
def lowest_common_ancestors(paths)
|
53
|
+
where(ltree_path_column => lowest_common_ancestor_paths(paths))
|
54
|
+
end
|
55
|
+
|
56
|
+
module InstanceMethods
|
57
|
+
def ltree_scope
|
58
|
+
self.class.base_class
|
59
|
+
end
|
60
|
+
|
61
|
+
def ltree_fragment_column
|
62
|
+
self.class.ltree_fragment_column
|
63
|
+
end
|
64
|
+
|
65
|
+
def ltree_fragment
|
66
|
+
send(self.ltree_fragment_column)
|
67
|
+
end
|
68
|
+
|
69
|
+
def ltree_parent_fragment_column
|
70
|
+
self.class.ltree_parent_fragment_column
|
71
|
+
end
|
72
|
+
|
73
|
+
def ltree_parent_fragment
|
74
|
+
send(ltree_parent_fragment_column)
|
75
|
+
end
|
76
|
+
|
77
|
+
def ltree_parent_fragment_changed?
|
78
|
+
changed_attributes.key?(ltree_parent_fragment_column.to_s)
|
79
|
+
end
|
80
|
+
|
81
|
+
def ltree_path_column
|
82
|
+
self.class.ltree_path_column
|
83
|
+
end
|
84
|
+
|
85
|
+
def ltree_path
|
86
|
+
send(ltree_path_column)
|
87
|
+
end
|
88
|
+
|
89
|
+
def ltree_path_was
|
90
|
+
send("#{ltree_path_column}_was")
|
91
|
+
end
|
92
|
+
|
93
|
+
def prevent_circular_paths
|
94
|
+
if parent && parent.ltree_path.split('.').include?(ltree_fragment.to_s)
|
95
|
+
errors.add(ltree_parent_fragment_column, :invalid)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def compute_path
|
100
|
+
if parent
|
101
|
+
"#{parent.ltree_path}.#{ltree_fragment}"
|
102
|
+
else
|
103
|
+
ltree_fragment.to_s
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def assign_path
|
108
|
+
self.send("#{ltree_path_column}=", compute_path)
|
109
|
+
end
|
110
|
+
|
111
|
+
def commit_path
|
112
|
+
update_column(ltree_path_column, compute_path)
|
113
|
+
end
|
114
|
+
|
115
|
+
def cascade_path_change
|
116
|
+
# Typically equivalent to:
|
117
|
+
# UPDATE whatever
|
118
|
+
# SET path = NEW.path || subpath(path, nlevel(OLD.path))
|
119
|
+
# WHERE path <@ OLD.path AND id != NEW.id;
|
120
|
+
ltree_scope.where(
|
121
|
+
["#{ltree_path_column} <@ :old_path AND #{ltree_fragment_column} != :id", :old_path => ltree_path_was, :id => ltree_fragment]
|
122
|
+
).update_all(
|
123
|
+
["#{ltree_path_column} = :new_path || subpath(#{ltree_path_column}, nlevel(:old_path))", :new_path => ltree_path, :old_path => ltree_path_was]
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
def root?
|
128
|
+
if self.ltree_parent_fragment
|
129
|
+
false
|
130
|
+
else
|
131
|
+
parent.nil?
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def leaf?
|
136
|
+
!children.any?
|
137
|
+
end
|
138
|
+
|
139
|
+
def depth # 1-based, for compatibility with ltree's nlevel().
|
140
|
+
if root?
|
141
|
+
1
|
142
|
+
elsif ltree_path
|
143
|
+
ltree_path.split('.').length
|
144
|
+
elsif parent
|
145
|
+
parent.depth + 1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def ancestors
|
150
|
+
ltree_scope.where("#{ltree_path_column} @> ? AND #{ltree_fragment_column} != ?", ltree_path, ltree_fragment)
|
151
|
+
end
|
152
|
+
|
153
|
+
def self_and_ancestors
|
154
|
+
ltree_scope.where("#{ltree_path_column} @> ?", ltree_path)
|
155
|
+
end
|
156
|
+
alias :and_ancestors :self_and_ancestors
|
157
|
+
|
158
|
+
def siblings
|
159
|
+
ltree_scope.where("#{ltree_parent_fragment_column} = ? AND #{ltree_fragment_column} != ?", ltree_parent_fragment, ltree_fragment)
|
160
|
+
end
|
161
|
+
|
162
|
+
def self_and_siblings
|
163
|
+
ltree_scope.where(ltree_parent_fragment_column => ltree_parent_fragment)
|
164
|
+
end
|
165
|
+
alias :and_siblings :self_and_siblings
|
166
|
+
|
167
|
+
def descendents
|
168
|
+
ltree_scope.where("#{ltree_path_column} <@ ? AND #{ltree_fragment_column} != ?", ltree_path, ltree_fragment)
|
169
|
+
end
|
170
|
+
|
171
|
+
def self_and_descendents
|
172
|
+
ltree_scope.where("#{ltree_path_column} <@ ?", ltree_path)
|
173
|
+
end
|
174
|
+
alias :and_descendents :self_and_descendents
|
175
|
+
|
176
|
+
def children
|
177
|
+
ltree_scope.where(ltree_parent_fragment_column => ltree_fragment)
|
178
|
+
end
|
179
|
+
|
180
|
+
def self_and_children
|
181
|
+
ltree_scope.where("#{ltree_fragment_column} = :id OR #{ltree_parent_fragment_column} = :id", :id => ltree_fragment)
|
182
|
+
end
|
183
|
+
alias :and_children :self_and_children
|
184
|
+
|
185
|
+
def leaves
|
186
|
+
descendents.leaves
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ltree_hierarchy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Rob Worley
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.1.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.1.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: pg
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Organizes ActiveRecord models into a tree/hierarchy using a materialized
|
63
|
+
path implementation based around PostgreSQL's ltree datatype. ltree's operators
|
64
|
+
ensure that queries are fast and easily understood.
|
65
|
+
email:
|
66
|
+
- robert.worley@gmail.com
|
67
|
+
executables: []
|
68
|
+
extensions: []
|
69
|
+
extra_rdoc_files: []
|
70
|
+
files:
|
71
|
+
- lib/ltree_hierarchy/hierarchy.rb
|
72
|
+
- lib/ltree_hierarchy/version.rb
|
73
|
+
- lib/ltree_hierarchy.rb
|
74
|
+
- Gemfile
|
75
|
+
- Gemfile.lock
|
76
|
+
- MIT-LICENSE
|
77
|
+
- Rakefile
|
78
|
+
- README.md
|
79
|
+
homepage: https://github.com/robworley/ltree_hierarchy
|
80
|
+
licenses: []
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubyforge_project: ltree_hierarchy
|
99
|
+
rubygems_version: 1.8.24
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: Organize ActiveRecord models into a tree using PostgreSQL's ltree datatype
|
103
|
+
test_files: []
|