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 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: []