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.
- 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
|