activerecord-hierarchical_query 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/LICENSE.txt +22 -0
- data/README.md +266 -0
- data/lib/active_record/hierarchical_query.rb +53 -0
- data/lib/active_record/hierarchical_query/adapters.rb +20 -0
- data/lib/active_record/hierarchical_query/adapters/postgresql.rb +29 -0
- data/lib/active_record/hierarchical_query/builder.rb +289 -0
- data/lib/active_record/hierarchical_query/cte/columns.rb +39 -0
- data/lib/active_record/hierarchical_query/cte/cycle_detector.rb +63 -0
- data/lib/active_record/hierarchical_query/cte/join_builder.rb +55 -0
- data/lib/active_record/hierarchical_query/cte/non_recursive_term.rb +44 -0
- data/lib/active_record/hierarchical_query/cte/orderings.rb +108 -0
- data/lib/active_record/hierarchical_query/cte/query.rb +94 -0
- data/lib/active_record/hierarchical_query/cte/recursive_term.rb +47 -0
- data/lib/active_record/hierarchical_query/cte/union_term.rb +28 -0
- data/lib/active_record/hierarchical_query/version.rb +5 -0
- data/lib/arel/nodes/postgresql.rb +28 -0
- data/spec/active_record/hierarchical_query_spec.rb +193 -0
- data/spec/database.travis.yml +4 -0
- data/spec/database.yml +4 -0
- data/spec/schema.rb +10 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/support/models.rb +39 -0
- metadata +171 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'arel/visitors/depth_first'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module HierarchicalQuery
|
5
|
+
module CTE
|
6
|
+
class Columns
|
7
|
+
# @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
|
8
|
+
def initialize(query)
|
9
|
+
@query = query
|
10
|
+
end
|
11
|
+
|
12
|
+
# returns columns to be selected from both recursive and non-recursive terms
|
13
|
+
def to_a
|
14
|
+
column_names = [@query.klass.primary_key] | connect_by_columns
|
15
|
+
column_names.map { |name| @query.table[name] }
|
16
|
+
end
|
17
|
+
alias_method :to_ary, :to_a
|
18
|
+
|
19
|
+
private
|
20
|
+
def connect_by_columns
|
21
|
+
extractor.call(@query.join_conditions).map { |column| column.name.to_s }
|
22
|
+
end
|
23
|
+
|
24
|
+
def extractor
|
25
|
+
target = []
|
26
|
+
|
27
|
+
->(arel) {
|
28
|
+
visitor = Arel::Visitors::DepthFirst.new do |node|
|
29
|
+
target << node if node.is_a?(Arel::Attributes::Attribute)
|
30
|
+
end
|
31
|
+
visitor.accept(arel)
|
32
|
+
|
33
|
+
target
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,63 @@
|
|
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
|
@@ -0,0 +1,55 @@
|
|
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
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'arel/nodes/postgresql'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module HierarchicalQuery
|
7
|
+
module CTE
|
8
|
+
class NonRecursiveTerm
|
9
|
+
DISALLOWED_CLAUSES = :order, :limit, :offset
|
10
|
+
|
11
|
+
attr_reader :query
|
12
|
+
delegate :builder, :orderings, :cycle_detector, :to => :query
|
13
|
+
delegate :start_with_value, :klass, :to => :builder
|
14
|
+
|
15
|
+
# @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
|
16
|
+
def initialize(query)
|
17
|
+
@query = query
|
18
|
+
end
|
19
|
+
|
20
|
+
def arel
|
21
|
+
arel = scope.select(query.columns)
|
22
|
+
.select(ordering_column)
|
23
|
+
.except(*DISALLOWED_CLAUSES)
|
24
|
+
.arel
|
25
|
+
|
26
|
+
cycle_detector.visit_non_recursive(arel)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def scope
|
31
|
+
start_with_value || klass.__send__(HierarchicalQuery::DELEGATOR_SCOPE)
|
32
|
+
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
|
+
end # class NonRecursiveTerm
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,108 @@
|
|
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
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
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
|
+
require 'active_record/hierarchical_query/cte/union_term'
|
8
|
+
|
9
|
+
module ActiveRecord
|
10
|
+
module HierarchicalQuery
|
11
|
+
module CTE
|
12
|
+
class Query
|
13
|
+
attr_reader :builder,
|
14
|
+
:columns,
|
15
|
+
:orderings,
|
16
|
+
:cycle_detector
|
17
|
+
|
18
|
+
delegate :klass, :table, :to => :builder
|
19
|
+
|
20
|
+
# @param [ActiveRecord::HierarchicalQuery::Builder] builder
|
21
|
+
def initialize(builder)
|
22
|
+
@builder = builder
|
23
|
+
@orderings = Orderings.new(builder)
|
24
|
+
@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
|
+
end
|
31
|
+
|
32
|
+
# @return [Arel::SelectManager]
|
33
|
+
def arel
|
34
|
+
apply_ordering { build_arel }
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Arel::Table]
|
38
|
+
def recursive_table
|
39
|
+
@recursive_table ||= Arel::Table.new("#{table.name}__recursive")
|
40
|
+
end
|
41
|
+
|
42
|
+
def join_conditions
|
43
|
+
builder.connect_by_value[recursive_table, table]
|
44
|
+
end
|
45
|
+
|
46
|
+
def order_clause
|
47
|
+
recursive_table[orderings.column_name].asc
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
# "categories__recursive" AS (
|
52
|
+
# SELECT ... FROM "categories"
|
53
|
+
# UNION ALL
|
54
|
+
# SELECT ... FROM "categories"
|
55
|
+
# INNER JOIN "categories__recursive" ON ...
|
56
|
+
# )
|
57
|
+
def with_query
|
58
|
+
Arel::Nodes::As.new(recursive_table, union_term.arel)
|
59
|
+
end
|
60
|
+
|
61
|
+
def union_term
|
62
|
+
UnionTerm.new(self)
|
63
|
+
end
|
64
|
+
|
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
|
+
def build_arel
|
76
|
+
Arel::SelectManager.new(table.engine).
|
77
|
+
with(:recursive, with_query).
|
78
|
+
from(recursive_table).
|
79
|
+
project(recursive_table[Arel.star]).
|
80
|
+
take(builder.limit_value).
|
81
|
+
skip(builder.offset_value)
|
82
|
+
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
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module HierarchicalQuery
|
3
|
+
module CTE
|
4
|
+
class RecursiveTerm
|
5
|
+
# @return [ActiveRecord::HierarchicalQuery::CTE::Query]
|
6
|
+
attr_reader :query
|
7
|
+
|
8
|
+
delegate :orderings,
|
9
|
+
:cycle_detector,
|
10
|
+
:recursive_table,
|
11
|
+
:join_conditions,
|
12
|
+
:to => :query
|
13
|
+
|
14
|
+
# @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
|
15
|
+
def initialize(query)
|
16
|
+
@query = query
|
17
|
+
end
|
18
|
+
|
19
|
+
def arel
|
20
|
+
arel = scope.select(query.columns)
|
21
|
+
.select(ordering_column)
|
22
|
+
.arel
|
23
|
+
.join(recursive_table).on(join_conditions)
|
24
|
+
|
25
|
+
cycle_detector.visit_recursive(arel)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def scope
|
30
|
+
query.builder.child_scope_value
|
31
|
+
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
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|