ltree_hierarchy 0.0.4
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/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
|
+
[](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: []
|