activerecord-hierarchical_query 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.
@@ -0,0 +1,28 @@
1
+ require 'active_record/hierarchical_query/cte/non_recursive_term'
2
+ require 'active_record/hierarchical_query/cte/recursive_term'
3
+
4
+ module ActiveRecord
5
+ module HierarchicalQuery
6
+ module CTE
7
+ class UnionTerm
8
+ # @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
9
+ def initialize(query)
10
+ @query = query
11
+ end
12
+
13
+ def arel
14
+ non_recursive_term.union(:all, recursive_term)
15
+ end
16
+
17
+ private
18
+ def recursive_term
19
+ RecursiveTerm.new(@query).arel
20
+ end
21
+
22
+ def non_recursive_term
23
+ NonRecursiveTerm.new(@query).arel
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module HierarchicalQuery
3
+ VERSION = '0.0.1'
4
+ end
5
+ end
@@ -0,0 +1,28 @@
1
+ module Arel
2
+ module Nodes
3
+ class PostgresArray < Node
4
+ include AliasPredication
5
+ attr_accessor :values
6
+
7
+ def initialize(values)
8
+ self.values = values
9
+ end
10
+ end
11
+
12
+ class ArrayConcat < Binary
13
+ end
14
+ end
15
+
16
+ module Visitors
17
+ class ToSql < Arel::Visitors::Visitor
18
+ private
19
+ def visit_Arel_Nodes_PostgresArray o, *a
20
+ "ARRAY[#{visit o.values, *a}]"
21
+ end
22
+
23
+ def visit_Arel_Nodes_ArrayConcat o, *a
24
+ "#{visit o.left, *a} || #{visit o.right, *a}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,193 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::HierarchicalQuery do
4
+ let(:klass) { Category }
5
+
6
+ let!(:root) { klass.create }
7
+ let!(:child_1) { klass.create(:parent => root) }
8
+ let!(:child_2) { klass.create(:parent => child_1) }
9
+ let!(:child_3) { klass.create(:parent => child_1) }
10
+ let!(:child_4) { klass.create(:parent => root) }
11
+ let!(:child_5) { klass.create(:parent => child_4) }
12
+
13
+ describe '#join_recursive' do
14
+ describe 'CONNECT BY clause' do
15
+ it 'throws error if CONNECT BY clause not specified' do
16
+ expect {
17
+ klass.join_recursive {}
18
+ }.to raise_error /CONNECT BY clause/
19
+ end
20
+
21
+ it 'joins parent and child rows by hash map' do
22
+ expect(
23
+ klass.join_recursive { connect_by(:id => :parent_id) }
24
+ ).to include root, child_1, child_2, child_3, child_4, child_5
25
+ end
26
+
27
+ it 'joins parent and child rows by block' do
28
+ expect(
29
+ klass.join_recursive do
30
+ connect_by { |parent, child| parent[:id].eq child[:parent_id] }
31
+ end
32
+ ).to include root, child_1, child_2, child_3, child_4, child_5
33
+ end
34
+ end
35
+
36
+ describe 'START WITH clause' do
37
+ def assert_start_with(&block)
38
+ expect(
39
+ klass.join_recursive do |b|
40
+ b.connect_by(:id => :parent_id).instance_eval(&block)
41
+ end
42
+ ).to match_array [root, child_1, child_2, child_3, child_4, child_5]
43
+ end
44
+
45
+ it 'filters rows in non-recursive term by hash' do
46
+ assert_start_with { start_with(:parent_id => nil) }
47
+ end
48
+
49
+ it 'filters rows in non-recursive term by block with arity > 0' do
50
+ assert_start_with { start_with { |root| root.where(:parent_id => nil) } }
51
+ end
52
+
53
+ it 'filters rows in non-recursive term by block with arity = 0' do
54
+ assert_start_with { start_with { where(:parent_id => nil) } }
55
+ end
56
+
57
+ it 'filters rows in non-recursive term by scope' do
58
+ assert_start_with { start_with(klass.where(:parent_id => nil)) }
59
+ end
60
+ end
61
+
62
+ describe 'ORDER SIBLINGS BY clause' do
63
+ def assert_ordered_by_name_desc(&block)
64
+ expect(
65
+ klass.join_recursive do |b|
66
+ b.connect_by(:id => :parent_id).start_with(:parent_id => nil).instance_eval(&block)
67
+ end
68
+ ).to eq [root, child_4, child_5, child_1, child_3, child_2]
69
+ end
70
+
71
+ def assert_ordered_by_name_asc(&block)
72
+ expect(
73
+ klass.join_recursive do |b|
74
+ b.connect_by(:id => :parent_id).start_with(:parent_id => nil).instance_eval(&block)
75
+ end
76
+ ).to eq [root, child_1, child_2, child_3, child_4, child_5]
77
+ end
78
+
79
+ it 'orders rows by Hash' do
80
+ assert_ordered_by_name_desc { order_siblings(:name => :desc) }
81
+ end
82
+
83
+ it 'orders rows by String' do
84
+ assert_ordered_by_name_desc { order_siblings('name desc') }
85
+ assert_ordered_by_name_asc { order_siblings('name asc') }
86
+ end
87
+
88
+ it 'orders rows by Arel::Nodes::Ordering' do
89
+ assert_ordered_by_name_desc { order_siblings(table[:name].desc) }
90
+ end
91
+
92
+ it 'orders rows by Arel::Nodes::Node' do
93
+ assert_ordered_by_name_asc { order_siblings(table[:name]) }
94
+ end
95
+
96
+ it 'throws error when something weird given' do
97
+ expect {
98
+ klass.join_recursive { connect_by(:id => :parent_id).order_siblings(1) }
99
+ }.to raise_error /ORDER BY SIBLINGS/
100
+ end
101
+
102
+ context 'when one attribute given and this attribute support natural sorting' do
103
+ let(:relation) do
104
+ klass.join_recursive do
105
+ connect_by(:id => :parent_id).
106
+ start_with(:parent_id => nil).
107
+ order_siblings(:position)
108
+ end
109
+ end
110
+
111
+ it 'orders rows by given attribute' do
112
+ expect(relation).to eq [root, child_1, child_2, child_3, child_4, child_5]
113
+ expect(relation.to_sql).not_to match /row_number/i
114
+ end
115
+ end
116
+ end
117
+
118
+ describe 'LIMIT and OFFSET clauses' do
119
+ let(:ordered_nodes) { [root, child_1, child_2, child_3, child_4, child_5] }
120
+
121
+ it 'limits all rows' do
122
+ expect(
123
+ klass.join_recursive do
124
+ connect_by(:id => :parent_id).
125
+ start_with(:parent_id => nil).
126
+ order_siblings(:name).
127
+ limit(2).
128
+ offset(2)
129
+ end
130
+ ).to eq ordered_nodes[2...4]
131
+ end
132
+ end
133
+
134
+ describe 'WHERE clause' do
135
+ it 'filters child rows' do
136
+ expect(
137
+ klass.join_recursive do
138
+ connect_by(:id => :parent_id).
139
+ start_with(:parent_id => nil).
140
+ where('depth < ?', 2)
141
+ end
142
+ ).to match_array [root, child_1, child_4]
143
+ end
144
+
145
+ it 'allows to use PRIOR relation' do
146
+ expect(
147
+ klass.join_recursive do |b|
148
+ b.connect_by(:id => :parent_id)
149
+ .start_with(:parent_id => nil)
150
+ .select(:depth)
151
+ .where(b.prior[:depth].lt(1))
152
+ end
153
+ ).to match_array [root, child_1, child_4]
154
+ end
155
+ end
156
+
157
+ describe 'SELECT clause' do
158
+ it 'adds column to both recursive and non-recursive term' do
159
+ expect(
160
+ klass.join_recursive(:as => 'categories_r') do
161
+ connect_by(:id => :parent_id).
162
+ start_with(:parent_id => nil).
163
+ select(:depth)
164
+ end.where('categories_r.depth = 0')
165
+ ).to eq [root]
166
+ end
167
+ end
168
+
169
+ describe 'NOCYCLE clause' do
170
+ before { klass.where(:id => child_4.id).update_all(:parent_id => child_5.id) }
171
+
172
+ it 'prevents recursive query from endless loops' do
173
+ expect(
174
+ klass.join_recursive do |query|
175
+ query.start_with(:id => child_4.id)
176
+ .connect_by(:id => :parent_id)
177
+ .nocycle
178
+ end
179
+ ).to match_array [child_4, child_5]
180
+ end
181
+ end
182
+ end
183
+
184
+ describe '#join_recursive options' do
185
+ describe ':as option' do
186
+ it 'builds a join with specified alias' do
187
+ expect(
188
+ klass.join_recursive(:as => 'my_table') { connect_by(:id => :parent_id) }.to_sql
189
+ ).to match /my_table/
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,4 @@
1
+ adapter: postgresql
2
+ database: hierarchical_query_test
3
+ username: postgres
4
+ min_messages: ERROR
data/spec/database.yml ADDED
@@ -0,0 +1,4 @@
1
+ adapter: postgresql
2
+ database: hierarchical_query_test
3
+ username: vagrant
4
+ min_messages: ERROR
data/spec/schema.rb ADDED
@@ -0,0 +1,10 @@
1
+ ActiveRecord::Migration.verbose = false
2
+
3
+ ActiveRecord::Schema.define(:version => 0) do
4
+ create_table :categories, :force => true do |t|
5
+ t.column :parent_id, :integer
6
+ t.column :name, :string
7
+ t.column :depth, :integer
8
+ t.column :position, :integer
9
+ end
10
+ end
@@ -0,0 +1,58 @@
1
+ # coding: utf-8
2
+ require 'pathname'
3
+ require 'logger'
4
+
5
+ SPEC_ROOT = Pathname.new(File.dirname(__FILE__))
6
+
7
+ require 'bundler/setup'
8
+
9
+ Bundler.setup(:default, ENV['TRAVIS'] ? :travis : :local)
10
+
11
+ require 'rspec'
12
+ require 'database_cleaner'
13
+ require 'active_record'
14
+
15
+ ActiveRecord::Base.establish_connection(YAML.load(SPEC_ROOT.join('database.yml').read))
16
+ ActiveRecord::Base.logger = Logger.new(ENV['DEBUG'] ? $stderr : '/dev/null')
17
+ ActiveRecord::Base.logger.formatter = proc do |severity, datetime, progname, msg|
18
+ "#{datetime.strftime('%H:%M:%S.%L')}: #{msg}\n"
19
+ end
20
+
21
+ load SPEC_ROOT.join('schema.rb')
22
+ require SPEC_ROOT.join('support', 'models').to_s
23
+
24
+ DatabaseCleaner.strategy = :transaction
25
+
26
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
27
+ RSpec.configure do |config|
28
+ config.treat_symbols_as_metadata_keys_with_true_values = true
29
+ config.run_all_when_everything_filtered = true
30
+ config.filter_run :focus
31
+
32
+ # Run specs in random order to surface order dependencies. If you find an
33
+ # order dependency and want to debug it, you can fix the order by providing
34
+ # the seed, which is printed after each run.
35
+ # --seed 1234
36
+ config.order = 'random'
37
+
38
+ config.before(:suite) do
39
+ DatabaseCleaner.strategy = :transaction
40
+ DatabaseCleaner.clean_with(:truncation)
41
+ end
42
+
43
+ config.around(:each) do |example|
44
+ DatabaseCleaner.start
45
+ example.run
46
+ DatabaseCleaner.clean
47
+ end
48
+ end
49
+
50
+ if ENV['TRAVIS']
51
+ require 'coveralls'
52
+ Coveralls.wear!
53
+ else
54
+ require 'simplecov'
55
+ SimpleCov.start
56
+ end
57
+
58
+ require 'active_record/hierarchical_query'
@@ -0,0 +1,39 @@
1
+ class Category < ActiveRecord::Base
2
+ @@generator = Enumerator.new do |y|
3
+ abc = ('a'..'z').to_a
4
+ sequence = abc.product(abc, abc).map(&:join).to_enum
5
+
6
+ loop do
7
+ # category aaa
8
+ # category aab
9
+ # ...
10
+ # category aaz
11
+ # category aba
12
+ # ...
13
+ y << "category #{sequence.next}"
14
+ end
15
+ end
16
+
17
+ belongs_to :parent, :class_name => 'Category'
18
+ has_many :children, :class_name => 'Category'
19
+
20
+ before_save :generate_name, :unless => :name?
21
+ before_save :count_depth
22
+ before_save :count_position
23
+
24
+ def generate_name
25
+ self.name = @@generator.next
26
+ end
27
+
28
+ def count_depth
29
+ self.depth = ancestors.count
30
+ end
31
+
32
+ def count_position
33
+ self.position = (self.class.where(:parent_id => parent_id).maximum(:position) || 0) + 1
34
+ end
35
+
36
+ def ancestors
37
+ parent ? parent.ancestors + [parent] : []
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,171 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-hierarchical_query
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexei Mikhailov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-11 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: 3.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 3.1.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.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
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.1.1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 10.1.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 2.14.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 2.14.1
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.17.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 0.17.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: database_cleaner
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: 1.2.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: 1.2.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: 0.8.2
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: 0.8.2
111
+ description:
112
+ email:
113
+ - amikhailov83@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - lib/active_record/hierarchical_query.rb
119
+ - lib/active_record/hierarchical_query/adapters.rb
120
+ - lib/active_record/hierarchical_query/adapters/postgresql.rb
121
+ - lib/active_record/hierarchical_query/builder.rb
122
+ - lib/active_record/hierarchical_query/cte/columns.rb
123
+ - lib/active_record/hierarchical_query/cte/cycle_detector.rb
124
+ - lib/active_record/hierarchical_query/cte/join_builder.rb
125
+ - lib/active_record/hierarchical_query/cte/non_recursive_term.rb
126
+ - lib/active_record/hierarchical_query/cte/orderings.rb
127
+ - lib/active_record/hierarchical_query/cte/query.rb
128
+ - lib/active_record/hierarchical_query/cte/recursive_term.rb
129
+ - lib/active_record/hierarchical_query/cte/union_term.rb
130
+ - lib/active_record/hierarchical_query/version.rb
131
+ - lib/arel/nodes/postgresql.rb
132
+ - spec/active_record/hierarchical_query_spec.rb
133
+ - spec/database.travis.yml
134
+ - spec/database.yml
135
+ - spec/schema.rb
136
+ - spec/spec_helper.rb
137
+ - spec/support/models.rb
138
+ - README.md
139
+ - LICENSE.txt
140
+ homepage: https://github.com/take-five/activerecord-hierarchical_query
141
+ licenses:
142
+ - MIT
143
+ metadata: {}
144
+ post_install_message:
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubyforge_project:
160
+ rubygems_version: 2.1.10
161
+ signing_key:
162
+ specification_version: 4
163
+ summary: Recursively traverse trees using a single SQL query
164
+ test_files:
165
+ - spec/active_record/hierarchical_query_spec.rb
166
+ - spec/database.travis.yml
167
+ - spec/database.yml
168
+ - spec/schema.rb
169
+ - spec/spec_helper.rb
170
+ - spec/support/models.rb
171
+ has_rdoc: