activerecord-hierarchical_query 0.0.1 → 0.0.2
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.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/lib/active_record/hierarchical_query/adapters.rb +19 -5
- data/lib/active_record/hierarchical_query/adapters/abstract.rb +41 -0
- data/lib/active_record/hierarchical_query/adapters/postgresql.rb +8 -18
- data/lib/active_record/hierarchical_query/builder.rb +5 -4
- data/lib/active_record/hierarchical_query/cte/non_recursive_term.rb +3 -14
- data/lib/active_record/hierarchical_query/cte/query.rb +6 -35
- data/lib/active_record/hierarchical_query/cte/recursive_term.rb +3 -17
- data/lib/active_record/hierarchical_query/join_builder.rb +47 -0
- data/lib/active_record/hierarchical_query/version.rb +1 -1
- data/lib/active_record/hierarchical_query/visitors/orderings.rb +87 -0
- data/lib/active_record/hierarchical_query/visitors/postgresql/cycle_detector.rb +49 -0
- data/lib/active_record/hierarchical_query/visitors/postgresql/orderings.rb +70 -0
- data/lib/active_record/hierarchical_query/visitors/visitor.rb +17 -0
- data/spec/database.travis.yml +5 -4
- data/spec/database.yml +13 -4
- data/spec/spec_helper.rb +7 -9
- metadata +25 -36
- data/lib/active_record/hierarchical_query/cte/cycle_detector.rb +0 -63
- data/lib/active_record/hierarchical_query/cte/join_builder.rb +0 -55
- data/lib/active_record/hierarchical_query/cte/orderings.rb +0 -108
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e317315d0b567e0880277e328380478df97fecd7
|
4
|
+
data.tar.gz: f1918a2d4c9a9e8a84f59b7af8fec42d6fb57dfa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aad9047098ff944791fd13f5fed4a120e39195d3f9f4d08a17f5e734dfc8e7409e2a034acec7e0200df336c4a530003ebca1d68c2a494fac9629db3d253ace5f
|
7
|
+
data.tar.gz: 890c7ebfff9df375347f00c061ed213167fe8e09da1cf567996afc9ca5417f264215c270e987a9e8a16925284d0f9cbafc842c2f26d8c8fd142dfd29761e88d9
|
data/README.md
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
[](https://codeclimate.com/github/take-five/activerecord-hierarchical_query)
|
5
5
|
[](https://coveralls.io/r/take-five/activerecord-hierarchical_query)
|
6
6
|
[](https://gemnasium.com/take-five/activerecord-hierarchical_query)
|
7
|
+
[](http://badge.fury.io/rb/activerecord-hierarchical_query)
|
7
8
|
|
8
9
|
Create hierarchical queries using simple DSL, recursively traverse trees using single SQL query.
|
9
10
|
|
@@ -1,20 +1,34 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
+
require 'active_support/core_ext/hash/keys'
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
5
|
+
|
3
6
|
module ActiveRecord
|
4
7
|
module HierarchicalQuery
|
5
8
|
module Adapters
|
6
9
|
SUPPORTED_ADAPTERS = %w(PostgreSQL)
|
7
10
|
|
8
|
-
|
11
|
+
ADAPTERS = Hash[
|
12
|
+
:PostgreSQL => :PostgreSQL,
|
13
|
+
:PostGIS => :PostgreSQL,
|
14
|
+
:OracleEnhanced => :Oracle
|
15
|
+
].stringify_keys
|
16
|
+
|
17
|
+
def self.autoload(name, path = name.to_s.underscore)
|
18
|
+
super name, "active_record/hierarchical_query/adapters/#{path}"
|
19
|
+
end
|
20
|
+
|
21
|
+
autoload :PostgreSQL, 'postgresql'
|
22
|
+
autoload :Oracle
|
9
23
|
|
10
24
|
def self.lookup(klass)
|
11
25
|
name = klass.connection.adapter_name
|
12
26
|
|
13
|
-
raise 'Your database does not support recursive queries' unless
|
14
|
-
|
27
|
+
raise 'Your database %s does not support recursive queries' % name unless
|
28
|
+
ADAPTERS.key?(name)
|
15
29
|
|
16
|
-
const_get(name)
|
30
|
+
const_get(ADAPTERS[name])
|
17
31
|
end
|
18
32
|
end # module Adapters
|
19
33
|
end # module HierarchicalQuery
|
20
|
-
end # module ActiveRecord
|
34
|
+
end # module ActiveRecord
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'active_record/hierarchical_query/visitors/visitor'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module HierarchicalQuery
|
7
|
+
module Adapters
|
8
|
+
# @api private
|
9
|
+
class Abstract
|
10
|
+
attr_reader :query,
|
11
|
+
:table
|
12
|
+
|
13
|
+
delegate :klass, :to => :query
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def visitors(*visitors_classes)
|
17
|
+
define_method :visitors do
|
18
|
+
@visitors ||= visitors_classes.map { |klass| klass.new(@query) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
|
24
|
+
def initialize(query)
|
25
|
+
@query = query
|
26
|
+
@table = klass.arel_table
|
27
|
+
end
|
28
|
+
|
29
|
+
# @example
|
30
|
+
# visit(:recursive_term, recursive_term.arel)
|
31
|
+
def visit(kind, object)
|
32
|
+
method_name = "visit_#{kind}"
|
33
|
+
|
34
|
+
visitors.reduce(object) do |*, visitor|
|
35
|
+
visitor.send(method_name, object) if visitor.respond_to?(method_name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,28 +1,18 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'arel/nodes/postgresql'
|
4
|
+
|
5
|
+
require 'active_record/hierarchical_query/adapters/abstract'
|
6
|
+
require 'active_record/hierarchical_query/visitors/postgresql/cycle_detector'
|
7
|
+
require 'active_record/hierarchical_query/visitors/postgresql/orderings'
|
4
8
|
|
5
9
|
module ActiveRecord
|
6
10
|
module HierarchicalQuery
|
7
11
|
module Adapters
|
8
12
|
# @api private
|
9
|
-
class PostgreSQL
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
delegate :klass, :to => :builder
|
14
|
-
delegate :build_join, :to => :@query
|
15
|
-
|
16
|
-
# @param [ActiveRecord::HierarchicalQuery::Builder] builder
|
17
|
-
def initialize(builder)
|
18
|
-
@builder = builder
|
19
|
-
@table = klass.arel_table
|
20
|
-
@query = CTE::Query.new(builder)
|
21
|
-
end
|
22
|
-
|
23
|
-
def prior
|
24
|
-
@query.recursive_table
|
25
|
-
end
|
13
|
+
class PostgreSQL < Abstract
|
14
|
+
visitors Visitors::PostgreSQL::CycleDetector,
|
15
|
+
Visitors::PostgreSQL::Orderings
|
26
16
|
end # class PostgreSQL
|
27
17
|
end # module Adapters
|
28
18
|
end # module HierarchicalQuery
|
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'active_support/core_ext/array/extract_options'
|
4
4
|
|
5
|
-
require 'active_record/hierarchical_query/
|
5
|
+
require 'active_record/hierarchical_query/cte/query'
|
6
|
+
require 'active_record/hierarchical_query/join_builder'
|
6
7
|
|
7
8
|
module ActiveRecord
|
8
9
|
module HierarchicalQuery
|
@@ -22,7 +23,7 @@ module ActiveRecord
|
|
22
23
|
|
23
24
|
def initialize(klass)
|
24
25
|
@klass = klass
|
25
|
-
@
|
26
|
+
@query = CTE::Query.new(self)
|
26
27
|
|
27
28
|
@start_with_value = nil
|
28
29
|
@connect_by_value = nil
|
@@ -239,7 +240,7 @@ module ActiveRecord
|
|
239
240
|
#
|
240
241
|
# @return [Arel::Table]
|
241
242
|
def prior
|
242
|
-
@
|
243
|
+
@query.recursive_table
|
243
244
|
end
|
244
245
|
alias_method :previous, :prior
|
245
246
|
|
@@ -269,7 +270,7 @@ module ActiveRecord
|
|
269
270
|
|
270
271
|
table_alias = join_options.fetch(:as, "#{table.name}__recursive")
|
271
272
|
|
272
|
-
|
273
|
+
JoinBuilder.new(@query, relation, table_alias).build
|
273
274
|
end
|
274
275
|
|
275
276
|
private
|
@@ -1,15 +1,13 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
require 'arel/nodes/postgresql'
|
4
|
-
|
5
3
|
module ActiveRecord
|
6
4
|
module HierarchicalQuery
|
7
5
|
module CTE
|
8
6
|
class NonRecursiveTerm
|
9
|
-
DISALLOWED_CLAUSES = :order, :limit, :offset
|
7
|
+
DISALLOWED_CLAUSES = :order, :limit, :offset, :group, :having
|
10
8
|
|
11
9
|
attr_reader :query
|
12
|
-
delegate :builder, :
|
10
|
+
delegate :builder, :adapter, :to => :query
|
13
11
|
delegate :start_with_value, :klass, :to => :builder
|
14
12
|
|
15
13
|
# @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
|
@@ -19,25 +17,16 @@ module ActiveRecord
|
|
19
17
|
|
20
18
|
def arel
|
21
19
|
arel = scope.select(query.columns)
|
22
|
-
.select(ordering_column)
|
23
20
|
.except(*DISALLOWED_CLAUSES)
|
24
21
|
.arel
|
25
22
|
|
26
|
-
|
23
|
+
adapter.visit(:non_recursive, arel)
|
27
24
|
end
|
28
25
|
|
29
26
|
private
|
30
27
|
def scope
|
31
28
|
start_with_value || klass.__send__(HierarchicalQuery::DELEGATOR_SCOPE)
|
32
29
|
end
|
33
|
-
|
34
|
-
def ordering_column
|
35
|
-
if orderings.any?
|
36
|
-
Arel::Nodes::PostgresArray.new([orderings.row_number_expression]).as(orderings.column_name)
|
37
|
-
else
|
38
|
-
[]
|
39
|
-
end
|
40
|
-
end
|
41
30
|
end # class NonRecursiveTerm
|
42
31
|
end
|
43
32
|
end
|
@@ -1,37 +1,30 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
+
require 'active_record/hierarchical_query/adapters'
|
3
4
|
require 'active_record/hierarchical_query/cte/columns'
|
4
|
-
require 'active_record/hierarchical_query/cte/cycle_detector'
|
5
|
-
require 'active_record/hierarchical_query/cte/join_builder'
|
6
|
-
require 'active_record/hierarchical_query/cte/orderings'
|
7
5
|
require 'active_record/hierarchical_query/cte/union_term'
|
8
6
|
|
9
7
|
module ActiveRecord
|
10
8
|
module HierarchicalQuery
|
11
9
|
module CTE
|
10
|
+
# CTE query builder
|
12
11
|
class Query
|
13
12
|
attr_reader :builder,
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:cycle_detector
|
13
|
+
:adapter,
|
14
|
+
:columns
|
17
15
|
|
18
16
|
delegate :klass, :table, :to => :builder
|
19
17
|
|
20
18
|
# @param [ActiveRecord::HierarchicalQuery::Builder] builder
|
21
19
|
def initialize(builder)
|
22
20
|
@builder = builder
|
23
|
-
@
|
21
|
+
@adapter = Adapters.lookup(klass).new(self)
|
24
22
|
@columns = Columns.new(self)
|
25
|
-
@cycle_detector = CycleDetector.new(self)
|
26
|
-
end
|
27
|
-
|
28
|
-
def build_join(relation, subquery_alias)
|
29
|
-
JoinBuilder.new(self, relation, subquery_alias).build
|
30
23
|
end
|
31
24
|
|
32
25
|
# @return [Arel::SelectManager]
|
33
26
|
def arel
|
34
|
-
|
27
|
+
adapter.visit(:cte, build_arel)
|
35
28
|
end
|
36
29
|
|
37
30
|
# @return [Arel::Table]
|
@@ -43,10 +36,6 @@ module ActiveRecord
|
|
43
36
|
builder.connect_by_value[recursive_table, table]
|
44
37
|
end
|
45
38
|
|
46
|
-
def order_clause
|
47
|
-
recursive_table[orderings.column_name].asc
|
48
|
-
end
|
49
|
-
|
50
39
|
private
|
51
40
|
# "categories__recursive" AS (
|
52
41
|
# SELECT ... FROM "categories"
|
@@ -62,16 +51,6 @@ module ActiveRecord
|
|
62
51
|
UnionTerm.new(self)
|
63
52
|
end
|
64
53
|
|
65
|
-
def apply_ordering
|
66
|
-
arel = yield
|
67
|
-
|
68
|
-
if should_order?
|
69
|
-
apply_ordering_to(arel)
|
70
|
-
else
|
71
|
-
arel
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
54
|
def build_arel
|
76
55
|
Arel::SelectManager.new(table.engine).
|
77
56
|
with(:recursive, with_query).
|
@@ -80,14 +59,6 @@ module ActiveRecord
|
|
80
59
|
take(builder.limit_value).
|
81
60
|
skip(builder.offset_value)
|
82
61
|
end
|
83
|
-
|
84
|
-
def should_order?
|
85
|
-
orderings.any? && builder.limit_value || builder.offset_value
|
86
|
-
end
|
87
|
-
|
88
|
-
def apply_ordering_to(select_manager)
|
89
|
-
select_manager.order(order_clause)
|
90
|
-
end
|
91
62
|
end
|
92
63
|
end
|
93
64
|
end
|
@@ -5,10 +5,9 @@ module ActiveRecord
|
|
5
5
|
# @return [ActiveRecord::HierarchicalQuery::CTE::Query]
|
6
6
|
attr_reader :query
|
7
7
|
|
8
|
-
delegate :
|
9
|
-
:cycle_detector,
|
10
|
-
:recursive_table,
|
8
|
+
delegate :recursive_table,
|
11
9
|
:join_conditions,
|
10
|
+
:adapter,
|
12
11
|
:to => :query
|
13
12
|
|
14
13
|
# @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
|
@@ -18,29 +17,16 @@ module ActiveRecord
|
|
18
17
|
|
19
18
|
def arel
|
20
19
|
arel = scope.select(query.columns)
|
21
|
-
.select(ordering_column)
|
22
20
|
.arel
|
23
21
|
.join(recursive_table).on(join_conditions)
|
24
22
|
|
25
|
-
|
23
|
+
adapter.visit(:recursive, arel)
|
26
24
|
end
|
27
25
|
|
28
26
|
private
|
29
27
|
def scope
|
30
28
|
query.builder.child_scope_value
|
31
29
|
end
|
32
|
-
|
33
|
-
def ordering_column
|
34
|
-
if orderings.any?
|
35
|
-
Arel::Nodes::ArrayConcat.new(parent_ordering_column, orderings.row_number_expression)
|
36
|
-
else
|
37
|
-
[]
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def parent_ordering_column
|
42
|
-
recursive_table[orderings.column_name]
|
43
|
-
end
|
44
30
|
end
|
45
31
|
end
|
46
32
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module HierarchicalQuery
|
3
|
+
class JoinBuilder
|
4
|
+
delegate :adapter, :to => :@query
|
5
|
+
|
6
|
+
# @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
|
7
|
+
# @param [ActiveRecord::Relation] join_to
|
8
|
+
# @param [#to_s] subquery_alias
|
9
|
+
def initialize(query, join_to, subquery_alias)
|
10
|
+
@query = query
|
11
|
+
@relation = join_to
|
12
|
+
@alias = Arel::Table.new(subquery_alias, ActiveRecord::Base)
|
13
|
+
end
|
14
|
+
|
15
|
+
def build
|
16
|
+
relation = @relation.joins(inner_join.to_sql)
|
17
|
+
|
18
|
+
adapter.visit(:joined_relation, relation)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def inner_join
|
23
|
+
Arel::Nodes::InnerJoin.new(aliased_subquery, constraint)
|
24
|
+
end
|
25
|
+
|
26
|
+
def primary_key
|
27
|
+
@relation.table[@relation.klass.primary_key]
|
28
|
+
end
|
29
|
+
|
30
|
+
def foreign_key
|
31
|
+
@alias[@query.klass.primary_key]
|
32
|
+
end
|
33
|
+
|
34
|
+
def constraint
|
35
|
+
Arel::Nodes::On.new(primary_key.eq(foreign_key))
|
36
|
+
end
|
37
|
+
|
38
|
+
def subquery
|
39
|
+
Arel::Nodes::Grouping.new(@query.arel.ast)
|
40
|
+
end
|
41
|
+
|
42
|
+
def aliased_subquery
|
43
|
+
Arel::Nodes::As.new(subquery, @alias)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module HierarchicalQuery
|
3
|
+
module Visitors
|
4
|
+
class Orderings < Visitor
|
5
|
+
ORDERING_COLUMN_ALIAS = '__order_column'
|
6
|
+
|
7
|
+
delegate :recursive_table, :to => :query
|
8
|
+
|
9
|
+
def visit_joined_relation(relation)
|
10
|
+
visit(relation) do
|
11
|
+
relation.order(order_clause)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def visit_cte(select_manager)
|
16
|
+
if should_order_cte?
|
17
|
+
select_manager.order(order_clause)
|
18
|
+
else
|
19
|
+
select_manager
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
def visit(object)
|
25
|
+
if orderings.any?
|
26
|
+
yield
|
27
|
+
else
|
28
|
+
object
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def order_clause
|
33
|
+
recursive_table[column_name].asc
|
34
|
+
end
|
35
|
+
|
36
|
+
def should_order_cte?
|
37
|
+
orderings.any? && builder.limit_value || builder.offset_value
|
38
|
+
end
|
39
|
+
|
40
|
+
def orderings
|
41
|
+
@orderings ||= builder.order_values.each_with_object([]) do |value, orderings|
|
42
|
+
orderings.concat Array.wrap(as_orderings(value))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def column_name
|
47
|
+
ORDERING_COLUMN_ALIAS
|
48
|
+
end
|
49
|
+
|
50
|
+
def as_orderings(value)
|
51
|
+
case value
|
52
|
+
when Arel::Nodes::Ordering
|
53
|
+
value
|
54
|
+
|
55
|
+
when Arel::Nodes::Node, Arel::Attributes::Attribute
|
56
|
+
value.asc
|
57
|
+
|
58
|
+
when Symbol
|
59
|
+
table[value].asc
|
60
|
+
|
61
|
+
when Hash
|
62
|
+
value.map { |field, dir| table[field].send(dir) }
|
63
|
+
|
64
|
+
when String
|
65
|
+
value.split(',').map do |expr|
|
66
|
+
string_as_ordering(expr)
|
67
|
+
end
|
68
|
+
|
69
|
+
else
|
70
|
+
raise 'Unknown expression in ORDER BY SIBLINGS clause'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def string_as_ordering(expr)
|
75
|
+
expr.strip!
|
76
|
+
|
77
|
+
if expr.gsub!(/\bdesc\z/i, '')
|
78
|
+
Arel.sql(expr).desc
|
79
|
+
else
|
80
|
+
expr.gsub!(/\basc\z/i, '')
|
81
|
+
Arel.sql(expr).asc
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module HierarchicalQuery
|
3
|
+
module Visitors
|
4
|
+
module PostgreSQL
|
5
|
+
class CycleDetector < Visitor
|
6
|
+
COLUMN_NAME = '__path'.freeze
|
7
|
+
|
8
|
+
def visit_non_recursive(arel)
|
9
|
+
if enabled?
|
10
|
+
arel.project Arel::Nodes::PostgresArray.new([primary_key]).as(column_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
arel
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit_recursive(arel)
|
17
|
+
if enabled?
|
18
|
+
arel.project Arel::Nodes::ArrayConcat.new(parent_column, primary_key)
|
19
|
+
arel.constraints << Arel::Nodes::Not.new(primary_key.eq(any(parent_column)))
|
20
|
+
end
|
21
|
+
|
22
|
+
arel
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def enabled?
|
27
|
+
builder.nocycle_value
|
28
|
+
end
|
29
|
+
|
30
|
+
def column_name
|
31
|
+
COLUMN_NAME
|
32
|
+
end
|
33
|
+
|
34
|
+
def parent_column
|
35
|
+
query.recursive_table[column_name]
|
36
|
+
end
|
37
|
+
|
38
|
+
def primary_key
|
39
|
+
table[klass.primary_key]
|
40
|
+
end
|
41
|
+
|
42
|
+
def any(argument)
|
43
|
+
Arel::Nodes::NamedFunction.new('ANY', [argument])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'active_record/hierarchical_query/visitors/orderings'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module HierarchicalQuery
|
5
|
+
module Visitors
|
6
|
+
module PostgreSQL
|
7
|
+
class Orderings < Visitors::Orderings
|
8
|
+
NATURAL_SORT_TYPES = Set[
|
9
|
+
:integer, :float, :decimal,
|
10
|
+
:datetime, :timestamp, :time, :date,
|
11
|
+
:boolean, :itet, :cidr, :ltree
|
12
|
+
]
|
13
|
+
|
14
|
+
delegate :first, :to => :orderings
|
15
|
+
|
16
|
+
def visit_non_recursive(arel)
|
17
|
+
project(arel) do
|
18
|
+
Arel::Nodes::PostgresArray.new([row_number_expression]).as(column_name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def visit_recursive(arel)
|
23
|
+
project(arel) do
|
24
|
+
Arel::Nodes::ArrayConcat.new(recursive_table[column_name], row_number_expression)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def project(arel)
|
30
|
+
visit(arel) { arel.project(yield) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def row_number_expression
|
34
|
+
if raw_ordering?
|
35
|
+
order_attribute
|
36
|
+
else
|
37
|
+
Arel.sql("ROW_NUMBER() OVER (ORDER BY #{orderings.map(&:to_sql).join(', ')})")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def raw_ordering?
|
42
|
+
ordered_by_attribute? &&
|
43
|
+
(column = order_column) &&
|
44
|
+
NATURAL_SORT_TYPES.include?(column.type)
|
45
|
+
end
|
46
|
+
|
47
|
+
def ordered_by_attribute?
|
48
|
+
orderings.one? && first.ascending? && order_attribute.is_a?(Arel::Attributes::Attribute)
|
49
|
+
end
|
50
|
+
|
51
|
+
def order_attribute
|
52
|
+
first.expr
|
53
|
+
end
|
54
|
+
|
55
|
+
def order_column
|
56
|
+
table = order_attribute.relation
|
57
|
+
|
58
|
+
if table.engine == ActiveRecord::Base
|
59
|
+
columns = table.engine.connection_pool.columns_hash[table.name]
|
60
|
+
else
|
61
|
+
columns = table.engine.columns_hash
|
62
|
+
end
|
63
|
+
|
64
|
+
columns[order_attribute.name.to_s]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module HierarchicalQuery
|
3
|
+
module Visitors
|
4
|
+
# @api private
|
5
|
+
class Visitor
|
6
|
+
attr_reader :query
|
7
|
+
|
8
|
+
delegate :builder, :to => :query
|
9
|
+
delegate :klass, :table, :to => :builder
|
10
|
+
|
11
|
+
def initialize(query)
|
12
|
+
@query = query
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/spec/database.travis.yml
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
pg:
|
2
|
+
adapter: postgresql
|
3
|
+
database: hierarchical_query_test
|
4
|
+
username: postgres
|
5
|
+
min_messages: ERROR
|
data/spec/database.yml
CHANGED
@@ -1,4 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
pg:
|
2
|
+
adapter: postgresql
|
3
|
+
database: hierarchical_query_test
|
4
|
+
username: vagrant
|
5
|
+
min_messages: ERROR
|
6
|
+
|
7
|
+
oracle:
|
8
|
+
adapter: oracle_enhanced
|
9
|
+
host: localhost
|
10
|
+
port: 1521
|
11
|
+
database: xe
|
12
|
+
username: system
|
13
|
+
password: manager
|
data/spec/spec_helper.rb
CHANGED
@@ -2,17 +2,20 @@
|
|
2
2
|
require 'pathname'
|
3
3
|
require 'logger'
|
4
4
|
|
5
|
-
|
5
|
+
ENV['TZ'] = 'UTC'
|
6
|
+
ENV['DB'] ||= 'pg'
|
6
7
|
|
7
|
-
|
8
|
+
SPEC_ROOT = Pathname.new(File.dirname(__FILE__))
|
8
9
|
|
9
|
-
|
10
|
+
require 'bundler'
|
11
|
+
Bundler.setup(:default, ENV['TRAVIS'] ? :travis : :local, ENV['DB'].to_sym)
|
10
12
|
|
11
13
|
require 'rspec'
|
12
14
|
require 'database_cleaner'
|
13
15
|
require 'active_record'
|
14
16
|
|
15
|
-
ActiveRecord::Base.
|
17
|
+
ActiveRecord::Base.configurations = YAML.load(SPEC_ROOT.join('database.yml').read)
|
18
|
+
ActiveRecord::Base.establish_connection(ENV['DB'].to_sym)
|
16
19
|
ActiveRecord::Base.logger = Logger.new(ENV['DEBUG'] ? $stderr : '/dev/null')
|
17
20
|
ActiveRecord::Base.logger.formatter = proc do |severity, datetime, progname, msg|
|
18
21
|
"#{datetime.strftime('%H:%M:%S.%L')}: #{msg}\n"
|
@@ -35,11 +38,6 @@ RSpec.configure do |config|
|
|
35
38
|
# --seed 1234
|
36
39
|
config.order = 'random'
|
37
40
|
|
38
|
-
config.before(:suite) do
|
39
|
-
DatabaseCleaner.strategy = :transaction
|
40
|
-
DatabaseCleaner.clean_with(:truncation)
|
41
|
-
end
|
42
|
-
|
43
41
|
config.around(:each) do |example|
|
44
42
|
DatabaseCleaner.start
|
45
43
|
example.run
|
metadata
CHANGED
@@ -1,111 +1,97 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-hierarchical_query
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexei Mikhailov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-05-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 3.1.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 3.1.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '1.5'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.5'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - ~>
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: 10.1.1
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - ~>
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 10.1.1
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rspec
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - ~>
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: 2.14.1
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - ~>
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
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
69
|
- !ruby/object:Gem::Dependency
|
84
70
|
name: database_cleaner
|
85
71
|
requirement: !ruby/object:Gem::Requirement
|
86
72
|
requirements:
|
87
|
-
- - ~>
|
73
|
+
- - "~>"
|
88
74
|
- !ruby/object:Gem::Version
|
89
75
|
version: 1.2.0
|
90
76
|
type: :development
|
91
77
|
prerelease: false
|
92
78
|
version_requirements: !ruby/object:Gem::Requirement
|
93
79
|
requirements:
|
94
|
-
- - ~>
|
80
|
+
- - "~>"
|
95
81
|
- !ruby/object:Gem::Version
|
96
82
|
version: 1.2.0
|
97
83
|
- !ruby/object:Gem::Dependency
|
98
84
|
name: simplecov
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
100
86
|
requirements:
|
101
|
-
- - ~>
|
87
|
+
- - "~>"
|
102
88
|
- !ruby/object:Gem::Version
|
103
89
|
version: 0.8.2
|
104
90
|
type: :development
|
105
91
|
prerelease: false
|
106
92
|
version_requirements: !ruby/object:Gem::Requirement
|
107
93
|
requirements:
|
108
|
-
- - ~>
|
94
|
+
- - "~>"
|
109
95
|
- !ruby/object:Gem::Version
|
110
96
|
version: 0.8.2
|
111
97
|
description:
|
@@ -115,19 +101,24 @@ executables: []
|
|
115
101
|
extensions: []
|
116
102
|
extra_rdoc_files: []
|
117
103
|
files:
|
104
|
+
- LICENSE.txt
|
105
|
+
- README.md
|
118
106
|
- lib/active_record/hierarchical_query.rb
|
119
107
|
- lib/active_record/hierarchical_query/adapters.rb
|
108
|
+
- lib/active_record/hierarchical_query/adapters/abstract.rb
|
120
109
|
- lib/active_record/hierarchical_query/adapters/postgresql.rb
|
121
110
|
- lib/active_record/hierarchical_query/builder.rb
|
122
111
|
- 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
112
|
- lib/active_record/hierarchical_query/cte/non_recursive_term.rb
|
126
|
-
- lib/active_record/hierarchical_query/cte/orderings.rb
|
127
113
|
- lib/active_record/hierarchical_query/cte/query.rb
|
128
114
|
- lib/active_record/hierarchical_query/cte/recursive_term.rb
|
129
115
|
- lib/active_record/hierarchical_query/cte/union_term.rb
|
116
|
+
- lib/active_record/hierarchical_query/join_builder.rb
|
130
117
|
- lib/active_record/hierarchical_query/version.rb
|
118
|
+
- lib/active_record/hierarchical_query/visitors/orderings.rb
|
119
|
+
- lib/active_record/hierarchical_query/visitors/postgresql/cycle_detector.rb
|
120
|
+
- lib/active_record/hierarchical_query/visitors/postgresql/orderings.rb
|
121
|
+
- lib/active_record/hierarchical_query/visitors/visitor.rb
|
131
122
|
- lib/arel/nodes/postgresql.rb
|
132
123
|
- spec/active_record/hierarchical_query_spec.rb
|
133
124
|
- spec/database.travis.yml
|
@@ -135,8 +126,6 @@ files:
|
|
135
126
|
- spec/schema.rb
|
136
127
|
- spec/spec_helper.rb
|
137
128
|
- spec/support/models.rb
|
138
|
-
- README.md
|
139
|
-
- LICENSE.txt
|
140
129
|
homepage: https://github.com/take-five/activerecord-hierarchical_query
|
141
130
|
licenses:
|
142
131
|
- MIT
|
@@ -147,17 +136,17 @@ require_paths:
|
|
147
136
|
- lib
|
148
137
|
required_ruby_version: !ruby/object:Gem::Requirement
|
149
138
|
requirements:
|
150
|
-
- -
|
139
|
+
- - ">="
|
151
140
|
- !ruby/object:Gem::Version
|
152
141
|
version: '0'
|
153
142
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
143
|
requirements:
|
155
|
-
- -
|
144
|
+
- - ">="
|
156
145
|
- !ruby/object:Gem::Version
|
157
146
|
version: '0'
|
158
147
|
requirements: []
|
159
148
|
rubyforge_project:
|
160
|
-
rubygems_version: 2.
|
149
|
+
rubygems_version: 2.2.2
|
161
150
|
signing_key:
|
162
151
|
specification_version: 4
|
163
152
|
summary: Recursively traverse trees using a single SQL query
|
@@ -1,63 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
|
3
|
-
module ActiveRecord
|
4
|
-
module HierarchicalQuery
|
5
|
-
module CTE
|
6
|
-
class CycleDetector
|
7
|
-
COLUMN_NAME = '__path'.freeze
|
8
|
-
|
9
|
-
attr_reader :query
|
10
|
-
|
11
|
-
delegate :builder, :to => :query
|
12
|
-
delegate :klass, :table, :to => :builder
|
13
|
-
|
14
|
-
# @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
|
15
|
-
def initialize(query)
|
16
|
-
@query = query
|
17
|
-
end
|
18
|
-
|
19
|
-
def column_name
|
20
|
-
COLUMN_NAME
|
21
|
-
end
|
22
|
-
|
23
|
-
def visit_non_recursive(arel)
|
24
|
-
visit arel do
|
25
|
-
arel.project Arel::Nodes::PostgresArray.new([primary_key]).as(column_name)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def visit_recursive(arel)
|
30
|
-
visit arel do
|
31
|
-
arel.project Arel::Nodes::ArrayConcat.new(parent_column, primary_key)
|
32
|
-
arel.constraints << Arel::Nodes::Not.new(primary_key.eq(any(parent_column)))
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
def enabled?
|
38
|
-
builder.nocycle_value
|
39
|
-
end
|
40
|
-
|
41
|
-
def parent_column
|
42
|
-
query.recursive_table[column_name]
|
43
|
-
end
|
44
|
-
|
45
|
-
def primary_key
|
46
|
-
table[klass.primary_key]
|
47
|
-
end
|
48
|
-
|
49
|
-
def visit(object)
|
50
|
-
if enabled?
|
51
|
-
yield
|
52
|
-
end
|
53
|
-
|
54
|
-
object
|
55
|
-
end
|
56
|
-
|
57
|
-
def any(argument)
|
58
|
-
Arel::Nodes::NamedFunction.new('ANY', [argument])
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
@@ -1,55 +0,0 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
module HierarchicalQuery
|
3
|
-
module CTE
|
4
|
-
class JoinBuilder
|
5
|
-
# @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
|
6
|
-
# @param [ActiveRecord::Relation] join_to
|
7
|
-
# @param [#to_s] subquery_alias
|
8
|
-
def initialize(query, join_to, subquery_alias)
|
9
|
-
@query = query
|
10
|
-
@relation = join_to
|
11
|
-
@alias = Arel::Table.new(subquery_alias, ActiveRecord::Base)
|
12
|
-
end
|
13
|
-
|
14
|
-
def build
|
15
|
-
apply_ordering { @relation.joins(inner_join.to_sql) }
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
def inner_join
|
20
|
-
Arel::Nodes::InnerJoin.new(aliased_subquery, constraint)
|
21
|
-
end
|
22
|
-
|
23
|
-
def primary_key
|
24
|
-
@relation.table[@relation.klass.primary_key]
|
25
|
-
end
|
26
|
-
|
27
|
-
def foreign_key
|
28
|
-
@alias[@query.klass.primary_key]
|
29
|
-
end
|
30
|
-
|
31
|
-
def constraint
|
32
|
-
Arel::Nodes::On.new(primary_key.eq(foreign_key))
|
33
|
-
end
|
34
|
-
|
35
|
-
def subquery
|
36
|
-
Arel::Nodes::Grouping.new(@query.arel.ast)
|
37
|
-
end
|
38
|
-
|
39
|
-
def aliased_subquery
|
40
|
-
Arel::Nodes::As.new(subquery, @alias)
|
41
|
-
end
|
42
|
-
|
43
|
-
def apply_ordering
|
44
|
-
scope = yield
|
45
|
-
|
46
|
-
if @query.orderings.any?
|
47
|
-
scope.order(@query.order_clause)
|
48
|
-
else
|
49
|
-
scope
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
@@ -1,108 +0,0 @@
|
|
1
|
-
require 'set'
|
2
|
-
require 'active_support/core_ext/array/wrap'
|
3
|
-
|
4
|
-
module ActiveRecord
|
5
|
-
module HierarchicalQuery
|
6
|
-
module CTE
|
7
|
-
class Orderings
|
8
|
-
include Enumerable
|
9
|
-
|
10
|
-
ORDERING_COLUMN_ALIAS = '__order_column'
|
11
|
-
|
12
|
-
NATURAL_SORT_TYPES = Set[
|
13
|
-
:integer, :float, :decimal,
|
14
|
-
:datetime, :timestamp, :time, :date,
|
15
|
-
:boolean, :itet, :cidr, :ltree
|
16
|
-
]
|
17
|
-
|
18
|
-
delegate :table, :to => :@builder
|
19
|
-
delegate :each, :to => :arel_nodes
|
20
|
-
|
21
|
-
# @param [ActiveRecord::HierarchicalQuery::Builder] builder
|
22
|
-
def initialize(builder)
|
23
|
-
@builder = builder
|
24
|
-
end
|
25
|
-
|
26
|
-
def arel_nodes
|
27
|
-
@arel_nodes ||= @builder.order_values.each_with_object([]) do |value, orderings|
|
28
|
-
orderings.concat Array.wrap(as_orderings(value))
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def row_number_expression
|
33
|
-
if raw_ordering?
|
34
|
-
order_attribute
|
35
|
-
else
|
36
|
-
Arel.sql("ROW_NUMBER() OVER (ORDER BY #{arel_nodes.map(&:to_sql).join(', ')})")
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def column_name
|
41
|
-
ORDERING_COLUMN_ALIAS
|
42
|
-
end
|
43
|
-
|
44
|
-
private
|
45
|
-
def as_orderings(value)
|
46
|
-
case value
|
47
|
-
when Arel::Nodes::Ordering
|
48
|
-
value
|
49
|
-
|
50
|
-
when Arel::Nodes::Node, Arel::Attributes::Attribute
|
51
|
-
value.asc
|
52
|
-
|
53
|
-
when Symbol
|
54
|
-
table[value].asc
|
55
|
-
|
56
|
-
when Hash
|
57
|
-
value.map { |field, dir| table[field].send(dir) }
|
58
|
-
|
59
|
-
when String
|
60
|
-
value.split(',').map do |expr|
|
61
|
-
string_as_ordering(expr)
|
62
|
-
end
|
63
|
-
|
64
|
-
else
|
65
|
-
raise 'Unknown expression in ORDER BY SIBLINGS clause'
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def string_as_ordering(expr)
|
70
|
-
expr.strip!
|
71
|
-
|
72
|
-
if expr.gsub!(/\s+desc\z/i, '')
|
73
|
-
Arel.sql(expr).desc
|
74
|
-
else
|
75
|
-
expr.gsub!(/\s+asc\z/i, '')
|
76
|
-
Arel.sql(expr).asc
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def raw_ordering?
|
81
|
-
ordered_by_attribute? &&
|
82
|
-
(column = order_column) &&
|
83
|
-
NATURAL_SORT_TYPES.include?(column.type)
|
84
|
-
end
|
85
|
-
|
86
|
-
def ordered_by_attribute?
|
87
|
-
arel_nodes.one? && first.ascending? && order_attribute.is_a?(Arel::Attributes::Attribute)
|
88
|
-
end
|
89
|
-
|
90
|
-
def order_attribute
|
91
|
-
first.expr
|
92
|
-
end
|
93
|
-
|
94
|
-
def order_column
|
95
|
-
table = order_attribute.relation
|
96
|
-
|
97
|
-
if table.engine == ActiveRecord::Base
|
98
|
-
columns = table.engine.connection_pool.columns_hash[table.name]
|
99
|
-
else
|
100
|
-
columns = table.engine.columns_hash
|
101
|
-
end
|
102
|
-
|
103
|
-
columns[order_attribute.name.to_s]
|
104
|
-
end
|
105
|
-
end # class Orderings
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|