hierarchy 1.0.0
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/.document +5 -0
- data/.gitignore +25 -0
- data/.rspec +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +100 -0
- data/LICENSE +20 -0
- data/README.textile +96 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/hierarchy.gemspec +67 -0
- data/lib/hierarchy/index_path.rb +59 -0
- data/lib/hierarchy/node.rb +53 -0
- data/lib/hierarchy.rb +166 -0
- data/lib/hierarchy_generator.rb +22 -0
- data/spec/hierarchy_spec.rb +110 -0
- data/spec/index_path_spec.rb +31 -0
- data/spec/spec_helper.rb +26 -0
- data/templates/add_ltree_type.rb +15 -0
- metadata +112 -0
data/.document
ADDED
data/.gitignore
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
|
4
|
+
## TEXTMATE
|
5
|
+
*.tmproj
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## EMACS
|
9
|
+
*~
|
10
|
+
\#*
|
11
|
+
.\#*
|
12
|
+
|
13
|
+
## VIM
|
14
|
+
*.swp
|
15
|
+
|
16
|
+
## PROJECT::GENERAL
|
17
|
+
coverage
|
18
|
+
rdoc
|
19
|
+
pkg
|
20
|
+
.bundle
|
21
|
+
.rvmrc
|
22
|
+
|
23
|
+
## PROJECT::DOCUMENTATION
|
24
|
+
.yardoc
|
25
|
+
doc
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-cfs
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
RedCloth (4.2.3)
|
5
|
+
abstract (1.0.0)
|
6
|
+
actionmailer (3.0.1)
|
7
|
+
actionpack (= 3.0.1)
|
8
|
+
mail (~> 2.2.5)
|
9
|
+
actionpack (3.0.1)
|
10
|
+
activemodel (= 3.0.1)
|
11
|
+
activesupport (= 3.0.1)
|
12
|
+
builder (~> 2.1.2)
|
13
|
+
erubis (~> 2.6.6)
|
14
|
+
i18n (~> 0.4.1)
|
15
|
+
rack (~> 1.2.1)
|
16
|
+
rack-mount (~> 0.6.12)
|
17
|
+
rack-test (~> 0.5.4)
|
18
|
+
tzinfo (~> 0.3.23)
|
19
|
+
activemodel (3.0.1)
|
20
|
+
activesupport (= 3.0.1)
|
21
|
+
builder (~> 2.1.2)
|
22
|
+
i18n (~> 0.4.1)
|
23
|
+
activerecord (3.0.1)
|
24
|
+
activemodel (= 3.0.1)
|
25
|
+
activesupport (= 3.0.1)
|
26
|
+
arel (~> 1.0.0)
|
27
|
+
tzinfo (~> 0.3.23)
|
28
|
+
activeresource (3.0.1)
|
29
|
+
activemodel (= 3.0.1)
|
30
|
+
activesupport (= 3.0.1)
|
31
|
+
activesupport (3.0.1)
|
32
|
+
arel (1.0.1)
|
33
|
+
activesupport (~> 3.0.0)
|
34
|
+
builder (2.1.2)
|
35
|
+
diff-lcs (1.1.2)
|
36
|
+
erubis (2.6.6)
|
37
|
+
abstract (>= 1.0.0)
|
38
|
+
gemcutter (0.6.1)
|
39
|
+
git (1.2.5)
|
40
|
+
i18n (0.4.2)
|
41
|
+
jeweler (1.4.0)
|
42
|
+
gemcutter (>= 0.1.0)
|
43
|
+
git (>= 1.2.5)
|
44
|
+
rubyforge (>= 2.0.0)
|
45
|
+
json_pure (1.4.6)
|
46
|
+
mail (2.2.9)
|
47
|
+
activesupport (>= 2.3.6)
|
48
|
+
i18n (~> 0.4.1)
|
49
|
+
mime-types (~> 1.16)
|
50
|
+
treetop (~> 1.4.8)
|
51
|
+
mime-types (1.16)
|
52
|
+
pg (0.9.0)
|
53
|
+
polyglot (0.3.1)
|
54
|
+
rack (1.2.1)
|
55
|
+
rack-mount (0.6.13)
|
56
|
+
rack (>= 1.0.0)
|
57
|
+
rack-test (0.5.6)
|
58
|
+
rack (>= 1.0)
|
59
|
+
rails (3.0.1)
|
60
|
+
actionmailer (= 3.0.1)
|
61
|
+
actionpack (= 3.0.1)
|
62
|
+
activerecord (= 3.0.1)
|
63
|
+
activeresource (= 3.0.1)
|
64
|
+
activesupport (= 3.0.1)
|
65
|
+
bundler (~> 1.0.0)
|
66
|
+
railties (= 3.0.1)
|
67
|
+
railties (3.0.1)
|
68
|
+
actionpack (= 3.0.1)
|
69
|
+
activesupport (= 3.0.1)
|
70
|
+
rake (>= 0.8.4)
|
71
|
+
thor (~> 0.14.0)
|
72
|
+
rake (0.8.7)
|
73
|
+
rspec (2.0.1)
|
74
|
+
rspec-core (~> 2.0.1)
|
75
|
+
rspec-expectations (~> 2.0.1)
|
76
|
+
rspec-mocks (~> 2.0.1)
|
77
|
+
rspec-core (2.0.1)
|
78
|
+
rspec-expectations (2.0.1)
|
79
|
+
diff-lcs (>= 1.1.2)
|
80
|
+
rspec-mocks (2.0.1)
|
81
|
+
rspec-core (~> 2.0.1)
|
82
|
+
rspec-expectations (~> 2.0.1)
|
83
|
+
rubyforge (2.0.4)
|
84
|
+
json_pure (>= 1.1.7)
|
85
|
+
thor (0.14.3)
|
86
|
+
treetop (1.4.8)
|
87
|
+
polyglot (>= 0.3.1)
|
88
|
+
tzinfo (0.3.23)
|
89
|
+
yard (0.6.1)
|
90
|
+
|
91
|
+
PLATFORMS
|
92
|
+
ruby
|
93
|
+
|
94
|
+
DEPENDENCIES
|
95
|
+
RedCloth
|
96
|
+
jeweler
|
97
|
+
pg
|
98
|
+
rails (>= 3.0)
|
99
|
+
rspec
|
100
|
+
yard
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Tim Morgan
|
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.textile
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
h1. Hierarchy -- Use PostgreSQL @LTREE@ columns in ActiveRecord
|
2
|
+
|
3
|
+
| *Author* | Tim Morgan |
|
4
|
+
| *Version* | 1.0 (Oct 30, 2010) |
|
5
|
+
| *License* | Released under the MIT license. |
|
6
|
+
|
7
|
+
h2. About
|
8
|
+
|
9
|
+
The @LTREE@ column type is a PostgreSQL-specific type (installation script is
|
10
|
+
available in the @contrib@ directory in your PostgreSQL installation) for
|
11
|
+
representing hierarchies. It is more efficient than the typical way of
|
12
|
+
accomplishing hierarchal structures in SQL, the @parent_id@ column (or similar).
|
13
|
+
|
14
|
+
This gem lets you use an @LTREE@-utilizing hierarchy in ActiveRecord. Including
|
15
|
+
this gem in your project gets you a) monkey-patches* to Arel to support @LTREE@
|
16
|
+
columns, and b) a module you can include in your models, providing an abundance
|
17
|
+
of methods to help you navigate and manipulate the hierarchy.
|
18
|
+
|
19
|
+
*Though I don't know for sure, I suspect when Arel 2.0 is supported in Rails, I
|
20
|
+
will be able to remove this monkey-patching ugliness.
|
21
|
+
|
22
|
+
h2. Installation
|
23
|
+
|
24
|
+
*Important Note:* This gem requires Ruby 1.9 and Rails 3.0.
|
25
|
+
|
26
|
+
Firstly, add the gem to your Rails project's @Gemfile@:
|
27
|
+
|
28
|
+
<pre><code>
|
29
|
+
gem 'hierarchy'
|
30
|
+
</code></pre>
|
31
|
+
|
32
|
+
Then, run the generator to install the migration:
|
33
|
+
|
34
|
+
<pre><code>
|
35
|
+
rails generate hierarchy
|
36
|
+
</code></pre>
|
37
|
+
|
38
|
+
Note that *this migration must precede any tables using @LTREEs@*, so reorder
|
39
|
+
the migration if you have to.
|
40
|
+
|
41
|
+
h2. Usage
|
42
|
+
|
43
|
+
Because this gem was hastily extracted from a personal project, it's a little
|
44
|
+
constraining in how it can be used. (Sorry.) Currently the gem requires that
|
45
|
+
your table schema have a column named @path@ of type @LTREE@, defined as in the
|
46
|
+
example below:
|
47
|
+
|
48
|
+
<pre><code>
|
49
|
+
path LTREE NOT NULL DEFAULT ''
|
50
|
+
</code></pre>
|
51
|
+
|
52
|
+
Once you've got that column in your model, feel free to include the @Hierarchy@
|
53
|
+
module:
|
54
|
+
|
55
|
+
<pre><code>
|
56
|
+
class Person < ActiveRecord::Base
|
57
|
+
include Hierarchy
|
58
|
+
end
|
59
|
+
</code></pre>
|
60
|
+
|
61
|
+
You can now define hierarchy by setting a model's @parent@, like so:
|
62
|
+
|
63
|
+
<pre><code>
|
64
|
+
person.parent = mother #=> Sets the `path` column appropriately
|
65
|
+
</code></pre>
|
66
|
+
|
67
|
+
You also have access to a wealth of ways to traverse the hierarchy:
|
68
|
+
|
69
|
+
<pre><code>
|
70
|
+
person.children.where(gender: :male)
|
71
|
+
person.top_level?
|
72
|
+
Person.treeified #=> returns a traversable tree of all people
|
73
|
+
</code></pre>
|
74
|
+
|
75
|
+
For more information on what you can do, see the {Hierarchy} module
|
76
|
+
documentation.
|
77
|
+
|
78
|
+
h2. Development
|
79
|
+
|
80
|
+
If you wish to develop for Hierarchy, the first thing you will want to do is get
|
81
|
+
specs up and running. This requires a call to @bundle install@ (obviously) and
|
82
|
+
setting up your test database.
|
83
|
+
|
84
|
+
As you can see in the @spec/spec_helper.rb@ file, the specs require that a
|
85
|
+
PostgreSQL database named @hierarchy_test@ exist and be owned by a
|
86
|
+
@hierarchy_tester@ user. Unfortunately I haven't written a way to configure this
|
87
|
+
(though patches are welcome). So, the following commands should suffice to get
|
88
|
+
you started:
|
89
|
+
|
90
|
+
<pre><code>
|
91
|
+
createuser hierarchy_tester # answer "no" to all prompts
|
92
|
+
createdb -O hierarchy_tester hierarchy_test
|
93
|
+
</code></pre>
|
94
|
+
|
95
|
+
With those steps done you should be able to run @rake spec@ and see the Glorious
|
96
|
+
Green.
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rake'
|
2
|
+
begin
|
3
|
+
require 'bundler'
|
4
|
+
rescue LoadError
|
5
|
+
puts "Bundler is not installed; install with `gem install bundler`."
|
6
|
+
exit 1
|
7
|
+
end
|
8
|
+
|
9
|
+
Bundler.require :default
|
10
|
+
|
11
|
+
Jeweler::Tasks.new do |gem|
|
12
|
+
gem.name = "hierarchy"
|
13
|
+
gem.summary = %Q{Use PostgreSQL LTREE type with ActiveRecord}
|
14
|
+
gem.description = %Q{Adds ActiveRecord support for hierarchial data structures using PostgreSQL's LTREE column type.}
|
15
|
+
gem.email = "git@timothymorgan.info"
|
16
|
+
gem.homepage = "http://github.com/riscfuture/hierarchy"
|
17
|
+
gem.authors = [ "Tim Morgan" ]
|
18
|
+
gem.required_ruby_version = '>= 1.9'
|
19
|
+
gem.add_dependency 'activerecord', '>= 3.0'
|
20
|
+
gem.add_dependency 'activesupport', '>= 3.0'
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
|
24
|
+
require 'rspec/core/rake_task'
|
25
|
+
RSpec::Core::RakeTask.new
|
26
|
+
|
27
|
+
YARD::Rake::YardocTask.new('doc') do |doc|
|
28
|
+
doc.options << "-m" << "textile"
|
29
|
+
doc.options << "--protected"
|
30
|
+
doc.options << "-r" << "README.textile"
|
31
|
+
doc.options << "-o" << "doc"
|
32
|
+
doc.options << "--title" << "Hierarchy Documentation".inspect
|
33
|
+
|
34
|
+
doc.files = [ 'lib/**/*', 'README.textile' ]
|
35
|
+
end
|
36
|
+
|
37
|
+
task(default: :spec)
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/hierarchy.gemspec
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{hierarchy}
|
8
|
+
s.version = "1.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Tim Morgan"]
|
12
|
+
s.date = %q{2010-10-30}
|
13
|
+
s.description = %q{Adds ActiveRecord support for hierarchial data structures using PostgreSQL's LTREE column type.}
|
14
|
+
s.email = %q{git@timothymorgan.info}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.textile"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
".rspec",
|
23
|
+
"Gemfile",
|
24
|
+
"Gemfile.lock",
|
25
|
+
"LICENSE",
|
26
|
+
"README.textile",
|
27
|
+
"Rakefile",
|
28
|
+
"VERSION",
|
29
|
+
"hierarchy.gemspec",
|
30
|
+
"lib/hierarchy.rb",
|
31
|
+
"lib/hierarchy/index_path.rb",
|
32
|
+
"lib/hierarchy/node.rb",
|
33
|
+
"lib/hierarchy_generator.rb",
|
34
|
+
"spec/hierarchy_spec.rb",
|
35
|
+
"spec/index_path_spec.rb",
|
36
|
+
"spec/spec_helper.rb",
|
37
|
+
"templates/add_ltree_type.rb"
|
38
|
+
]
|
39
|
+
s.homepage = %q{http://github.com/riscfuture/hierarchy}
|
40
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
41
|
+
s.require_paths = ["lib"]
|
42
|
+
s.required_ruby_version = Gem::Requirement.new(">= 1.9")
|
43
|
+
s.rubygems_version = %q{1.3.7}
|
44
|
+
s.summary = %q{Use PostgreSQL LTREE type with ActiveRecord}
|
45
|
+
s.test_files = [
|
46
|
+
"spec/hierarchy_spec.rb",
|
47
|
+
"spec/index_path_spec.rb",
|
48
|
+
"spec/spec_helper.rb"
|
49
|
+
]
|
50
|
+
|
51
|
+
if s.respond_to? :specification_version then
|
52
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
53
|
+
s.specification_version = 3
|
54
|
+
|
55
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
56
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 3.0"])
|
57
|
+
s.add_runtime_dependency(%q<activesupport>, [">= 3.0"])
|
58
|
+
else
|
59
|
+
s.add_dependency(%q<activerecord>, [">= 3.0"])
|
60
|
+
s.add_dependency(%q<activesupport>, [">= 3.0"])
|
61
|
+
end
|
62
|
+
else
|
63
|
+
s.add_dependency(%q<activerecord>, [">= 3.0"])
|
64
|
+
s.add_dependency(%q<activesupport>, [">= 3.0"])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Hierarchy
|
2
|
+
|
3
|
+
# An array of integers representing an ordered list of IDs. Duck-types an
|
4
|
+
# @Array@ in many ways.
|
5
|
+
|
6
|
+
class IndexPath
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
delegate :&, :+, :-, :<<, :==, :[], :[]=, :at, :clear, :collect, :collect!,
|
10
|
+
:concat, :count, :delete_at, :delete_if, :drop, :drop_while, :each,
|
11
|
+
:each_index, :empty?, :eql?, :fetch, :index, :first, :include?,
|
12
|
+
:insert, :join, :keep_if, :last, :length, :map, :map!, :pop, :push,
|
13
|
+
:reject, :reject!, :reverse, :reverse!, :rindex, :select, :select!,
|
14
|
+
:shift, :size, :slice, :slice!, :take, :take_while, :to_a, :to_ary,
|
15
|
+
:unshift, :values_at, :|, to: :indexes
|
16
|
+
|
17
|
+
# @overload initialize(id, ...)
|
18
|
+
# Creates an index path from a list of integer IDs.
|
19
|
+
# @param [Fixnum] id An integer ID.
|
20
|
+
# @return [IndexPath] A new instance.
|
21
|
+
# @raise [ArgumentError] If an invalid ID is given.
|
22
|
+
|
23
|
+
def initialize(*indexes)
|
24
|
+
raise ArgumentError, "IndexPath indexes must be integers" unless indexes.all? { |index| index.kind_of?(Fixnum) }
|
25
|
+
@indexes = indexes
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates an index path from a PostgreSQL @LTREE@ column.
|
29
|
+
#
|
30
|
+
# @param [String] string An @LTREE@ column value, such as "1.10.22".
|
31
|
+
# @return [IndexPath] A corresponding index path.
|
32
|
+
|
33
|
+
def self.from_ltree(string)
|
34
|
+
new(*(string.split('.').map(&:to_i)))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Defines a natural ordering of index paths. Paths with lower IDs at the same
|
38
|
+
# index level will come before those with higher IDs at that index level.
|
39
|
+
# Lower IDs at shallower index levels come before lower IDs at deeper index
|
40
|
+
# levels.
|
41
|
+
#
|
42
|
+
# @param [IndexPath] other An index path to compare.
|
43
|
+
# @return [-1, 0, 1] -1 if this index path comes before the other one, 0 if
|
44
|
+
# they are identical, or 1 if this index path comes after the other one.
|
45
|
+
# @raise [ArgumentError] If something other than an index path is given.
|
46
|
+
|
47
|
+
def <=>(other)
|
48
|
+
raise ArgumentError, "Can't compare IndexPath and #{other.class.to_s}" unless other.kind_of?(IndexPath)
|
49
|
+
indexes <=> other.send(:indexes)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @private
|
53
|
+
def inspect() "#<#{self.class.to_s} #{@indexes.inspect}>" end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def indexes() @indexes end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Hierarchy
|
2
|
+
|
3
|
+
# A node in a tree structure. A node can have zero or more {#children}, and
|
4
|
+
# has a reverse link back to its parent.
|
5
|
+
|
6
|
+
class Node
|
7
|
+
# @return [Array<Node>] This node's children.
|
8
|
+
attr_reader :children
|
9
|
+
|
10
|
+
# @return [Node, nil] This node's parent, or @nil@ if it is a root node.
|
11
|
+
attr_reader :parent
|
12
|
+
|
13
|
+
# @return The object this node contains.
|
14
|
+
attr_accessor :content
|
15
|
+
|
16
|
+
# Creates a new root node with no children.
|
17
|
+
#
|
18
|
+
# @param content The content the node will contain.
|
19
|
+
|
20
|
+
def initialize(content)
|
21
|
+
@children = []
|
22
|
+
@content = content
|
23
|
+
end
|
24
|
+
|
25
|
+
# Adds a node as a child of this one. Sets the {#parent} of the given node.
|
26
|
+
#
|
27
|
+
# @param [Node] child The node to add as a child of this node.
|
28
|
+
|
29
|
+
def <<(child)
|
30
|
+
children << child
|
31
|
+
child.instance_variable_set :@parent, self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Performs a depth-first traversal using this as the root node of the tree.
|
35
|
+
# @yield [node] Each node in depth-first order.
|
36
|
+
# @yieldparam [Node] node A child node.
|
37
|
+
|
38
|
+
def traverse(&block)
|
39
|
+
children.each { |child| child.traverse &block }
|
40
|
+
block[self]
|
41
|
+
end
|
42
|
+
|
43
|
+
# @private
|
44
|
+
def inspect
|
45
|
+
str = "#<#{self.class.to_s} #{content.inspect}"
|
46
|
+
unless children.empty?
|
47
|
+
str << ": [ #{children.map(&:inspect).join(', ')} ]"
|
48
|
+
end
|
49
|
+
str << ">"
|
50
|
+
str
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/hierarchy.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
# @private
|
2
|
+
module Arel
|
3
|
+
# @private
|
4
|
+
module Sql
|
5
|
+
# @private
|
6
|
+
module Attributes
|
7
|
+
# @private
|
8
|
+
def self.for_with_psql(column)
|
9
|
+
case column.sql_type
|
10
|
+
when 'ltree' then String
|
11
|
+
else for_without_psql(column)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
unless singleton_class.method_defined?(:for_without_psql)
|
15
|
+
singleton_class.send :alias_method, :for_without_psql, :for
|
16
|
+
singleton_class.send :alias_method, :for, :for_with_psql
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'hierarchy_generator'
|
23
|
+
require 'hierarchy/index_path'
|
24
|
+
require 'hierarchy/node'
|
25
|
+
|
26
|
+
# Adds a tree structure to a model. This is very similar to @acts_as_nested_set@
|
27
|
+
# but uses the PostgreSQL-specific @ltree@ feature for schema storage.
|
28
|
+
#
|
29
|
+
# Your model must have a @path@ field of type @ltree@. This field will be a
|
30
|
+
# period-delimited list of IDs of records above this one in the hierarchy. In
|
31
|
+
# addition, you should also consider the following indexes:
|
32
|
+
#
|
33
|
+
# <pre><code>
|
34
|
+
# CREATE INDEX index1 ON table USING gist(path)
|
35
|
+
# CREATE INDEX index2 ON table USING btree(path)
|
36
|
+
# </code></pre>
|
37
|
+
#
|
38
|
+
# replacing @table@ with your table and @index1@/@index2@ with appropriate names
|
39
|
+
# for these indexes.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# class MyModel < ActiveRecord::Base
|
43
|
+
# include Hierarchy
|
44
|
+
# end
|
45
|
+
|
46
|
+
module Hierarchy
|
47
|
+
extend ActiveSupport::Concern
|
48
|
+
|
49
|
+
# @private
|
50
|
+
included do |base|
|
51
|
+
base.extend ActiveSupport::Memoizable
|
52
|
+
base.memoize :index_path, :ancestors
|
53
|
+
|
54
|
+
base.scope :parent_of, ->(obj) { obj.top_level? ? base.where('false') : base.where(id: obj.index_path.last) }
|
55
|
+
base.scope :children_of, ->(obj) { base.where(path: obj.my_path) }
|
56
|
+
base.scope :ancestors_of, ->(obj) { base.where(id: obj.index_path.to_a) }
|
57
|
+
base.scope :descendants_of, ->(obj) { base.where([ "path <@ ?", obj.my_path ]) }
|
58
|
+
base.scope :siblings_of, ->(obj) { base.where(path: obj.path) }
|
59
|
+
base.scope :priority_order, base.order("NLEVEL(path) ASC")
|
60
|
+
|
61
|
+
base.before_save { |obj| obj.path ||= '' }
|
62
|
+
end
|
63
|
+
|
64
|
+
module ClassMethods
|
65
|
+
|
66
|
+
# @overload treeified
|
67
|
+
# @return [Hash<ActiveRecord::Base, Hash<...>>] All models organized
|
68
|
+
# into a tree structure. Returns a hash where each key is a tree root,
|
69
|
+
# and the values are themselves hashes whose keys are the children of the
|
70
|
+
# respective model.
|
71
|
+
|
72
|
+
def treeified(root=nil, objects=nil)
|
73
|
+
path = root ? root.content.my_path : ''
|
74
|
+
root ||= Node.new(nil)
|
75
|
+
objects ||= order('id ASC').all.sort_by { |o| [ o.index_path, o.id ] }
|
76
|
+
|
77
|
+
while objects.first and objects.first.path == path
|
78
|
+
child = objects.shift
|
79
|
+
root << Node.new(child)
|
80
|
+
end
|
81
|
+
|
82
|
+
root.children.each do |child|
|
83
|
+
treeified child, objects
|
84
|
+
end
|
85
|
+
|
86
|
+
return root
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Methods added to instances of the class this module is included into.
|
91
|
+
|
92
|
+
module InstanceMethods
|
93
|
+
# Sets the object above this one in the hierarchy.
|
94
|
+
#
|
95
|
+
# @param [ActiveRecord::Base] parent The parent object.
|
96
|
+
# @raise [ArgumentError] If @parent@ is an unsaved record with no primary key.
|
97
|
+
|
98
|
+
def parent=(parent)
|
99
|
+
raise ArgumentError, "Parent cannot be a new record" if parent.try(:new_record?)
|
100
|
+
self.path = parent.try(:my_path)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns an array of ancestors above this object. Note that a) this array
|
104
|
+
# is ordered with the most senior ancestor at the beginning of the list, and
|
105
|
+
# b) this is an _array_, not a _relation_. For that reason, you can pass
|
106
|
+
# any additional scope options to the method.
|
107
|
+
#
|
108
|
+
# @param [Hash] options Additional finder options.
|
109
|
+
# @return [Array] The objects above this one in the hierarchy.
|
110
|
+
|
111
|
+
def ancestors(options={})
|
112
|
+
objects = self.class.ancestors_of(self).scoped(options).group_by(&:id)
|
113
|
+
index_path.map { |id| objects[id].first }
|
114
|
+
end
|
115
|
+
|
116
|
+
# @return [ActiveRecord::Relation] The objects below this one in the
|
117
|
+
# hierarchy.
|
118
|
+
|
119
|
+
def descendants
|
120
|
+
self.class.descendants_of self
|
121
|
+
end
|
122
|
+
|
123
|
+
# @return [ActiveRecord::Base] The object directly above this one in the
|
124
|
+
# hierarchy.
|
125
|
+
|
126
|
+
def parent
|
127
|
+
top_level? ? nil : self.class.parent_of(self).first
|
128
|
+
end
|
129
|
+
|
130
|
+
# @return [ActiveRecord::Relation] The objects directly below this one
|
131
|
+
# in the hierarchy.
|
132
|
+
|
133
|
+
def children
|
134
|
+
self.class.children_of self
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return [Array] The objects at the same hierarchical level of this one.
|
138
|
+
|
139
|
+
def siblings
|
140
|
+
self.class.siblings_of(self) - [ self ]
|
141
|
+
end
|
142
|
+
|
143
|
+
# @return [true, false] Whether or not this object has no parents.
|
144
|
+
|
145
|
+
def top_level?
|
146
|
+
path == ''
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [true, false] Whether or not this object has no children. Makes a
|
150
|
+
# database call.
|
151
|
+
|
152
|
+
def bottom_level?
|
153
|
+
children.empty?
|
154
|
+
end
|
155
|
+
|
156
|
+
# @private
|
157
|
+
def my_path
|
158
|
+
path.blank? ? id.to_s : "#{path}.#{id}"
|
159
|
+
end
|
160
|
+
|
161
|
+
# @private
|
162
|
+
def index_path
|
163
|
+
IndexPath.from_ltree path
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators'
|
3
|
+
require 'rails/generators/migration'
|
4
|
+
|
5
|
+
# @private
|
6
|
+
class HierarchyGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
source_root "#{File.dirname __FILE__}/../templates"
|
10
|
+
|
11
|
+
def self.next_migration_number(dirname)
|
12
|
+
if ActiveRecord::Base.timestamped_migrations then
|
13
|
+
Time.now.utc.strftime "%Y%m%d%H%M%S"
|
14
|
+
else
|
15
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def copy_files
|
20
|
+
migration_template "add_ltree_type.rb", "db/migrate/add_ltree_type.rb"
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe Hierarchy do
|
4
|
+
before :each do
|
5
|
+
@objects = Hash.new
|
6
|
+
2.times { |i| @objects[:"grandparent_#{i + 1}"] = Model.create! }
|
7
|
+
2.times do |gp_num|
|
8
|
+
2.times { |i| @objects[:"parent_#{gp_num + 1}_#{i + 1}"] = Model.create!(parent: @objects[:"grandparent_#{gp_num + 1}"]) }
|
9
|
+
end
|
10
|
+
2.times do |gp_num|
|
11
|
+
2.times do |p_num|
|
12
|
+
2.times { |i| @objects[:"child_#{gp_num + 1}_#{p_num + 1}_#{i + 1}"] = Model.create!(parent: @objects[:"parent_#{gp_num + 1}_#{p_num + 1}"]) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe ".parent_of" do
|
18
|
+
it "should return the parent object" do
|
19
|
+
Model.parent_of(@objects[:parent_1_1]).should == [ @objects[:grandparent_1] ]
|
20
|
+
Model.parent_of(@objects[:parent_2_2]).should == [ @objects[:grandparent_2] ]
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should return an empty relation for top-level objects" do
|
24
|
+
Model.parent_of(@objects[:grandparent_1]).should be_empty
|
25
|
+
Model.parent_of(@objects[:grandparent_2]).should be_empty
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe ".children_of" do
|
30
|
+
it "should return the direct children of an object" do
|
31
|
+
Model.children_of(@objects[:grandparent_1]).should == [ :parent_1_1, :parent_1_2 ].map { |name| @objects[name] }
|
32
|
+
Model.children_of(@objects[:parent_2_1]).should == [ :child_2_1_1, :child_2_1_2 ].map { |name| @objects[name] }
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should return an empty relation for leaf objects" do
|
36
|
+
Model.children_of(@objects[:child_1_1_1]).should be_empty
|
37
|
+
Model.children_of(@objects[:child_2_2_2]).should be_empty
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe ".ancestors_of" do
|
42
|
+
it "should return all ancestors of an object" do
|
43
|
+
Model.ancestors_of(@objects[:child_1_2_1]).should == [ :grandparent_1, :parent_1_2 ].map { |name| @objects[name] }
|
44
|
+
Model.ancestors_of(@objects[:parent_2_1]).should == [ @objects[:grandparent_2] ]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should return an empty relation for top-level objects" do
|
48
|
+
Model.ancestors_of(@objects[:grandparent_1]).should be_empty
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe ".descendants_of" do
|
53
|
+
it "should return all descendants of an object" do
|
54
|
+
Model.descendants_of(@objects[:grandparent_2]).should == [ :parent_2_1, :parent_2_2, :child_2_1_1, :child_2_1_2, :child_2_2_1, :child_2_2_2 ].map { |name| @objects[name] }
|
55
|
+
Model.descendants_of(@objects[:parent_2_1]).should == [ :child_2_1_1, :child_2_1_2 ].map { |name| @objects[name] }
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should return an empty relation for leaf objects" do
|
59
|
+
Model.children_of(@objects[:child_1_1_1]).should be_empty
|
60
|
+
Model.children_of(@objects[:child_2_2_2]).should be_empty
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe ".siblings_of" do
|
65
|
+
it "should return all sibling objects" do
|
66
|
+
Model.siblings_of(@objects[:parent_1_1]).should == [ :parent_1_1, :parent_1_2 ].map { |name| @objects[name] }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#parent=" do
|
71
|
+
it "should raise an error if parent is unsaved" do
|
72
|
+
expect { Model.create!.parent = Model.new }.to raise_error(ArgumentError)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should set the path ltree appropriately" do
|
76
|
+
ggp = Model.create!
|
77
|
+
gp = Model.create!(path: ggp.id.to_s)
|
78
|
+
pa = Model.create!(path: "#{ggp.id}.#{gp.id}")
|
79
|
+
path = [ ggp.id, gp.id, pa.id ].join('.')
|
80
|
+
|
81
|
+
object = Model.new
|
82
|
+
parent = Model.create!(path: path)
|
83
|
+
object.parent = parent
|
84
|
+
object.save!
|
85
|
+
object.path.should eql("#{path}.#{parent.id}")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#top_level?" do
|
90
|
+
it "should return true for a top-level object" do
|
91
|
+
Model.create!.should be_top_level
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should return false for an object with a parent" do
|
95
|
+
parent = Model.create!
|
96
|
+
Model.create!(parent: parent).should_not be_top_level
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "#bottom_level?" do
|
101
|
+
it "should return true for an object with no children" do
|
102
|
+
Model.create!.should be_bottom_level
|
103
|
+
end
|
104
|
+
it "should return false for an object with children" do
|
105
|
+
parent = Model.create!
|
106
|
+
Model.create!(parent: parent)
|
107
|
+
parent.should_not be_bottom_level
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe Hierarchy::IndexPath do
|
4
|
+
describe ".<=>" do
|
5
|
+
def compare(ip1, ip2)
|
6
|
+
Hierarchy::IndexPath.new(*ip1) <=> Hierarchy::IndexPath.new(*ip2)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should return -1 if this index path comes before the given index path" do
|
10
|
+
# all except last digit equal
|
11
|
+
compare([ 1,2,3 ], [ 1,2,4 ]).should eql(-1)
|
12
|
+
# unequal in higher precedence
|
13
|
+
compare([ 1,2,3 ], [ 1,3,2 ]).should eql(-1)
|
14
|
+
# other is longer
|
15
|
+
compare([ 1,2,3 ], [ 1,2,3,4 ]).should eql(-1)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should return 1 if this index path comes after the given index path" do
|
19
|
+
# all except last digit equal
|
20
|
+
compare([ 1,2,4 ], [ 1,2,3 ]).should eql(1)
|
21
|
+
# unequal in higher precedence
|
22
|
+
compare([ 1,3,2 ], [ 1,2,3 ]).should eql(1)
|
23
|
+
# this is longer
|
24
|
+
compare([ 1,2,3,4 ], [ 1,2,3, ]).should eql(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should return 0 if the index paths are equal" do
|
28
|
+
compare([ 1,2,3 ], [ 1,2,3 ]).should eql(0)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Bundler.require :default, :test
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
|
8
|
+
require 'hierarchy'
|
9
|
+
|
10
|
+
ActiveRecord::Base.establish_connection(
|
11
|
+
adapter: 'postgresql',
|
12
|
+
database: 'hierarchy_test',
|
13
|
+
username: 'hierarchy_tester'
|
14
|
+
)
|
15
|
+
system "psql -f `pg_config --sharedir`/contrib/ltree.sql hierarchy_test &>/dev/null"
|
16
|
+
|
17
|
+
class Model < ActiveRecord::Base
|
18
|
+
include Hierarchy
|
19
|
+
end
|
20
|
+
|
21
|
+
RSpec.configure do |config|
|
22
|
+
config.before(:each) do
|
23
|
+
Model.connection.execute "DROP TABLE IF EXISTS models"
|
24
|
+
Model.connection.execute "CREATE TABLE models (id SERIAL PRIMARY KEY, path LTREE NOT NULL DEFAULT '')"
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class AddLtreeType < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
cmd = "psql -f `pg_config --sharedir`/contrib/ltree.sql #{ActiveRecord::Base.connection.instance_variable_get(:@config)[:database]}"
|
4
|
+
puts cmd
|
5
|
+
result = system(cmd)
|
6
|
+
raise "Bad exit" unless result
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.down
|
10
|
+
cmd = "psql -f `pg_config --sharedir`/contrib/uninstall_ltree.sql #{ActiveRecord::Base.connection.instance_variable_get(:@config)[:database]}"
|
11
|
+
puts cmd
|
12
|
+
result = system(cmd)
|
13
|
+
raise "Bad exit" unless result
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hierarchy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 1.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Tim Morgan
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-10-30 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activerecord
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 3
|
29
|
+
- 0
|
30
|
+
version: "3.0"
|
31
|
+
type: :runtime
|
32
|
+
prerelease: false
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: activesupport
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 3
|
43
|
+
- 0
|
44
|
+
version: "3.0"
|
45
|
+
type: :runtime
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *id002
|
48
|
+
description: Adds ActiveRecord support for hierarchial data structures using PostgreSQL's LTREE column type.
|
49
|
+
email: git@timothymorgan.info
|
50
|
+
executables: []
|
51
|
+
|
52
|
+
extensions: []
|
53
|
+
|
54
|
+
extra_rdoc_files:
|
55
|
+
- LICENSE
|
56
|
+
- README.textile
|
57
|
+
files:
|
58
|
+
- .document
|
59
|
+
- .gitignore
|
60
|
+
- .rspec
|
61
|
+
- Gemfile
|
62
|
+
- Gemfile.lock
|
63
|
+
- LICENSE
|
64
|
+
- README.textile
|
65
|
+
- Rakefile
|
66
|
+
- VERSION
|
67
|
+
- hierarchy.gemspec
|
68
|
+
- lib/hierarchy.rb
|
69
|
+
- lib/hierarchy/index_path.rb
|
70
|
+
- lib/hierarchy/node.rb
|
71
|
+
- lib/hierarchy_generator.rb
|
72
|
+
- spec/hierarchy_spec.rb
|
73
|
+
- spec/index_path_spec.rb
|
74
|
+
- spec/spec_helper.rb
|
75
|
+
- templates/add_ltree_type.rb
|
76
|
+
has_rdoc: true
|
77
|
+
homepage: http://github.com/riscfuture/hierarchy
|
78
|
+
licenses: []
|
79
|
+
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options:
|
82
|
+
- --charset=UTF-8
|
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
|
+
segments:
|
91
|
+
- 1
|
92
|
+
- 9
|
93
|
+
version: "1.9"
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
version: "0"
|
102
|
+
requirements: []
|
103
|
+
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 1.3.7
|
106
|
+
signing_key:
|
107
|
+
specification_version: 3
|
108
|
+
summary: Use PostgreSQL LTREE type with ActiveRecord
|
109
|
+
test_files:
|
110
|
+
- spec/hierarchy_spec.rb
|
111
|
+
- spec/index_path_spec.rb
|
112
|
+
- spec/spec_helper.rb
|