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 +19 -0
- data/Gemfile +6 -0
- data/Guardfile +11 -0
- data/LICENSE +22 -0
- data/README.md +132 -0
- data/Rakefile +7 -0
- data/lib/snapshot_tree/acts_as_tree.rb +179 -0
- data/lib/snapshot_tree/template.yml +121 -0
- data/lib/snapshot_tree/version.rb +3 -0
- data/lib/snapshot_tree.rb +8 -0
- data/snapshot_tree.gemspec +27 -0
- data/spec/acts_as_tree_spec.rb +213 -0
- data/spec/db/database.yml.sample +8 -0
- data/spec/db/schema.rb +23 -0
- data/spec/setup.rb +23 -0
- data/spec/spec_helper.rb +7 -0
- metadata +157 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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,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,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
|
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
|
data/spec/spec_helper.rb
ADDED
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: []
|