xylem 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.travis.yml +35 -0
- data/Appraisals +15 -0
- data/Gemfile +5 -0
- data/README.md +179 -0
- data/Rakefile +8 -0
- data/Vagrantfile +15 -0
- data/bench/acts_as_tree.json +32 -0
- data/bench/ancestry.json +32 -0
- data/bench/awesome_nested_set.json +32 -0
- data/bench/benchmark.rb +85 -0
- data/bench/xylem.json +32 -0
- data/bootstrap.sh +32 -0
- data/gemfiles/ar_4.0.gemfile +9 -0
- data/gemfiles/ar_4.1.gemfile +9 -0
- data/gemfiles/ar_4.2.gemfile +9 -0
- data/gemfiles/ar_5beta.gemfile +9 -0
- data/lib/xylem/version.rb +3 -0
- data/lib/xylem.rb +96 -0
- data/test/xylem_tests.rb +226 -0
- data/xylem.gemspec +28 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1f36a0a448ea0f87bbb5bb18b7ebb4f090e8a724
|
4
|
+
data.tar.gz: 74c92b1a1d23c3524480c3c97521f5f1bf3395b7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 25b9ac95f3a6c51d2abc5195269a7f6bab89376dffb32034db0a9aad18cf52974083ac5666515cdd0ebb6d93b77f68c80d1253ff256a4900e271ab99f6aec9b3
|
7
|
+
data.tar.gz: 9406bb0aec116ecee01b8f7dfdd3c5e3f190580eba20afb2f8e3a2a0fe57b81b4a23aa1d2d68889ed92fdd7adf6f2e982d55cf9429d223bfb7f9c9e57ad962c1
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
language: ruby
|
2
|
+
cache: bundler
|
3
|
+
rvm:
|
4
|
+
- 2.3.0
|
5
|
+
- 2.2.4
|
6
|
+
- 2.1.8
|
7
|
+
- 2.0.0
|
8
|
+
- 1.9.3
|
9
|
+
gemfile:
|
10
|
+
- gemfiles/ar_4.0.gemfile
|
11
|
+
- gemfiles/ar_4.1.gemfile
|
12
|
+
- gemfiles/ar_4.2.gemfile
|
13
|
+
- gemfiles/ar_5beta.gemfile
|
14
|
+
before_script:
|
15
|
+
- psql -c 'create database xylem_test;' -U postgres
|
16
|
+
addons:
|
17
|
+
apt:
|
18
|
+
sources:
|
19
|
+
- debian-sid
|
20
|
+
packages:
|
21
|
+
- sqlite3
|
22
|
+
env:
|
23
|
+
matrix:
|
24
|
+
- DB=sqlite
|
25
|
+
- DB=postgres
|
26
|
+
global:
|
27
|
+
secure: "PSK+NqEOpz6P3lDxQ/dIYg6F2FQrdtrkiE72hbL2S0uJ+BJMGp/Uw3h23ihUmlJAPqjNjtxyoslKYLiCiUje/6ID7DhxL8q2Yerqtf0mnMnHrX2mmSkrXrLva9RkMEbpXlHUVT7eZyEZKaw/3ZL917XPo2/M/dX8e0zwtUjhfOEejzXvWuW53A+B155ghpKnB9krbI71CXzK9fsPGR9fi4Mb7+eyZ24Qi3snrRADz9gQbpCG6rLs1j0E052NnLCttkMYU0d5+K9VI+eyDnWboiPThDY/iCw7arTkE6HPjnPbtZp2/GyiOQMpGxV8pzBrtAiy167wUzzhdhtKhLUvl0RUKyRY811N9EJAXEoXydf+r/SHCYNNrTaKKf/dMuTJT1YKFiqthlFS3lFT4f3JduRoPT0YsvPliAWDSdIG1XOINg5DnrNtVgKdwfLjb4klUK4MAw2cwF2M0iKbaNaj9AAduMctRxJyfY91+uCMOnnj4KXNXDzcUjLqXyZPd2qPBWf9wU1lL7mll/B2UDHuAaKqVDe18yut4LeszN0JFClZdGNlSdYXfj/1+C5b80hd5f8ckzlZ5ZYkksOTPDcByev1uvkDThSixru+LI8kYyTENgRAptMSHrUFmGn4XINELOvJi9dsjq9nhSC3DP30dCcddQalcxcSZQI8EeWOVb4="
|
28
|
+
matrix:
|
29
|
+
exclude:
|
30
|
+
- rvm: 1.9.3
|
31
|
+
gemfile: gemfiles/ar_5beta.gemfile
|
32
|
+
- rvm: 2.0.0
|
33
|
+
gemfile: gemfiles/ar_5beta.gemfile
|
34
|
+
- rvm: 2.1.8
|
35
|
+
gemfile: gemfiles/ar_5beta.gemfile
|
data/Appraisals
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
appraise 'ar-4.0' do
|
2
|
+
gem 'activerecord', '~> 4.0'
|
3
|
+
end
|
4
|
+
|
5
|
+
appraise 'ar-4.1' do
|
6
|
+
gem 'activerecord', '~> 4.1'
|
7
|
+
end
|
8
|
+
|
9
|
+
appraise 'ar-4.2' do
|
10
|
+
gem 'activerecord', '~> 4.2'
|
11
|
+
end
|
12
|
+
|
13
|
+
appraise 'ar-5beta' do
|
14
|
+
gem 'activerecord', '5.0.0.beta1'
|
15
|
+
end
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
# Xylem
|
2
|
+
[![Build Status](https://travis-ci.org/oesgalha/xylem.svg)](https://travis-ci.org/oesgalha/xylem)
|
3
|
+
[![Code Climate](https://codeclimate.com/github/oesgalha/xylem/badges/gpa.svg)](https://codeclimate.com/github/oesgalha/xylem)
|
4
|
+
[![Test Coverage](https://codeclimate.com/github/oesgalha/xylem/badges/coverage.svg)](https://codeclimate.com/github/oesgalha/xylem/coverage)
|
5
|
+
[![Dependency Status](https://gemnasium.com/oesgalha/xylem.svg)](https://gemnasium.com/oesgalha/xylem)
|
6
|
+
|
7
|
+
Xylem provides a simple way to store and retrieve hierarchical data in ActiveRecord.
|
8
|
+
|
9
|
+
## What
|
10
|
+
|
11
|
+
Xylem uses the Adjacency List approach to store hierarchical data, and use [recursive CTEs](https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression) to query through it.
|
12
|
+
|
13
|
+
That means that the storage strategy is simple: in order to map an ActiveRecord Model to a tree-like structure with parents and children, it's needed to add only one column to the table which contains a node's parent id. If the node is a root node that column will have a null value (or `nil`). With that, the insertion and removal of nodes is a simple process and thus it should be simple to recover a tree in a corrupted state and guarantee data consistency.
|
14
|
+
|
15
|
+
Also queries that traverse the tree (such as get ancestors or descendants of a node) are made in one single recursive SQL statement.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'xylem', github: 'oesgalha/xylem'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
Now add a `parent_id` column to your ActiveRecord::Base model.
|
30
|
+
Let's suppose that you want to add the tree behavior to a model called `Menu` with a database table called `menus`.
|
31
|
+
|
32
|
+
You could invoke the rails generator tool like this:
|
33
|
+
```
|
34
|
+
rails g migration add_parent_to_menus parent:references
|
35
|
+
```
|
36
|
+
|
37
|
+
Or you could create the migration by hand, with something like that:
|
38
|
+
```ruby
|
39
|
+
class AddParentToMenus < ActiveRecord::Migration
|
40
|
+
def change
|
41
|
+
add_reference :menus, :parent, index: true, foreign_key: true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
Some notes regarding migration:
|
47
|
+
* Add an index is optional, but recommended for performance sake.
|
48
|
+
* Add a foreign key is optional, but it's recommended to guarantee data consistency
|
49
|
+
* The `foreign_key: true` option is available in [rails 4.2.1](https://github.com/rails/rails/blob/v4.2.1/activerecord/CHANGELOG.md) or newer
|
50
|
+
|
51
|
+
Now you need to enable the tree behavior in or model by adding the `acts_as_tree` directive in it.
|
52
|
+
Let's suppose again that you're dealing with a model called `Menu`, you should add the following:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
class Menu < ActiveRecord::Base
|
56
|
+
acts_as_tree
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
And you are ready to go! Check the config options in the Usage section below.
|
61
|
+
|
62
|
+
## Usage
|
63
|
+
|
64
|
+
### acts_as_tree options
|
65
|
+
|
66
|
+
* :counter_cache => The name of the column that will cache the node children count. In order to use this, you need to create an integer column with the same name (ex: `counter_cache: :children_count`).
|
67
|
+
* :touch => If `true`, when you updated or destroy a node, it's ancestors will be touched: `updated_at` column is updated with the current time. (the default value is `false`)
|
68
|
+
* :dependent => Controls what happens with the children of a deleted node. Choose one of the following options (the default value is :destroy)
|
69
|
+
* :destroy children are also destroyed.
|
70
|
+
* :delete_all delete children direct in the database (this will skip callbacks)
|
71
|
+
* :nullify set the children's parent_id to NULL (nil), therefore turning them into new roots (this will also skip callbacks)
|
72
|
+
* :restrict_with_exception an exception is raised if there is an attempt to destroy a record with children
|
73
|
+
* :restrict_with_error a validation error is added to the record if there is an attempt to destroy it and it has children
|
74
|
+
|
75
|
+
### Class methods
|
76
|
+
|
77
|
+
Xylem adds the following class methods to a class with the `acts_as_tree` directive:
|
78
|
+
|
79
|
+
* `root`: returns the first root of the tree
|
80
|
+
* `roots`: returns all the roots from the tree
|
81
|
+
* `leaves`: returns all the leaves from the tree
|
82
|
+
|
83
|
+
### Instance methods
|
84
|
+
|
85
|
+
Xylem adds the following class methods to a class with the `acts_as_tree` directive:
|
86
|
+
|
87
|
+
* `ancestors`: returns the ancestors from root to the parent of the node. Ordered from the root to the parent.
|
88
|
+
* `self_and_ancestors` returns the ancestors from the root to the node itself. Ordered from the root to the node.
|
89
|
+
* `descendants` returns all the descendants of the given node. Ordered by depth from the closer to the node to the more distant ones.
|
90
|
+
* `self_and_descendants` returns the node and all it's descendants. Ordered by depth with the node first and the descendants later.
|
91
|
+
* `root` returns the current node root.
|
92
|
+
* `siblings` returns the siblings from the node, excluding itself.
|
93
|
+
* `self_and_siblings` returns the node and it's siblings.
|
94
|
+
* `children` returns the direct children of the node.
|
95
|
+
* `self_and_children` returns the node and it's direct children.
|
96
|
+
* `parent` returns the node parent.
|
97
|
+
* `root?` returns true if the current node is a root, false otherwise.
|
98
|
+
* `leaf?` returns true if the current node is a leaf, false otherwise.
|
99
|
+
|
100
|
+
### Examples
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
class Menu < ActiveRecord::Base
|
104
|
+
acts_as_tree
|
105
|
+
end
|
106
|
+
|
107
|
+
root = Menu.create!(name: 'root menu')
|
108
|
+
child1 = root.children.create!('option 1')
|
109
|
+
child2 = Menu.create!(name: 'option 2', parent: root)
|
110
|
+
subchild = Menu.create!(name: 'suboption 1', parent: child2)
|
111
|
+
|
112
|
+
Menu.roots # => [ root ]
|
113
|
+
Menu.leaves # => [ child1, subchild ]
|
114
|
+
|
115
|
+
root.root? # => true
|
116
|
+
child1.leaf? # => true
|
117
|
+
|
118
|
+
child2.self_and_descendants # => [ child2, subchild ]
|
119
|
+
|
120
|
+
child1.root # => root
|
121
|
+
child1.self_and_ancestors # => [ root, child1 ]
|
122
|
+
|
123
|
+
child1.self_and_siblings # => [ child1, child2 ]
|
124
|
+
```
|
125
|
+
|
126
|
+
## Dependencies
|
127
|
+
|
128
|
+
* ActiveRecord 4+
|
129
|
+
|
130
|
+
This gem relies on the `Recursive CTE` feature which was introduced to SQL in the 1999 revision.
|
131
|
+
So you need a database management system that implements it. This gem is tested against PostgreSQL and SQLite, those started to support the recursive CTE in the following versions:
|
132
|
+
* PostgreSQL 8.4+
|
133
|
+
* SQLite 3.8.3+
|
134
|
+
|
135
|
+
It seems that MySQL still has no support to it. If you use MySQL you can look for other gems to help you deal with models organized in trees, check the Alternatives section bellow.
|
136
|
+
|
137
|
+
This gem is tested against ruby 1.9.3 and newer only.
|
138
|
+
|
139
|
+
## Alternatives
|
140
|
+
|
141
|
+
There are other gems that allow you to treat models like trees, with different approaches.
|
142
|
+
Here are some other gems you could use to achieve the same objective:
|
143
|
+
|
144
|
+
* [acts_as_tree](https://github.com/amerine/acts_as_tree)
|
145
|
+
* [awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set)
|
146
|
+
* [ancestry](https://github.com/stefankroes/ancestry)
|
147
|
+
|
148
|
+
## Contributing
|
149
|
+
|
150
|
+
1. Fork it ( https://github.com/oesgalha/xylem/fork )
|
151
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
152
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
153
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
154
|
+
5. Create a new Pull Request
|
155
|
+
|
156
|
+
## License
|
157
|
+
|
158
|
+
Copyright (c) 2015-2016 Oscar Esgalha
|
159
|
+
|
160
|
+
MIT License
|
161
|
+
|
162
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
163
|
+
a copy of this software and associated documentation files (the
|
164
|
+
"Software"), to deal in the Software without restriction, including
|
165
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
166
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
167
|
+
permit persons to whom the Software is furnished to do so, subject to
|
168
|
+
the following conditions:
|
169
|
+
|
170
|
+
The above copyright notice and this permission notice shall be
|
171
|
+
included in all copies or substantial portions of the Software.
|
172
|
+
|
173
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
174
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
175
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
176
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
177
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
178
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
179
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/Vagrantfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- mode: ruby -*-
|
2
|
+
# vi: set ft=ruby :
|
3
|
+
|
4
|
+
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
5
|
+
VAGRANTFILE_API_VERSION = "2"
|
6
|
+
|
7
|
+
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
8
|
+
config.vm.box = 'ubuntu/trusty64'
|
9
|
+
config.vm.network :private_network, ip: "192.168.33.78"
|
10
|
+
config.vm.synced_folder '.', '/vagrant', nfs: true
|
11
|
+
config.ssh.forward_agent = true
|
12
|
+
config.vm.define "xylem_develop" do |xylem_develop|
|
13
|
+
end
|
14
|
+
config.vm.provision :shell, path: 'bootstrap.sh', keep_color: true
|
15
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"name": "insertion",
|
4
|
+
"ips": 412.7091261183738,
|
5
|
+
"stddev": 51
|
6
|
+
},
|
7
|
+
{
|
8
|
+
"name": "parent",
|
9
|
+
"ips": 633.857043437574,
|
10
|
+
"stddev": 85
|
11
|
+
},
|
12
|
+
{
|
13
|
+
"name": "children",
|
14
|
+
"ips": 8.660125142509473,
|
15
|
+
"stddev": 2
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"name": "roots",
|
19
|
+
"ips": 1029.8556860775493,
|
20
|
+
"stddev": 136
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"name": "ancestors",
|
24
|
+
"ips": 251.68388495529973,
|
25
|
+
"stddev": 33
|
26
|
+
},
|
27
|
+
{
|
28
|
+
"name": "descendants",
|
29
|
+
"ips": 0.19643176694334188,
|
30
|
+
"stddev": 0
|
31
|
+
}
|
32
|
+
]
|
data/bench/ancestry.json
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"name": "insertion",
|
4
|
+
"ips": 529.3829399001362,
|
5
|
+
"stddev": 68
|
6
|
+
},
|
7
|
+
{
|
8
|
+
"name": "parent",
|
9
|
+
"ips": 798.8413756182359,
|
10
|
+
"stddev": 112
|
11
|
+
},
|
12
|
+
{
|
13
|
+
"name": "children",
|
14
|
+
"ips": 19.534339432833036,
|
15
|
+
"stddev": 2
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"name": "roots",
|
19
|
+
"ips": 1306.76576446662,
|
20
|
+
"stddev": 254
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"name": "ancestors",
|
24
|
+
"ips": 501.12099359692917,
|
25
|
+
"stddev": 112
|
26
|
+
},
|
27
|
+
{
|
28
|
+
"name": "descendants",
|
29
|
+
"ips": 14.539749441549349,
|
30
|
+
"stddev": 2
|
31
|
+
}
|
32
|
+
]
|
@@ -0,0 +1,32 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"name": "insertion",
|
4
|
+
"ips": 7.721853007625269,
|
5
|
+
"stddev": 2
|
6
|
+
},
|
7
|
+
{
|
8
|
+
"name": "parent",
|
9
|
+
"ips": 569.4524509694182,
|
10
|
+
"stddev": 191
|
11
|
+
},
|
12
|
+
{
|
13
|
+
"name": "children",
|
14
|
+
"ips": 154.39379306573127,
|
15
|
+
"stddev": 34
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"name": "roots",
|
19
|
+
"ips": 820.8230612972056,
|
20
|
+
"stddev": 119
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"name": "ancestors",
|
24
|
+
"ips": 346.3686578783685,
|
25
|
+
"stddev": 70
|
26
|
+
},
|
27
|
+
{
|
28
|
+
"name": "descendants",
|
29
|
+
"ips": 52.86002694684352,
|
30
|
+
"stddev": 7
|
31
|
+
}
|
32
|
+
]
|
data/bench/benchmark.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'bundler/inline'
|
2
|
+
|
3
|
+
BENCH_GEM = ENV['BENCH_GEM']
|
4
|
+
|
5
|
+
unless ['xylem', 'acts_as_tree', 'awesome_nested_set', 'ancestry'].include?(BENCH_GEM)
|
6
|
+
fail 'Please provide a environment variable BENCH_GEM with one o the following values: [xylem, acts_as_tree, awesome_nested_set, ancestry]'
|
7
|
+
end
|
8
|
+
|
9
|
+
gemfile(true) do
|
10
|
+
source 'https://rubygems.org'
|
11
|
+
gem 'activerecord', require: 'active_record'
|
12
|
+
gem 'pg'
|
13
|
+
gem 'benchmark-ips'
|
14
|
+
|
15
|
+
case BENCH_GEM
|
16
|
+
when 'xylem'
|
17
|
+
gem 'xylem', path: '..'
|
18
|
+
when 'acts_as_tree'
|
19
|
+
gem 'acts_as_tree'
|
20
|
+
when 'awesome_nested_set'
|
21
|
+
gem 'activesupport', require: 'active_support/core_ext/module/delegation'
|
22
|
+
gem 'awesome_nested_set'
|
23
|
+
when 'ancestry'
|
24
|
+
gem 'ancestry'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: 'xylem_test', username: 'postgres')
|
29
|
+
ActiveRecord::Base.connection.execute('DROP SCHEMA public CASCADE; CREATE SCHEMA public;')
|
30
|
+
|
31
|
+
ActiveRecord::Base.send(:include, ActsAsTree) if BENCH_GEM == 'acts_as_tree'
|
32
|
+
|
33
|
+
class Comment < ActiveRecord::Base
|
34
|
+
acts_as_tree if ['xylem', 'acts_as_tree'].include?(BENCH_GEM)
|
35
|
+
acts_as_nested_set if BENCH_GEM == 'awesome_nested_set'
|
36
|
+
has_ancestry if BENCH_GEM == 'ancestry'
|
37
|
+
|
38
|
+
connection.create_table table_name, force: true do |t|
|
39
|
+
t.string :payload
|
40
|
+
t.integer :parent_id, index: true if BENCH_GEM != 'ancestry'
|
41
|
+
t.integer :lft, index: true if BENCH_GEM == 'awesome_nested_set'
|
42
|
+
t.integer :rgt, index: true if BENCH_GEM == 'awesome_nested_set'
|
43
|
+
t.string :ancestry, index: true if BENCH_GEM == 'ancestry'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def recursive_child(par, depth)
|
48
|
+
unless depth == 0
|
49
|
+
5.times do
|
50
|
+
recursive_child(Comment.create!(payload: SecureRandom.hex(128), parent: par), depth - 1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
puts 'INSERTING TEST DATA...'
|
56
|
+
|
57
|
+
recursive_child(nil, 5)
|
58
|
+
|
59
|
+
@comment1 = Comment.roots.first
|
60
|
+
@comment11 = @comment1.children.sample
|
61
|
+
@comment111 = @comment11.children.sample
|
62
|
+
@comment1111 = @comment111.children.sample
|
63
|
+
@comment11111 = @comment1111.children.sample
|
64
|
+
|
65
|
+
Benchmark.ips do |x|
|
66
|
+
x.report('insertion') do |times|
|
67
|
+
times.times { Comment.create!(parent: @comment111) }
|
68
|
+
end
|
69
|
+
x.report('parent') do |times|
|
70
|
+
times.times { @comment11111.reload.parent }
|
71
|
+
end
|
72
|
+
x.report('children') do |times|
|
73
|
+
times.times { @comment111.reload.children.to_a }
|
74
|
+
end
|
75
|
+
x.report('roots') do |times|
|
76
|
+
times.times { Comment.roots.to_a }
|
77
|
+
end
|
78
|
+
x.report('ancestors') do |times|
|
79
|
+
times.times { @comment11111.reload.ancestors.to_a }
|
80
|
+
end
|
81
|
+
x.report('descendants') do |times|
|
82
|
+
times.times { @comment1.reload.descendants.to_a }
|
83
|
+
end
|
84
|
+
x.json!("#{ENV['BENCH_GEM']}.json")
|
85
|
+
end
|
data/bench/xylem.json
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"name": "insertion",
|
4
|
+
"ips": 600.964555391788,
|
5
|
+
"stddev": 34
|
6
|
+
},
|
7
|
+
{
|
8
|
+
"name": "parent",
|
9
|
+
"ips": 1070.2536522884402,
|
10
|
+
"stddev": 120
|
11
|
+
},
|
12
|
+
{
|
13
|
+
"name": "children",
|
14
|
+
"ips": 11.90851991649796,
|
15
|
+
"stddev": 2
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"name": "roots",
|
19
|
+
"ips": 1713.3724759018537,
|
20
|
+
"stddev": 210
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"name": "ancestors",
|
24
|
+
"ips": 511.49023662770406,
|
25
|
+
"stddev": 61
|
26
|
+
},
|
27
|
+
{
|
28
|
+
"name": "descendants",
|
29
|
+
"ips": 12.000439734178782,
|
30
|
+
"stddev": 2
|
31
|
+
}
|
32
|
+
]
|
data/bootstrap.sh
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Shell file taken at: https://github.com/rails/rails-dev-box
|
2
|
+
function install {
|
3
|
+
echo installing $1
|
4
|
+
shift
|
5
|
+
apt-get -y install "$@" >/dev/null 2>&1
|
6
|
+
}
|
7
|
+
|
8
|
+
echo updating package information
|
9
|
+
apt-add-repository -y ppa:brightbox/ruby-ng >/dev/null 2>&1
|
10
|
+
apt-get -y update >/dev/null 2>&1
|
11
|
+
|
12
|
+
install 'development tools' build-essential
|
13
|
+
|
14
|
+
install Ruby ruby2.2 ruby2.2-dev
|
15
|
+
update-alternatives --set ruby /usr/bin/ruby2.2 >/dev/null 2>&1
|
16
|
+
update-alternatives --set gem /usr/bin/gem2.2 >/dev/null 2>&1
|
17
|
+
|
18
|
+
echo installing Bundler
|
19
|
+
gem install bundler -N >/dev/null 2>&1
|
20
|
+
|
21
|
+
install Git git
|
22
|
+
install SQLite sqlite3 libsqlite3-dev
|
23
|
+
|
24
|
+
install PostgreSQL postgresql postgresql-contrib libpq-dev
|
25
|
+
sudo -u postgres createdb -O postgres xylem_test
|
26
|
+
|
27
|
+
install 'Nokogiri dependencies' libxml2 libxml2-dev libxslt1-dev
|
28
|
+
|
29
|
+
# Needed for docs generation.
|
30
|
+
update-locale LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 LC_ALL=en_US.UTF-8
|
31
|
+
|
32
|
+
echo 'ready to roll out'
|
data/lib/xylem.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Xylem
|
4
|
+
module InstanceMethods
|
5
|
+
def ancestors
|
6
|
+
_xylem_query(:id, parent_id, :id, :parent_id, :desc)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self_and_ancestors
|
10
|
+
_xylem_query(:id, id, :id, :parent_id, :desc)
|
11
|
+
end
|
12
|
+
|
13
|
+
def descendants
|
14
|
+
_xylem_query(:parent_id, id, :parent_id, :id, :asc)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self_and_descendants
|
18
|
+
_xylem_query(:id, id, :parent_id, :id, :asc)
|
19
|
+
end
|
20
|
+
|
21
|
+
def root
|
22
|
+
ancestors.first
|
23
|
+
end
|
24
|
+
|
25
|
+
def siblings
|
26
|
+
self.class.where(parent_id: parent_id).where.not(id: id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self_and_siblings
|
30
|
+
self.class.where(parent_id: parent_id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self_and_children
|
34
|
+
[self] + children
|
35
|
+
end
|
36
|
+
|
37
|
+
def root?
|
38
|
+
parent.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
def leaf?
|
42
|
+
children.size == 0
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def _xylem_query(where_col, where_val, join_lft_col, join_rgt_col, order_stmt)
|
48
|
+
rcte = Arel::Table.new(:recusive_cte)
|
49
|
+
table = self.class.arel_table
|
50
|
+
i_select = table.project([table[Arel.star], Arel::Nodes::As.new(1, Arel::Nodes::SqlLiteral.new('level'))]).where(table[where_col].eq(where_val))
|
51
|
+
r_select = table.project([table[Arel.star], Arel::Nodes::SqlLiteral.new('level + 1')]).join(rcte).on(table[join_lft_col].eq(rcte[join_rgt_col]))
|
52
|
+
as_stmt = Arel::Nodes::As.new(rcte, i_select.union(:all, r_select))
|
53
|
+
self.class.from(Arel::Nodes::TableAlias.new(rcte.project(Arel.star).with(:recursive, as_stmt), self.class.table_name).to_sql).order(level: order_stmt)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
def root
|
59
|
+
roots.first
|
60
|
+
end
|
61
|
+
|
62
|
+
def roots
|
63
|
+
where(parent: nil)
|
64
|
+
end
|
65
|
+
|
66
|
+
def leaves
|
67
|
+
t = arel_table
|
68
|
+
where(t[:id].not_in(t.project([t[:parent_id]]).where(t[:parent_id].not_eq(nil)).distinct))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class ActiveRecord::Base
|
74
|
+
def self.acts_as_tree(options = {})
|
75
|
+
config = {
|
76
|
+
counter_cache: options[:counter_cache] || nil,
|
77
|
+
dependent: options[:destroy] || :destroy,
|
78
|
+
touch: options[:touch] || false
|
79
|
+
}
|
80
|
+
|
81
|
+
has_many :children,
|
82
|
+
class_name: name,
|
83
|
+
foreign_key: :parent_id,
|
84
|
+
dependent: config[:dependent],
|
85
|
+
inverse_of: :parent
|
86
|
+
|
87
|
+
belongs_to :parent,
|
88
|
+
class_name: name,
|
89
|
+
counter_cache: config[:counter_cache],
|
90
|
+
touch: config[:touch],
|
91
|
+
inverse_of: :children
|
92
|
+
|
93
|
+
extend Xylem::ClassMethods
|
94
|
+
include Xylem::InstanceMethods
|
95
|
+
end
|
96
|
+
end
|
data/test/xylem_tests.rb
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'codeclimate-test-reporter'
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'minitest/pride'
|
5
|
+
|
6
|
+
require 'xylem'
|
7
|
+
|
8
|
+
case ENV['DB']
|
9
|
+
when 'sqlite'
|
10
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
11
|
+
when 'postgres'
|
12
|
+
ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: 'xylem_test', username: 'postgres')
|
13
|
+
ActiveRecord::Base.connection.execute('DROP SCHEMA public CASCADE; CREATE SCHEMA public;')
|
14
|
+
else
|
15
|
+
fail 'Please provide a environment varible DB with either "postgres" or "sqlite" to define the tested database'
|
16
|
+
end
|
17
|
+
|
18
|
+
class Category < ActiveRecord::Base
|
19
|
+
acts_as_tree
|
20
|
+
connection.create_table table_name, force: true do |t|
|
21
|
+
t.integer :parent_id
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Menu < ActiveRecord::Base
|
26
|
+
acts_as_tree
|
27
|
+
default_scope { where(draft: false) }
|
28
|
+
connection.create_table table_name, force: true do |t|
|
29
|
+
t.integer :parent_id
|
30
|
+
t.boolean :draft, default: false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class PlainModel < ActiveRecord::Base
|
35
|
+
connection.create_table table_name, force: true do |t|
|
36
|
+
t.string :name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class XylemTestCase < MiniTest::Test
|
41
|
+
def setup
|
42
|
+
@product = Category.create!
|
43
|
+
@service = Category.create!
|
44
|
+
@physical = Category.create!(parent_id: @product.id)
|
45
|
+
@digital = Category.create!(parent_id: @product.id)
|
46
|
+
@training = Category.create!(parent_id: @service.id)
|
47
|
+
@consultancy = Category.create!(parent_id: @service.id)
|
48
|
+
@hosting = Category.create!(parent_id: @service.id)
|
49
|
+
@daily_training = Category.create!(parent_id: @training.id)
|
50
|
+
@weekly_training = Category.create!(parent_id: @training.id)
|
51
|
+
|
52
|
+
@gibberish = Menu.create!(draft: true)
|
53
|
+
@main = Menu.create!
|
54
|
+
@option1 = Menu.create!(parent: @main)
|
55
|
+
@option2 = Menu.create!(parent: @main, draft: true)
|
56
|
+
@option3 = Menu.create!(parent: @main)
|
57
|
+
@suboption11 = Menu.create!(parent: @option1)
|
58
|
+
@suboption12 = Menu.create!(parent: @option1)
|
59
|
+
@suboption21 = Menu.create!(parent: @option2, draft: true)
|
60
|
+
end
|
61
|
+
|
62
|
+
def teardown
|
63
|
+
ar_connection = ActiveRecord::Base.connection
|
64
|
+
ar_connection.tables.each { |t| ar_connection.execute "DELETE FROM #{t}" }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class ClassMethodsTest < XylemTestCase
|
69
|
+
def test_root
|
70
|
+
assert_equal @product, Category.root
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_scoped_root
|
74
|
+
assert_equal @main, Menu.root
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_roots
|
78
|
+
assert_equal [@product, @service], Category.roots
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_scoped_roots
|
82
|
+
assert_equal [@main], Menu.roots
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_leaves
|
86
|
+
assert_equal [@physical, @digital, @consultancy, @hosting, @daily_training, @weekly_training], Category.leaves
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_scoped_leaves
|
90
|
+
assert_equal [@option3, @suboption11, @suboption12], Menu.leaves
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_plain_model
|
94
|
+
refute_respond_to PlainModel, :root
|
95
|
+
refute_respond_to PlainModel, :roots
|
96
|
+
refute_respond_to PlainModel, :leaves
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class InstanceMethodsTest < XylemTestCase
|
101
|
+
def test_ancestors
|
102
|
+
assert_equal [@service, @training], @weekly_training.ancestors
|
103
|
+
assert_equal [@product], @digital.ancestors
|
104
|
+
assert_empty @product.ancestors
|
105
|
+
assert_empty @service.ancestors
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_scoped_ancestors
|
109
|
+
assert_equal [@main, @option1], @suboption12.ancestors
|
110
|
+
assert_empty @main.ancestors
|
111
|
+
assert_empty @gibberish.ancestors
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_self_and_ancestors
|
115
|
+
assert_equal [@service, @training, @weekly_training], @weekly_training.self_and_ancestors
|
116
|
+
assert_equal [@product, @digital], @digital.self_and_ancestors
|
117
|
+
assert_equal [@product], @product.self_and_ancestors
|
118
|
+
assert_equal [@service], @service.self_and_ancestors
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_scoped_self_and_ancestors
|
122
|
+
assert_equal [@main, @option1, @suboption12], @suboption12.self_and_ancestors
|
123
|
+
assert_equal [@main], @main.self_and_ancestors
|
124
|
+
assert_empty @gibberish.self_and_ancestors
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_descendants
|
128
|
+
assert_equal [@physical, @digital], @product.descendants
|
129
|
+
assert_equal [@training, @consultancy, @hosting, @daily_training, @weekly_training], @service.descendants
|
130
|
+
assert_equal [@daily_training, @weekly_training], @training.descendants
|
131
|
+
assert_empty @physical.descendants
|
132
|
+
assert_empty @consultancy.descendants
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_scoped_descendants
|
136
|
+
assert_equal [@option1, @option3, @suboption11, @suboption12], @main.descendants
|
137
|
+
assert_equal [@suboption11, @suboption12], @option1.descendants
|
138
|
+
assert_empty @gibberish.descendants
|
139
|
+
assert_empty @suboption11.descendants
|
140
|
+
assert_empty @suboption21.descendants
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_self_and_descendants
|
144
|
+
assert_equal [@physical, @digital], @product.descendants
|
145
|
+
assert_equal [@training, @consultancy, @hosting, @daily_training, @weekly_training], @service.descendants
|
146
|
+
assert_equal [@daily_training, @weekly_training], @training.descendants
|
147
|
+
assert_empty @physical.descendants
|
148
|
+
assert_empty @consultancy.descendants
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_scoped_self_and_descendants
|
152
|
+
assert_equal [@main, @option1, @option3, @suboption11, @suboption12], @main.self_and_descendants
|
153
|
+
assert_equal [@option1, @suboption11, @suboption12], @option1.self_and_descendants
|
154
|
+
assert_equal [@suboption11], @suboption11.self_and_descendants
|
155
|
+
assert_empty @gibberish.self_and_descendants
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_root
|
159
|
+
assert_equal @service, @weekly_training.root
|
160
|
+
assert_equal @product, @digital.root
|
161
|
+
refute @product.root
|
162
|
+
end
|
163
|
+
|
164
|
+
def test_scoped_root
|
165
|
+
assert_equal @main, @suboption11.root
|
166
|
+
assert_equal @main, @option3.root
|
167
|
+
refute @main.root
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_siblings
|
171
|
+
assert_equal [@digital], @physical.siblings
|
172
|
+
assert_equal [@training, @hosting], @consultancy.siblings
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_scoped_siblings
|
176
|
+
assert_equal [@option3], @option1.siblings
|
177
|
+
assert_equal [@suboption11], @suboption12.siblings
|
178
|
+
end
|
179
|
+
|
180
|
+
def test_self_and_siblings
|
181
|
+
assert_equal [@physical, @digital], @physical.self_and_siblings
|
182
|
+
assert_equal [@training, @consultancy, @hosting], @consultancy.self_and_siblings
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_scoped_self_and_siblings
|
186
|
+
assert_equal [@option1, @option3], @option1.self_and_siblings
|
187
|
+
assert_equal [@suboption11, @suboption12], @suboption12.self_and_siblings
|
188
|
+
end
|
189
|
+
|
190
|
+
def test_children
|
191
|
+
assert_equal [@physical, @digital], @product.children
|
192
|
+
assert_equal [@daily_training, @weekly_training], @training.children
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_scoped_children
|
196
|
+
assert_equal [@option1, @option3], @main.children
|
197
|
+
assert_equal [@suboption11, @suboption12], @option1.children
|
198
|
+
end
|
199
|
+
|
200
|
+
def test_self_and_children
|
201
|
+
assert_equal [@product, @physical, @digital], @product.self_and_children
|
202
|
+
assert_equal [@training, @daily_training, @weekly_training], @training.self_and_children
|
203
|
+
end
|
204
|
+
|
205
|
+
def test_scoped_self_and_children
|
206
|
+
assert_equal [@product, @physical, @digital], @product.self_and_children
|
207
|
+
assert_equal [@training, @daily_training, @weekly_training], @training.self_and_children
|
208
|
+
end
|
209
|
+
|
210
|
+
def test_root?
|
211
|
+
assert @product.root?
|
212
|
+
assert @main.root?
|
213
|
+
refute @physical.root?
|
214
|
+
refute @option3.root?
|
215
|
+
end
|
216
|
+
|
217
|
+
def test_leaf?
|
218
|
+
assert @daily_training.leaf?
|
219
|
+
assert @consultancy.leaf?
|
220
|
+
assert @suboption12.leaf?
|
221
|
+
assert @option3.leaf?
|
222
|
+
refute @service.leaf?
|
223
|
+
refute @training.leaf?
|
224
|
+
refute @option1.leaf?
|
225
|
+
end
|
226
|
+
end
|
data/xylem.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'xylem/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'xylem'
|
8
|
+
spec.version = Xylem::VERSION
|
9
|
+
spec.authors = ['Oscar Esgalha']
|
10
|
+
spec.email = ['oscaresgalha@gmail.com']
|
11
|
+
spec.summary = %q{Xylem uses the Adjacency List approach to store and query through hierarchical data with ActiveRecord.}
|
12
|
+
spec.description = %q{Xylem provides a simple way to store and retrieve hierarchical data through ActiveRecord.}
|
13
|
+
spec.homepage = 'https://github.com/oesgalha/xylem'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'activerecord', '>= 4.0'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
24
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
spec.add_development_dependency 'minitest', '~> 5.6'
|
26
|
+
spec.add_development_dependency 'pg', '>= 0.11'
|
27
|
+
spec.add_development_dependency 'sqlite3'
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xylem
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oscar Esgalha
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.6'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.11'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.11'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Xylem provides a simple way to store and retrieve hierarchical data through
|
98
|
+
ActiveRecord.
|
99
|
+
email:
|
100
|
+
- oscaresgalha@gmail.com
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- ".gitignore"
|
106
|
+
- ".travis.yml"
|
107
|
+
- Appraisals
|
108
|
+
- Gemfile
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- Vagrantfile
|
112
|
+
- bench/acts_as_tree.json
|
113
|
+
- bench/ancestry.json
|
114
|
+
- bench/awesome_nested_set.json
|
115
|
+
- bench/benchmark.rb
|
116
|
+
- bench/xylem.json
|
117
|
+
- bootstrap.sh
|
118
|
+
- gemfiles/ar_4.0.gemfile
|
119
|
+
- gemfiles/ar_4.1.gemfile
|
120
|
+
- gemfiles/ar_4.2.gemfile
|
121
|
+
- gemfiles/ar_5beta.gemfile
|
122
|
+
- lib/xylem.rb
|
123
|
+
- lib/xylem/version.rb
|
124
|
+
- test/xylem_tests.rb
|
125
|
+
- xylem.gemspec
|
126
|
+
homepage: https://github.com/oesgalha/xylem
|
127
|
+
licenses:
|
128
|
+
- MIT
|
129
|
+
metadata: {}
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
requirements: []
|
145
|
+
rubyforge_project:
|
146
|
+
rubygems_version: 2.4.5
|
147
|
+
signing_key:
|
148
|
+
specification_version: 4
|
149
|
+
summary: Xylem uses the Adjacency List approach to store and query through hierarchical
|
150
|
+
data with ActiveRecord.
|
151
|
+
test_files:
|
152
|
+
- test/xylem_tests.rb
|