ltree_hierarchy 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ if ENV["EDGE_RAILS"]
4
+ gem 'pg'
5
+ gem 'rails', :git => 'git://github.com/rails/rails'
6
+ else
7
+ gemspec
8
+ end
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,4 @@
1
+ require "ltree_hierarchy/hierarchy"
2
+ require "ltree_hierarchy/version"
3
+
4
+ ActiveRecord::Base.extend(Ltree::Hierarchy)
@@ -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
@@ -0,0 +1,5 @@
1
+ module Ltree
2
+ module Hierarchy
3
+ VERSION = "0.0.4"
4
+ end
5
+ 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: []