snapshot_tree 0.0.1

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/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .rspec
19
+ spec/db/database.yml
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in snapshot_tree.gemspec
4
+ gemspec
5
+
6
+ gem 'awesome_print'
data/Guardfile ADDED
@@ -0,0 +1,11 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', version: 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
7
+ watch(%r{^lib/(.+)\.yml$}) { "spec" }
8
+ watch('spec/spec_helper.rb') { "spec" }
9
+ watch('spec/setup.rb') { "spec" }
10
+ end
11
+
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Godwin Ko
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # Snapshot Tree
2
+
3
+ Yet another tree implementation of adjacency list structure using recursive query of Postgresql >= 8.4.
4
+
5
+ The main implementation different among others similar gems is the support
6
+ of handling multiple effective tree snapshot, for which the parent/child relationship
7
+ history can all be kept in a single relationship table with different effective date.
8
+
9
+ Since ActiveRecord doesn't support update of relation table through has_many :through association,
10
+ the creation of child and parent directly through association are not suported by this gem.
11
+ Please reference other gems in case if multiple snapshot with effective date handling is not necessary.
12
+
13
+ ## Requirements
14
+
15
+ * PostgreSQL version >= 8.4
16
+ * ActiveRecord
17
+
18
+ ## Setup
19
+
20
+ 1. Add this line to your application's Gemfile: ```gem 'snapshot_tree'```
21
+
22
+ 2. Run ```bundle install```
23
+
24
+ 3. Add ```acts_as_tree``` to your hierarchical model(s), see configuration section below.
25
+
26
+ The ActiveRecord associations ```parent_tree_nodes``` and ```child_tree_nodes``` will be
27
+ added automatically to ease the creation of tree association records
28
+
29
+ ```ruby
30
+ class Node < ActiveRecord::Base
31
+ include SnapshotTree::ActsAsTree
32
+ acts_as_tree
33
+ end
34
+ ```
35
+
36
+ 4. Add a database migration to store the hierarchy relation for your model.
37
+ Relation table's name must be the model's table name, followed by "_tree".
38
+
39
+ ```ruby
40
+ class CreateModelTrees < ActiveRecord::Migration
41
+ def change
42
+ create_table :model_trees do |t|
43
+ t.integer :child_id
44
+ t.integer :parent_id
45
+ t.boolean :is_active # default field name for :is_active_field option
46
+ t.date :effective_on # default field name for :snapshot_field option
47
+
48
+ t.timestamps
49
+ end
50
+ end
51
+ end
52
+ ```
53
+
54
+ 5. Run ```rake db:migrate```
55
+
56
+ ## Configuration
57
+
58
+ When you include ```acts_as_tree``` in your model, you can provide a hash to override the following defaults:
59
+
60
+ * ```:child_key``` to override the column name of the child foreign key in relation table. (default: ```child_id```)
61
+ * ```:parent_key``` to override the column name of the parent foreign key in relation table. (default: ```parent_id```)
62
+ * ```:node_prefix``` to override the field name prefix of generated field after getting descendent_nodes or ancestor_nodes. (default: ```node```)
63
+ * ```:snapshot_field``` to override the column name of snapshot effective date in relations table. (default: ```effective_on```)
64
+ - set it to ```nil``` will disable effective date filtering when getting tree snapshot.
65
+ * ```:is_active_field``` to override the column name of snapshot record active status in relation table. (default: ```is_active```)
66
+ - set it to ```nil``` will disable active status checking.
67
+ * ```:dependent``` determines what happens when a node is destroyed. Defaults to ```nullify```.
68
+ * ```:nullify``` will simply set the parent column to null. Each child node will be considered a "root" node. This is the default.
69
+ * ```:delete_all``` will delete all descendant nodes (which circumvents the destroy hooks)
70
+ * ```:destroy``` will destroy all descendant nodes (which runs the destroy hooks on each child node)
71
+
72
+ ## Usage
73
+
74
+ ### Creation tree association:
75
+
76
+ ```ruby
77
+ grandpa = Node.create(:name => 'grandpa')
78
+ parent = Node.create(:name => 'parent')
79
+ child = Node.create(:name => 'child')
80
+
81
+ grandpa.child_tree_nodes.create(:child_id => parent.id, :effective_on => '2012-01-01')
82
+ child.parent_tree_nodes.create(:parent_id => parent.id, :effective_on => Date.today)
83
+
84
+ ```
85
+
86
+ Accessing the tree:
87
+
88
+ #### Class methods
89
+
90
+ * ```Node.root_nodes``` returns all root nodes
91
+ * ```Node.leaf_nodes``` returns all leaf nodes
92
+ * ```Node.descendent_nodes``` returns all descendent nodes, including children, children's children, ... etc.
93
+ * ```Node.ancestor_nodes``` returns all ancestor nodes, including parent, grandparent, great grandparent, ... etc.
94
+
95
+ #### Instance methods
96
+
97
+ * ```Node.root_node``` returns the root node for this node
98
+ * ```Node.root_node?``` returns true if this is a root node
99
+ * ```Node.leaf_node?``` returns true if this is a leaf node
100
+ * ```Node.parent_node``` returns the parent node for this node
101
+ * ```Node.child_nodes``` returns an array of direct children node for this node
102
+ * ```Node.descendent_nodes``` returns all descendent nodes for this node, including children, children's children, ... etc.
103
+ * ```Node.ancestor_nodes``` returns all ancestor nodes for this node, including parent, grandparent, great grandparent, ... etc.
104
+
105
+ #### Extra options
106
+
107
+ When calling the instance/class methods, you can pass a hash of options to override the default behavour:
108
+
109
+ * ```:as_of``` to query the snapsot tree as of specify effective date, the latest effective date <= this parameter will be used to filter records. This defaults to today.
110
+ * ```:depth``` to limit to number of level to query, 0 for all levels, 1 for direct children only, 2 for direct children and grand children, ... etc. This defaults to 0.
111
+
112
+ When calling the class methods, you must pass an model or model id in order to query tree nodes:
113
+
114
+ ```Node.descendent_nodes(grandpa, :depth => 1, :as_of => '2010-01-01')```
115
+
116
+ ## Contributing
117
+
118
+ 1. Fork it
119
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
120
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
121
+ 4. Push to the branch (`git push origin my-new-feature`)
122
+ 5. Create new Pull Request
123
+
124
+ ## Thanks to
125
+
126
+ * https://github.com/chrisroberts/acts_as_sane_tree
127
+
128
+ * https://github.com/mceachen/closure_tree
129
+
130
+ * [Bill Karwin](http://karwin.blogspot.com/)'s excellent
131
+ [Models for hierarchical data presentation](http://www.slideshare.net/billkarwin/models-for-hierarchical-data)
132
+ for a description of different tree storage algorithms.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ desc 'Open an irb console'
5
+ task :console do
6
+ sh 'pry -Ilib -Ispec -rsetup'
7
+ end
@@ -0,0 +1,179 @@
1
+ module SnapshotTree
2
+ module ActsAsTree
3
+ extend ActiveSupport::Concern
4
+
5
+ def descendent_nodes(*args)
6
+ self.class.descendent_nodes(id, *args)
7
+ end
8
+
9
+ alias_method :descendant_nodes, :descendent_nodes
10
+
11
+ def ancestor_nodes(*args)
12
+ self.class.ancestor_nodes(id, *args)
13
+ end
14
+
15
+ def root_node?(*args)
16
+ self.class.root_nodes(id, *args).where(id: id).size > 0
17
+ end
18
+
19
+ def leaf_node?(*args)
20
+ self.class.leaf_nodes(id, *args).where(id: id).size > 0
21
+ end
22
+
23
+ def root_node(*args)
24
+ if root_node?(*args)
25
+ self
26
+ else
27
+ ancestor_nodes(*args).limit(1).first
28
+ end
29
+ end
30
+
31
+ def parent_node(*args)
32
+ ancestor_nodes(*args).try(:last)
33
+ end
34
+
35
+ def child_nodes(*args)
36
+ opts = args.detect { |x| x.is_a?(Hash) }
37
+ opts ? opts.merge!(depth: 1) : args << {depth: 1}
38
+
39
+ descendent_nodes(*args)
40
+ end
41
+
42
+ module ClassMethods
43
+ def acts_as_tree(opts = {})
44
+ options = {
45
+ parent_key: :parent_id,
46
+ child_key: :child_id,
47
+ snapshot_field: :effective_on,
48
+ is_active_field: :is_active,
49
+ node_prefix: :node,
50
+ dependent: :nullify
51
+ }.merge(opts)
52
+
53
+ options[:model_class] = self.name
54
+ options[:model_table] = options[:model_class].tableize
55
+ options[:join_class] = "#{self.name}Tree" unless options[:join_class]
56
+ options[:join_class] = options[:join_class].to_s
57
+ options[:join_table] = options[:join_class].tableize
58
+
59
+ instance_variable_set :@tree_helper, TreeHelper.new(options)
60
+
61
+ has_many :parent_tree_nodes,
62
+ class_name: options[:join_class],
63
+ foreign_key: options[:child_key],
64
+ autosave: true,
65
+ dependent: options[:dependent],
66
+ conditions: options[:is_active_field] ? {options[:is_active_field].to_sym => true} : nil
67
+
68
+ has_many :child_tree_nodes,
69
+ class_name: options[:join_class],
70
+ foreign_key: options[:parent_key],
71
+ autosave: true,
72
+ dependent: options[:dependent],
73
+ conditions: options[:is_active_field] ? {options[:is_active_field].to_sym => true} : nil
74
+
75
+ "#{options[:node_prefix]}_depth".to_sym.tap do |field|
76
+ define_method(field) do
77
+ read_attribute(field).try(:to_i)
78
+ end
79
+ end
80
+
81
+ "#{options[:node_prefix]}_path".to_sym.tap do |field|
82
+ define_method(field) do
83
+ read_attribute(field).gsub(/[{}]/, '').split(',').map(&:to_i) if read_attribute(field)
84
+ end
85
+ end
86
+ end
87
+
88
+ def root_nodes(*args)
89
+ opts = @tree_helper.parse_args(*args)
90
+
91
+ sql = @tree_helper.nodes_query(:root)
92
+ sql = sql.gsub(/__snapshot_value__/, opts[:as_of].to_s(format: :db)) if @tree_helper.snapshot_field?
93
+
94
+ self.unscoped.from(sql).order('1')
95
+ end
96
+
97
+ def leaf_nodes(*args)
98
+ opts = @tree_helper.parse_args(*args)
99
+
100
+ sql = @tree_helper.nodes_query(:leaf)
101
+ sql = sql.gsub(/__snapshot_value__/, opts[:as_of].to_s(format: :db)) if @tree_helper.snapshot_field?
102
+
103
+ self.unscoped.from(sql).order('1')
104
+ end
105
+
106
+ def descendent_nodes(*args)
107
+ opts = @tree_helper.parse_args(*args)
108
+
109
+ sql = @tree_helper.nodes_query(:descendent)
110
+ sql = sql.gsub(/__model_id__/, opts[:model_id].to_s)
111
+ sql = sql.gsub(/__snapshot_value__/, opts[:as_of].to_s(format: :db)) if @tree_helper.snapshot_field?
112
+
113
+ query = self.unscoped.from(sql)
114
+ query = query.where("#{@tree_helper.node_field(:depth)} <= #{opts[:depth]}") if opts[:depth] > 0
115
+ query = query.order("#{@tree_helper.node_field(:path)}")
116
+ end
117
+
118
+ alias_method :descendant_nodes, :descendent_nodes
119
+
120
+ def ancestor_nodes(*args)
121
+ opts = @tree_helper.parse_args(*args)
122
+
123
+ sql = @tree_helper.nodes_query(:ancestor)
124
+ sql = sql.gsub(/__model_id__/, opts[:model_id].to_s)
125
+ sql = sql.gsub(/__snapshot_value__/, opts[:as_of].to_s(format: :db)) if @tree_helper.snapshot_field?
126
+
127
+ query = self.unscoped.from(sql)
128
+ query = query.where("#{@tree_helper.node_field(:depth)} <= #{opts[:depth]}") if opts[:depth] > 0
129
+ query = query.order("#{@tree_helper.node_field(:depth)} DESC")
130
+ end
131
+ end
132
+
133
+ class TreeHelper
134
+ def initialize(opts)
135
+ @opts = opts
136
+ @query = {}
137
+ @template = YAML::load(File.open(File.dirname(__FILE__) + '/template.yml'))
138
+ end
139
+
140
+ def nodes_query(query_type)
141
+ return @query[query_type] if @query[query_type]
142
+
143
+ @query[query_type] = Handlebars.compile(
144
+ @template["#{query_type}_query"]
145
+ ).call(
146
+ {
147
+ model_table: @opts[:model_table],
148
+ join_table: @opts[:join_table],
149
+ child_key: "#{@opts[:child_key]}",
150
+ parent_key: "#{@opts[:parent_key]}",
151
+ path: "#{@opts[:node_prefix]}_path",
152
+ depth: "#{@opts[:node_prefix]}_depth",
153
+ cycle: "#{@opts[:node_prefix]}_cycle",
154
+ snapshot_field: "#{@opts[:snapshot_field]}",
155
+ is_active_field: "#{@opts[:is_active_field]}"
156
+ }
157
+ )
158
+ end
159
+
160
+ def node_field(field)
161
+ "#{@opts[:model_table]}.#{@opts[:node_prefix]}_#{field}"
162
+ end
163
+
164
+ def snapshot_field?
165
+ @opts[:snapshot_field].present?
166
+ end
167
+
168
+ def parse_args(*args)
169
+ opts = args.detect { |x| x.is_a?(Hash) } || {}
170
+ args.delete(opts) if opts.size > 0
171
+ opts[:as_of] = opts[:as_of].respond_to?(:to_date) ? opts[:as_of].to_date : Date.today
172
+ opts[:depth] = opts[:depth].to_i
173
+ opts[:model_id] = args[0].is_a?(ActiveRecord::Base) ? args[0].id : args[0].to_i
174
+ opts
175
+ end
176
+
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,121 @@
1
+ root_query: |
2
+ (
3
+ SELECT model.* FROM {{model_table}} model
4
+ {{#if snapshot_field}}
5
+ LEFT JOIN (
6
+ SELECT {{child_key}}, {{parent_key}}, {{snapshot_field}}, id,
7
+ {{#is_active_field}}{{this}}, {{/is_active_field}}
8
+ RANK() OVER (PARTITION BY {{child_key}} ORDER BY {{snapshot_field}} DESC, id DESC)
9
+ FROM {{join_table}}
10
+ WHERE {{snapshot_field}} <= '__snapshot_value__'
11
+ {{#is_active_field}}AND {{this}}{{/is_active_field}}
12
+ ) AS tree ON tree.{{child_key}} = model.id AND COALESCE(tree.rank, 1) = 1
13
+ {{else}}
14
+ LEFT JOIN {{join_table}} tree ON tree.{{child_key}} = model.id
15
+ {{/if}}
16
+ WHERE tree.{{parent_key}} IS NULL
17
+ ) AS {{model_table}}
18
+
19
+ leaf_query: |
20
+ (
21
+ SELECT model.* FROM {{model_table}} model
22
+ {{#if snapshot_field}}
23
+ LEFT JOIN (
24
+ SELECT {{child_key}}, {{parent_key}}, {{snapshot_field}}, id,
25
+ {{#is_active_field}}{{this}}, {{/is_active_field}}
26
+ RANK() OVER (PARTITION BY {{child_key}} ORDER BY {{snapshot_field}} DESC, id DESC)
27
+ FROM {{join_table}}
28
+ WHERE {{snapshot_field}} <= '__snapshot_value__'
29
+ {{#is_active_field}}AND {{this}}{{/is_active_field}}
30
+ ) AS tree ON tree.{{parent_key}} = model.id AND COALESCE(tree.rank, 1) = 1
31
+ {{else}}
32
+ LEFT JOIN {{join_table}} tree ON tree.{{parent_key}} = model.id
33
+ {{/if}}
34
+ WHERE tree.{{child_key}} IS NULL
35
+ ) AS {{model_table}}
36
+
37
+ descendent_query: |
38
+ (
39
+ WITH RECURSIVE tree AS (
40
+ SELECT
41
+ alias.{{child_key}}, alias.{{parent_key}},
42
+ ARRAY[alias.{{parent_key}}] AS {{path}}, 1 AS {{depth}}, false AS {{cycle}}
43
+ FROM {{join_table}} alias
44
+ {{#if snapshot_field}}JOIN snapshot ON snapshot.id = alias.id{{/if}}
45
+ WHERE alias.{{parent_key}} = __model_id__
46
+ UNION ALL
47
+ SELECT
48
+ alias.{{child_key}}, alias.{{parent_key}},
49
+ tree.{{path}} || alias.{{parent_key}} AS {{path}},
50
+ tree.{{depth}} + 1 AS {{depth}},
51
+ alias.{{child_key}} = ANY(tree.{{path}}) AS {{cycle}}
52
+ FROM tree
53
+ JOIN {{join_table}} alias ON alias.{{parent_key}} = tree.{{child_key}}
54
+ {{#if snapshot_field}}JOIN snapshot ON snapshot.id = alias.id{{/if}}
55
+ WHERE NOT tree.{{cycle}}
56
+ )
57
+ ,snapshot AS (
58
+ SELECT alias.id
59
+ FROM {{model_table}} model
60
+ {{#if snapshot_field}}
61
+ LEFT JOIN (
62
+ SELECT {{child_key}}, {{parent_key}}, {{snapshot_field}}, id,
63
+ {{#is_active_field}}{{this}}, {{/is_active_field}}
64
+ RANK() OVER (PARTITION BY {{child_key}} ORDER BY {{snapshot_field}} DESC, id DESC)
65
+ FROM {{join_table}}
66
+ WHERE {{snapshot_field}} <= '__snapshot_value__'
67
+ {{#is_active_field}}AND {{this}}{{/is_active_field}}
68
+ ) AS alias ON alias.{{child_key}} = model.id AND COALESCE(rank, 1) = 1
69
+ {{else}}
70
+ LEFT JOIN {{join_table}} alias ON alias.{{child_key}} = model.id
71
+ {{/if}}
72
+ WHERE alias.{{parent_key}} IS NOT NULL
73
+ )
74
+ SELECT model.*, tree.*
75
+ FROM tree
76
+ JOIN {{model_table}} model ON model.id = tree.{{child_key}}
77
+ WHERE NOT tree.{{cycle}}
78
+ ) AS {{model_table}}
79
+
80
+ ancestor_query: |
81
+ (
82
+ WITH RECURSIVE tree AS (
83
+ SELECT
84
+ alias.{{child_key}}, alias.{{parent_key}},
85
+ ARRAY[alias.{{parent_key}}] AS {{path}}, 1 AS {{depth}}, false AS {{cycle}}
86
+ FROM {{join_table}} alias
87
+ {{#if snapshot_field}}JOIN snapshot ON snapshot.id = alias.id{{/if}}
88
+ WHERE alias.{{child_key}} = __model_id__
89
+ UNION ALL
90
+ SELECT
91
+ alias.{{child_key}}, alias.{{parent_key}},
92
+ tree.{{path}} || alias.{{parent_key}} AS {{path}},
93
+ tree.{{depth}} + 1 AS {{depth}},
94
+ alias.{{parent_key}} = ANY(tree.{{path}}) AS {{cycle}}
95
+ FROM tree
96
+ JOIN {{join_table}} alias ON alias.{{child_key}} = tree.{{parent_key}}
97
+ {{#if snapshot_field}}JOIN snapshot ON snapshot.id = alias.id{{/if}}
98
+ WHERE NOT tree.{{cycle}}
99
+ )
100
+ ,snapshot AS (
101
+ SELECT alias.id
102
+ FROM {{model_table}} model
103
+ {{#if snapshot_field}}
104
+ LEFT JOIN (
105
+ SELECT {{child_key}}, {{parent_key}}, {{snapshot_field}}, id,
106
+ {{#is_active_field}}{{this}}, {{/is_active_field}}
107
+ RANK() OVER (PARTITION BY {{child_key}} ORDER BY {{snapshot_field}} DESC, id DESC)
108
+ FROM {{join_table}}
109
+ WHERE {{snapshot_field}} <= '__snapshot_value__'
110
+ {{#is_active_field}}AND {{this}}{{/is_active_field}}
111
+ ) AS alias ON alias.{{child_key}} = model.id AND COALESCE(rank, 1) = 1
112
+ {{else}}
113
+ LEFT JOIN {{join_table}} alias ON alias.{{child_key}} = model.id
114
+ {{/if}}
115
+ WHERE alias.{{parent_key}} IS NOT NULL
116
+ )
117
+ SELECT model.*, tree.*
118
+ FROM tree
119
+ JOIN {{model_table}} model ON model.id = tree.{{parent_key}}
120
+ WHERE NOT tree.{{cycle}}
121
+ ) AS {{model_table}}
@@ -0,0 +1,3 @@
1
+ module SnapshotTree
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,8 @@
1
+ require "active_record"
2
+ require 'handlebars'
3
+
4
+ require "snapshot_tree/version"
5
+ require "snapshot_tree/acts_as_tree"
6
+
7
+ module SnapshotTree
8
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/snapshot_tree/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Szetobo"]
6
+ gem.email = ["szetobo@gmail.com"]
7
+ gem.homepage = "https://github.com/szetobo/snapshot_tree"
8
+ gem.summary = "Mutliple snapshot hierarchical tree implementation of adjacency list using recursive query of Postgresql"
9
+ gem.description = "Mutliple snapshot hierarchical tree implementation of adjacency list using recursive query of Postgresql"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "snapshot_tree"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = SnapshotTree::VERSION
17
+
18
+ gem.add_dependency "activerecord", ">= 3.0.0"
19
+ gem.add_dependency "activesupport", ">= 3.0.0"
20
+ gem.add_dependency "pg", ">= 0.11.0"
21
+ gem.add_dependency "hbs", "~> 0.1.2"
22
+
23
+ gem.add_development_dependency "rake"
24
+ gem.add_development_dependency "rspec", "~> 2.6"
25
+ gem.add_development_dependency "guard-rspec"
26
+ gem.add_development_dependency "pry"
27
+ end
@@ -0,0 +1,213 @@
1
+ require 'spec_helper'
2
+
3
+ describe SnapshotTree do
4
+
5
+ describe 'after suite setup' do
6
+ it 'should have populated nodes and node_trees' do
7
+ Node.count.should == 15
8
+ NodeTree.count.should == 37
9
+ end
10
+ end
11
+
12
+ describe 'when requesting root/leaf nodes' do
13
+ it 'should return a relation' do
14
+ Node.root_nodes.should be_a_kind_of ActiveRecord::Relation
15
+ Node.leaf_nodes.should be_a_kind_of ActiveRecord::Relation
16
+ end
17
+
18
+ it 'should allow scope chaining' do
19
+ Node.where(name: 'root_1').first.should == Node.root_nodes.where(name: 'root_1').first
20
+ Node.where(name: 'node_2_1').first.should == Node.leaf_nodes.where(name: 'node_2_1').first
21
+ end
22
+
23
+ it 'should return all root nodes' do
24
+ Node.root_nodes.count.should == 3
25
+ Node.root_nodes(as_of: '1800-01-01').count.should == 15
26
+ end
27
+
28
+ it 'should have empty parent node for root nodes' do
29
+ Node.root_nodes.all? { |r| r.parent_node.nil? }.should be true
30
+ end
31
+
32
+ it 'should return all leaf nodes' do
33
+ Node.leaf_nodes.count.should == 8
34
+ Node.leaf_nodes(as_of: '1800-01-01').count.should == 15
35
+ end
36
+
37
+ it 'should have empty child nodes for leaf nodes' do
38
+ Node.leaf_nodes.all? { |r| r.child_nodes.size == 0 }.should be true
39
+ end
40
+ end
41
+
42
+ describe 'when access tree_1' do
43
+ before(:each) do
44
+ @parent = Node.root_nodes.first
45
+ end
46
+
47
+ it 'should have 5 descendents and 1 child' do
48
+ @parent.parent_node.should be nil
49
+ @parent.descendent_nodes.size.should == 5
50
+ @parent.child_nodes.size.should == 1
51
+ end
52
+
53
+ it 'should have 1 child for each node except the last node' do
54
+ nodes = @parent.descendent_nodes.all
55
+ (nodes.size - 1).times do |i|
56
+ nodes[i].root_node.should == @parent
57
+ nodes[i].parent_node.should == (i > 0 ? nodes[i-1] : @parent)
58
+ nodes[i].child_nodes.size.should == 1
59
+ nodes[i].child_nodes[0].name.should == nodes[i+1].name
60
+ end
61
+ nodes[-1].child_nodes.size.should == 0
62
+ end
63
+
64
+ it 'should be a flat tree before year 2000' do
65
+ nodes = @parent.descendent_nodes(as_of: '1990-01-01').where("name ~* 'node_1_'").all
66
+ nodes.all? { |r| r.parent_node(as_of: '1990-01-01') == @parent }.should be true
67
+ nodes.all? { |r| r.child_nodes(as_of: '1990-01-01').where("name ~* 'node_1_'").size == 0 }.should be true
68
+ end
69
+
70
+ it 'should be all root nodes before year 1900' do
71
+ nodes = Node.root_nodes(as_of: '1890-01-01').where("name ~* 'root_1|node_1_'").all
72
+ nodes.size.should == 6
73
+ nodes.all? { |r| r.parent_node(as_of: '1890-01-01').nil? }.should be true
74
+ end
75
+ end
76
+
77
+ describe 'when access tree_2' do
78
+ before(:each) do
79
+ @parent = Node.root_nodes.where(name: 'root_2').first
80
+ end
81
+
82
+ it 'should have 5 descendents and 5 child' do
83
+ @parent.parent_node.should be nil
84
+ @parent.descendent_nodes.size.should == 5
85
+ @parent.child_nodes.size.should == 5
86
+ end
87
+
88
+ it 'should have no child for each node' do
89
+ nodes = @parent.descendent_nodes.all
90
+ nodes.size.times do |i|
91
+ nodes[i].root_node.should == @parent
92
+ nodes[i].parent_node.should == @parent
93
+ nodes[i].child_nodes.size.should == 0
94
+ end
95
+ end
96
+
97
+ it 'should be all child of root_1 before year 2000' do
98
+ root_1 = Node.where(name: 'root_1').first
99
+ nodes = @parent.descendent_nodes(as_of: '1990-01-01').where("name ~* 'node_2_'").all
100
+ nodes.all? { |r| r.parent_node(as_of: '1990-01-01') == root_1 }.should be true
101
+ nodes.all? { |r| r.child_nodes(as_of: '1990-01-01').size == 0 }.should be true
102
+ end
103
+ end
104
+
105
+ describe 'when access tree_3' do
106
+ before(:each) do
107
+ @parent = Node.root_nodes.where(name: 'root_3').first
108
+ end
109
+
110
+ it 'should have 2 descendents and 2 child' do
111
+ @parent.parent_node.should be nil
112
+ @parent.descendent_nodes.size.should == 2
113
+ @parent.child_nodes.size.should == 2
114
+ end
115
+
116
+ it 'should have no child for each node' do
117
+ nodes = @parent.descendent_nodes.all
118
+ nodes.size.times do |i|
119
+ nodes[i].root_node.should == @parent
120
+ nodes[i].parent_node.should == @parent
121
+ nodes[i].child_nodes.size.should == 0
122
+ end
123
+ end
124
+
125
+ it 'should be respect the snapshot history' do
126
+ node = Node.where(name: 'node_3_1').first
127
+ node.parent_node(as_of:'1901-01-01').name.should == 'node_1_1'
128
+ node.parent_node(as_of:'1911-01-01').name.should == 'node_1_2'
129
+ node.parent_node(as_of:'1921-01-01').name.should == 'node_1_3'
130
+ node.parent_node(as_of:'1931-01-01').name.should == 'node_1_4'
131
+ node.parent_node(as_of:'1941-01-01').name.should == 'node_1_5'
132
+ node.parent_node(as_of:'2010-01-01').name.should == 'root_3'
133
+ end
134
+
135
+ it 'should be respect the active status of snapshot history' do
136
+ node = Node.where(name: 'node_3_2').first
137
+ node.parent_node(as_of:'1901-01-01').name.should == 'node_1_1'
138
+ node.parent_node(as_of:'1911-01-01').name.should == 'node_1_1'
139
+ node.parent_node(as_of:'1921-01-01').name.should == 'node_1_3'
140
+ node.parent_node(as_of:'1931-01-01').name.should == 'node_1_3'
141
+ node.parent_node(as_of:'1941-01-01').name.should == 'node_1_5'
142
+ node.parent_node(as_of:'2010-01-01').name.should == 'root_3'
143
+ end
144
+ end
145
+
146
+ #
147
+ # db setup for the whole test suite
148
+ #
149
+ before(:all) do
150
+ if Node.table_exists? && NodeTree.table_exists?
151
+ NodeTree.delete_all
152
+ Node.delete_all
153
+ Node.connection.execute("select setval('nodes_id_seq', 1, false); select setval('node_trees_id_seq', 1, false);")
154
+ else
155
+ load(File.dirname(__FILE__) + '/db/schema.rb')
156
+ end
157
+
158
+ # for testing multi-level hierarchy
159
+ parent1 = Node.create(name: "root_1")
160
+ node = Node.new(name: "node_1_1")
161
+ node.parent_tree_nodes.build(parent_id: parent1.id, effective_on: '1900-01-01')
162
+ node.save!
163
+ nodes = [node]
164
+ 4.times do |i|
165
+ node = Node.new(name: "node_1_#{i + 2}")
166
+ node.parent_tree_nodes.build(parent_id: nodes[i].id, effective_on: '2000-01-01')
167
+ node.parent_tree_nodes.build(parent_id: parent1.id, effective_on: '1900-01-01')
168
+ node.save!
169
+ nodes << node
170
+ end
171
+
172
+ # for testing single-level hierarchy
173
+ parent2 = Node.create(name: "root_2")
174
+ 5.times do |i|
175
+ node = Node.new(name: "node_2_#{i + 1}")
176
+ node.parent_tree_nodes.build(parent_id: parent2.id, effective_on: '2000-01-01')
177
+ node.parent_tree_nodes.build(parent_id: parent1.id, effective_on: '1900-01-01')
178
+ node.save!
179
+ end
180
+
181
+ parent3 = Node.create(name: "root_3")
182
+
183
+ # testing effective snapshot
184
+ node = Node.new(name: "node_3_1")
185
+ node.parent_tree_nodes.build(parent_id: nodes[0].id, effective_on: '1900-01-01')
186
+ node.parent_tree_nodes.build(parent_id: nodes[1].id, effective_on: '1910-01-01')
187
+ node.parent_tree_nodes.build(parent_id: nodes[2].id, effective_on: '1920-01-01')
188
+ node.parent_tree_nodes.build(parent_id: nodes[3].id, effective_on: '1930-01-01')
189
+ node.parent_tree_nodes.build(parent_id: nodes[4].id, effective_on: '1940-01-01')
190
+ node.parent_tree_nodes.build(parent_id: parent1.id, effective_on: '2000-01-01', created_at: Time.now - 2.hour)
191
+ node.parent_tree_nodes.build(parent_id: parent2.id, effective_on: '2000-01-01', created_at: Time.now - 1.hour)
192
+ node.parent_tree_nodes.build(parent_id: parent3.id, effective_on: '2000-01-01')
193
+ node.save!
194
+
195
+ # testing active snapshot
196
+ node = Node.new(name: "node_3_2")
197
+ node.parent_tree_nodes.build(parent_id: nodes[0].id, effective_on: '1900-01-01')
198
+ node.parent_tree_nodes.build(parent_id: nodes[1].id, effective_on: '1910-01-01')
199
+ node.parent_tree_nodes.build(parent_id: nodes[2].id, effective_on: '1920-01-01')
200
+ node.parent_tree_nodes.build(parent_id: nodes[3].id, effective_on: '1930-01-01')
201
+ node.parent_tree_nodes.build(parent_id: nodes[4].id, effective_on: '1940-01-01')
202
+ node.parent_tree_nodes.build(parent_id: nodes[0].id, effective_on: '2000-01-01', created_at: Time.now - 4.hour)
203
+ node.parent_tree_nodes.build(parent_id: nodes[1].id, effective_on: '2000-01-01', created_at: Time.now - 3.hour)
204
+ node.parent_tree_nodes.build(parent_id: nodes[2].id, effective_on: '2000-01-01', created_at: Time.now - 2.hour)
205
+ node.parent_tree_nodes.build(parent_id: parent3.id, effective_on: '2000-01-01', created_at: Time.now - 1.hour)
206
+ node.parent_tree_nodes.build(parent_id: parent1.id, effective_on: '2000-01-01')
207
+ node.save!
208
+ NodeTree.where(child_id: node.id, parent_id: nodes[1].id).first.update_attribute(:is_active, false)
209
+ NodeTree.where(child_id: node.id, parent_id: nodes[3].id).first.update_attribute(:is_active, false)
210
+ NodeTree.where(child_id: node.id, parent_id: parent1.id).first.update_attribute(:is_active, false)
211
+ end
212
+
213
+ end
@@ -0,0 +1,8 @@
1
+ adapter: postgresql
2
+ encoding: unicode
3
+ database: tree_test
4
+ # username: postgres
5
+ # password: postgres
6
+ pool: 5
7
+ port: 5432
8
+
data/spec/db/schema.rb ADDED
@@ -0,0 +1,23 @@
1
+ # encoding: UTF-8
2
+ ActiveRecord::Schema.define(:version => 0) do
3
+
4
+ create_table "nodes", :force => true do |t|
5
+ t.string "name"
6
+ t.datetime "created_at", :null => false
7
+ t.datetime "updated_at", :null => false
8
+ end
9
+
10
+ create_table "node_trees", :force => true do |t|
11
+ t.integer "child_id"
12
+ t.integer "parent_id"
13
+ t.boolean "is_active"
14
+ t.date "effective_on"
15
+ t.datetime "created_at", :null => false
16
+ t.datetime "updated_at", :null => false
17
+ end
18
+
19
+ add_index "node_trees", ["child_id"], :name => "index_node_trees_on_child_id"
20
+ add_index "node_trees", ["parent_id"], :name => "index_node_trees_on_parent_id"
21
+ add_index "node_trees", ["is_active"], :name => "index_node_trees_on_is_active"
22
+ add_index "node_trees", ["effective_on"], :name => "index_node_trees_on_effective_on"
23
+ end
data/spec/setup.rb ADDED
@@ -0,0 +1,23 @@
1
+ $: << File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+
6
+ require 'pry'
7
+
8
+ require 'snapshot_tree'
9
+
10
+ ActiveRecord::Base.establish_connection YAML::load(File.open(File.dirname(__FILE__) + '/db/database.yml'))
11
+
12
+ class Node < ActiveRecord::Base
13
+ include SnapshotTree::ActsAsTree
14
+ acts_as_tree
15
+ validates :name, presence: true
16
+ end
17
+
18
+ class NodeTree < ActiveRecord::Base
19
+ belongs_to :node
20
+ belongs_to :parent, class_name: 'Node'
21
+ validates :node, :parent, associated: true
22
+ validates :effective_on, :is_active, presence: true
23
+ end
@@ -0,0 +1,7 @@
1
+ require 'setup'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snapshot_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Szetobo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: &13337080 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *13337080
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ requirement: &13335920 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *13335920
36
+ - !ruby/object:Gem::Dependency
37
+ name: pg
38
+ requirement: &13368200 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 0.11.0
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *13368200
47
+ - !ruby/object:Gem::Dependency
48
+ name: hbs
49
+ requirement: &13365500 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.2
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *13365500
58
+ - !ruby/object:Gem::Dependency
59
+ name: rake
60
+ requirement: &13387820 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *13387820
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: &13385800 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: '2.6'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *13385800
80
+ - !ruby/object:Gem::Dependency
81
+ name: guard-rspec
82
+ requirement: &13382620 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *13382620
91
+ - !ruby/object:Gem::Dependency
92
+ name: pry
93
+ requirement: &13405940 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *13405940
102
+ description: Mutliple snapshot hierarchical tree implementation of adjacency list
103
+ using recursive query of Postgresql
104
+ email:
105
+ - szetobo@gmail.com
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - .gitignore
111
+ - Gemfile
112
+ - Guardfile
113
+ - LICENSE
114
+ - README.md
115
+ - Rakefile
116
+ - lib/snapshot_tree.rb
117
+ - lib/snapshot_tree/acts_as_tree.rb
118
+ - lib/snapshot_tree/template.yml
119
+ - lib/snapshot_tree/version.rb
120
+ - snapshot_tree.gemspec
121
+ - spec/acts_as_tree_spec.rb
122
+ - spec/db/database.yml.sample
123
+ - spec/db/schema.rb
124
+ - spec/setup.rb
125
+ - spec/spec_helper.rb
126
+ homepage: https://github.com/szetobo/snapshot_tree
127
+ licenses: []
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ! '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ segments:
139
+ - 0
140
+ hash: 3800555106948873925
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ none: false
143
+ requirements:
144
+ - - ! '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ segments:
148
+ - 0
149
+ hash: 3800555106948873925
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 1.8.15
153
+ signing_key:
154
+ specification_version: 3
155
+ summary: Mutliple snapshot hierarchical tree implementation of adjacency list using
156
+ recursive query of Postgresql
157
+ test_files: []