cuatlan-activerecord-hierarchical_query 1.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 +356 -0
- data/lib/active_record/hierarchical_query.rb +53 -0
- data/lib/active_record/hierarchical_query/cte/columns.rb +26 -0
- data/lib/active_record/hierarchical_query/cte/cycle_detector.rb +54 -0
- data/lib/active_record/hierarchical_query/cte/non_recursive_term.rb +55 -0
- data/lib/active_record/hierarchical_query/cte/query_builder.rb +88 -0
- data/lib/active_record/hierarchical_query/cte/recursive_term.rb +47 -0
- data/lib/active_record/hierarchical_query/cte/union_term.rb +35 -0
- data/lib/active_record/hierarchical_query/join_builder.rb +107 -0
- data/lib/active_record/hierarchical_query/orderings.rb +119 -0
- data/lib/active_record/hierarchical_query/query.rb +333 -0
- data/lib/active_record/hierarchical_query/version.rb +5 -0
- data/lib/activerecord-hierarchical_query.rb +1 -0
- data/lib/arel/nodes/postgresql.rb +60 -0
- data/spec/active_record/hierarchical_query_spec.rb +338 -0
- data/spec/database.travis.yml +5 -0
- data/spec/database.yml +8 -0
- data/spec/schema.rb +21 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/support/models.rb +50 -0
- metadata +127 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module HierarchicalQuery
|
3
|
+
module CTE
|
4
|
+
class CycleDetector
|
5
|
+
COLUMN_NAME = '__path'.freeze
|
6
|
+
|
7
|
+
delegate :klass, :table, to: :@query
|
8
|
+
|
9
|
+
# @param [ActiveRecord::HierarchicalQuery::Query] query
|
10
|
+
def initialize(query)
|
11
|
+
@query = query
|
12
|
+
end
|
13
|
+
|
14
|
+
def apply_to_non_recursive(arel)
|
15
|
+
if enabled?
|
16
|
+
arel.project Arel::Nodes::PostgresArray.new([primary_key]).as(column_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
arel
|
20
|
+
end
|
21
|
+
|
22
|
+
def apply_to_recursive(arel)
|
23
|
+
if enabled?
|
24
|
+
arel.project Arel::Nodes::ArrayConcat.new(parent_column, primary_key)
|
25
|
+
arel.constraints << Arel::Nodes::Not.new(primary_key.eq(any(parent_column)))
|
26
|
+
end
|
27
|
+
|
28
|
+
arel
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def enabled?
|
33
|
+
@query.nocycle_value
|
34
|
+
end
|
35
|
+
|
36
|
+
def column_name
|
37
|
+
COLUMN_NAME
|
38
|
+
end
|
39
|
+
|
40
|
+
def parent_column
|
41
|
+
@query.recursive_table[column_name]
|
42
|
+
end
|
43
|
+
|
44
|
+
def primary_key
|
45
|
+
table[klass.primary_key]
|
46
|
+
end
|
47
|
+
|
48
|
+
def any(argument)
|
49
|
+
Arel::Nodes::NamedFunction.new('ANY', [argument])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module HierarchicalQuery
|
5
|
+
module CTE
|
6
|
+
class NonRecursiveTerm
|
7
|
+
DISALLOWED_CLAUSES = :order, :limit, :offset, :group, :having
|
8
|
+
|
9
|
+
attr_reader :builder
|
10
|
+
delegate :query, to: :builder
|
11
|
+
|
12
|
+
# @param [ActiveRecord::HierarchicalQuery::CTE::QueryBuilder] builder
|
13
|
+
def initialize(builder)
|
14
|
+
@builder = builder
|
15
|
+
end
|
16
|
+
|
17
|
+
def bind_values
|
18
|
+
scope.bound_attributes
|
19
|
+
end
|
20
|
+
|
21
|
+
def arel
|
22
|
+
arel = scope.arel
|
23
|
+
|
24
|
+
builder.cycle_detector.apply_to_non_recursive(arel)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def scope
|
29
|
+
@scope ||= query.
|
30
|
+
start_with_value.
|
31
|
+
select(columns).
|
32
|
+
except(*DISALLOWED_CLAUSES).
|
33
|
+
reorder(nil)
|
34
|
+
end
|
35
|
+
|
36
|
+
def columns
|
37
|
+
columns = builder.columns.to_a
|
38
|
+
|
39
|
+
if query.orderings.any?
|
40
|
+
columns << ordering
|
41
|
+
end
|
42
|
+
|
43
|
+
columns
|
44
|
+
end
|
45
|
+
|
46
|
+
def ordering
|
47
|
+
value = query.orderings.row_number_expression
|
48
|
+
column_name = query.ordering_column_name
|
49
|
+
|
50
|
+
Arel::Nodes::PostgresArray.new([value]).as(column_name)
|
51
|
+
end
|
52
|
+
end # class NonRecursiveTerm
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,88 @@
|
|
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/union_term'
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
module HierarchicalQuery
|
9
|
+
module CTE
|
10
|
+
# CTE query builder
|
11
|
+
class QueryBuilder
|
12
|
+
attr_reader :query,
|
13
|
+
:columns,
|
14
|
+
:cycle_detector,
|
15
|
+
:options
|
16
|
+
|
17
|
+
delegate :klass, :table, :recursive_table, to: :query
|
18
|
+
|
19
|
+
# @param [ActiveRecord::HierarchicalQuery::Query] query
|
20
|
+
def initialize(query, options: {})
|
21
|
+
@query = query
|
22
|
+
@columns = Columns.new(@query)
|
23
|
+
@cycle_detector = CycleDetector.new(@query)
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
|
27
|
+
def bind_values
|
28
|
+
union_term.bind_values
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Arel::SelectManager]
|
32
|
+
def build_arel
|
33
|
+
build_manager
|
34
|
+
build_select
|
35
|
+
build_limits
|
36
|
+
build_order
|
37
|
+
|
38
|
+
@arel
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def build_manager
|
43
|
+
@arel = Arel::SelectManager.new(table).
|
44
|
+
with(:recursive, with_query).
|
45
|
+
from(recursive_table)
|
46
|
+
end
|
47
|
+
|
48
|
+
# "categories__recursive" AS (
|
49
|
+
# SELECT ... FROM "categories"
|
50
|
+
# UNION ALL
|
51
|
+
# SELECT ... FROM "categories"
|
52
|
+
# INNER JOIN "categories__recursive" ON ...
|
53
|
+
# )
|
54
|
+
def with_query
|
55
|
+
Arel::Nodes::As.new(recursive_table, union_term.arel)
|
56
|
+
end
|
57
|
+
|
58
|
+
def union_term
|
59
|
+
@union_term ||= UnionTerm.new(self, @options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_select
|
63
|
+
if @query.distinct_value
|
64
|
+
@arel.project(recursive_table[Arel.star]).distinct
|
65
|
+
else
|
66
|
+
@arel.project(recursive_table[Arel.star])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_limits
|
71
|
+
@arel.take(query.limit_value).skip(query.offset_value)
|
72
|
+
end
|
73
|
+
|
74
|
+
def build_order
|
75
|
+
@arel.order(order_column.asc) if should_order?
|
76
|
+
end
|
77
|
+
|
78
|
+
def should_order?
|
79
|
+
query.orderings.any? && (query.limit_value || query.offset_value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def order_column
|
83
|
+
recursive_table[query.ordering_column_name]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
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 :builder
|
7
|
+
|
8
|
+
delegate :query, to: :builder
|
9
|
+
|
10
|
+
# @param [ActiveRecord::HierarchicalQuery::CTE::QueryBuilder] builder
|
11
|
+
def initialize(builder)
|
12
|
+
@builder = builder
|
13
|
+
end
|
14
|
+
|
15
|
+
def bind_values
|
16
|
+
scope.bound_attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
def arel
|
20
|
+
arel = scope.arel
|
21
|
+
.join(query.recursive_table).on(query.join_conditions)
|
22
|
+
|
23
|
+
builder.cycle_detector.apply_to_recursive(arel)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def scope
|
28
|
+
@scope ||= query.child_scope_value.select(columns).reorder(nil)
|
29
|
+
end
|
30
|
+
|
31
|
+
def columns
|
32
|
+
columns = builder.columns.to_a
|
33
|
+
columns << ordering if query.orderings.any?
|
34
|
+
columns
|
35
|
+
end
|
36
|
+
|
37
|
+
def ordering
|
38
|
+
column_name = query.ordering_column_name
|
39
|
+
left = query.recursive_table[column_name]
|
40
|
+
right = query.orderings.row_number_expression
|
41
|
+
|
42
|
+
Arel::Nodes::ArrayConcat.new(left, right)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,35 @@
|
|
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::QueryBuilder] builder
|
9
|
+
def initialize(builder, options = {})
|
10
|
+
@builder = builder
|
11
|
+
@union_type = options.fetch(:union_type, :all)
|
12
|
+
end
|
13
|
+
|
14
|
+
def bind_values
|
15
|
+
non_recursive_term.bind_values + recursive_term.bind_values
|
16
|
+
end
|
17
|
+
|
18
|
+
def arel
|
19
|
+
non_recursive_term.arel.union(union_type, recursive_term.arel)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
attr_reader :union_type
|
24
|
+
|
25
|
+
def recursive_term
|
26
|
+
@rt ||= RecursiveTerm.new(@builder)
|
27
|
+
end
|
28
|
+
|
29
|
+
def non_recursive_term
|
30
|
+
@nrt ||= NonRecursiveTerm.new(@builder)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'active_record/hierarchical_query/cte/query_builder'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module HierarchicalQuery
|
5
|
+
class JoinBuilder
|
6
|
+
# @param [ActiveRecord::HierarchicalQuery::Query] query
|
7
|
+
# @param [ActiveRecord::Relation] join_to
|
8
|
+
# @param [#to_s] subquery_alias
|
9
|
+
# @param [Hash] options (:outer_join_hierarchical, :union_type)
|
10
|
+
def initialize(query, join_to, subquery_alias, options = {})
|
11
|
+
@query = query
|
12
|
+
@builder = CTE::QueryBuilder.new(query, options: options)
|
13
|
+
@relation = join_to
|
14
|
+
@alias = Arel::Table.new(subquery_alias)
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
def build
|
19
|
+
if ActiveRecord.gem_version >= Gem::Version.new('5.2.0')
|
20
|
+
relation = @relation
|
21
|
+
|
22
|
+
# add ordering by "__order_column"
|
23
|
+
relation.order_values += order_columns if ordered?
|
24
|
+
|
25
|
+
relation = relation.joins(joined_arel_node)
|
26
|
+
|
27
|
+
relation
|
28
|
+
else
|
29
|
+
relation = @relation
|
30
|
+
|
31
|
+
# add ordering by "__order_column"
|
32
|
+
relation.order_values += order_columns if ordered?
|
33
|
+
|
34
|
+
relation = relation.joins(joined_arel_node)
|
35
|
+
|
36
|
+
# copy bound variables from inner subquery
|
37
|
+
relation.bind_values += bind_values
|
38
|
+
|
39
|
+
relation
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def joined_arel_node
|
45
|
+
@options[:outer_join_hierarchical] == true ? outer_join : inner_join
|
46
|
+
end
|
47
|
+
|
48
|
+
def inner_join
|
49
|
+
Arel::Nodes::InnerJoin.new(aliased_subquery, constraint)
|
50
|
+
end
|
51
|
+
|
52
|
+
def outer_join
|
53
|
+
Arel::Nodes::OuterJoin.new(aliased_subquery, constraint)
|
54
|
+
end
|
55
|
+
|
56
|
+
def aliased_subquery
|
57
|
+
SubqueryAlias.new(subquery, @alias)
|
58
|
+
end
|
59
|
+
|
60
|
+
def subquery
|
61
|
+
Arel::Nodes::Grouping.new(cte_arel.ast)
|
62
|
+
end
|
63
|
+
|
64
|
+
def cte_arel
|
65
|
+
@cte_arel ||= @builder.build_arel
|
66
|
+
end
|
67
|
+
|
68
|
+
def constraint
|
69
|
+
Arel::Nodes::On.new(primary_key.eq(foreign_key))
|
70
|
+
end
|
71
|
+
|
72
|
+
def primary_key
|
73
|
+
@relation.table[@relation.klass.primary_key]
|
74
|
+
end
|
75
|
+
|
76
|
+
def custom_foreign_key
|
77
|
+
@options[:foreign_key]
|
78
|
+
end
|
79
|
+
|
80
|
+
def foreign_key
|
81
|
+
custom_foreign_key ? @alias[custom_foreign_key] : @alias[@query.klass.primary_key]
|
82
|
+
end
|
83
|
+
|
84
|
+
def bind_values
|
85
|
+
@builder.bind_values
|
86
|
+
end
|
87
|
+
|
88
|
+
def ordered?
|
89
|
+
@query.orderings.any?
|
90
|
+
end
|
91
|
+
|
92
|
+
def order_columns
|
93
|
+
[@query.recursive_table[@query.ordering_column_name].asc]
|
94
|
+
end
|
95
|
+
|
96
|
+
# This node is required to support joins to aliased Arel nodes
|
97
|
+
class SubqueryAlias < Arel::Nodes::As
|
98
|
+
attr_reader :table_name
|
99
|
+
|
100
|
+
def initialize(subquery, alias_node)
|
101
|
+
super
|
102
|
+
@table_name = alias_node.name
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module HierarchicalQuery
|
3
|
+
class Orderings
|
4
|
+
NATURAL_SORT_TYPES = Set[
|
5
|
+
:integer, :float, :decimal,
|
6
|
+
:datetime, :timestamp, :time, :date,
|
7
|
+
:boolean, :itet, :cidr, :ltree
|
8
|
+
]
|
9
|
+
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
attr_reader :order_values, :table
|
13
|
+
|
14
|
+
# @param [Array] order_values
|
15
|
+
# @param [Arel::Table] table
|
16
|
+
def initialize(order_values, table)
|
17
|
+
@order_values, @table = order_values, table
|
18
|
+
@values = nil # cached orderings
|
19
|
+
end
|
20
|
+
|
21
|
+
def each(&block)
|
22
|
+
return @values.each(&block) if @values
|
23
|
+
return enum_for(__method__) unless block_given?
|
24
|
+
|
25
|
+
@values = []
|
26
|
+
|
27
|
+
order_values.each do |value|
|
28
|
+
Array.wrap(as_orderings(value)).each do |ordering|
|
29
|
+
@values << ordering
|
30
|
+
|
31
|
+
yield ordering
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@values
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns order expression to be inserted into SELECT clauses of both
|
39
|
+
# non-recursive and recursive terms.
|
40
|
+
#
|
41
|
+
# @return [Arel::Nodes::Node] order expression
|
42
|
+
def row_number_expression
|
43
|
+
if raw_ordering?
|
44
|
+
order_attribute
|
45
|
+
else
|
46
|
+
Arel.sql("ROW_NUMBER() OVER (ORDER BY #{map(&:to_sql).join(', ')})")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def as_orderings(value)
|
52
|
+
case value
|
53
|
+
when Arel::Nodes::Ordering
|
54
|
+
value
|
55
|
+
|
56
|
+
when Arel::Nodes::Node, Arel::Attributes::Attribute
|
57
|
+
value.asc
|
58
|
+
|
59
|
+
when Symbol
|
60
|
+
table[value].asc
|
61
|
+
|
62
|
+
when Hash
|
63
|
+
value.map { |field, dir| table[field].send(dir) }
|
64
|
+
|
65
|
+
when String
|
66
|
+
value.split(',').map do |expr|
|
67
|
+
string_as_ordering(expr)
|
68
|
+
end
|
69
|
+
|
70
|
+
else
|
71
|
+
raise 'Unknown expression in ORDER BY SIBLINGS clause'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def string_as_ordering(expr)
|
76
|
+
expr.strip!
|
77
|
+
|
78
|
+
if expr.gsub!(/\bdesc\z/i, '')
|
79
|
+
Arel.sql(expr).desc
|
80
|
+
else
|
81
|
+
expr.gsub!(/\basc\z/i, '')
|
82
|
+
Arel.sql(expr).asc
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def raw_ordering?
|
87
|
+
ordered_by_attribute? &&
|
88
|
+
(column = order_column) &&
|
89
|
+
NATURAL_SORT_TYPES.include?(column.type)
|
90
|
+
end
|
91
|
+
|
92
|
+
def ordered_by_attribute?
|
93
|
+
one? && first.ascending? && order_attribute.is_a?(Arel::Attributes::Attribute)
|
94
|
+
end
|
95
|
+
|
96
|
+
def order_attribute
|
97
|
+
first.expr
|
98
|
+
end
|
99
|
+
|
100
|
+
def order_column
|
101
|
+
table = order_attribute.relation
|
102
|
+
|
103
|
+
engine = table.class.engine
|
104
|
+
if engine == ActiveRecord::Base
|
105
|
+
columns =
|
106
|
+
if engine.connection.respond_to?(:schema_cache)
|
107
|
+
engine.connection.schema_cache.columns_hash(table.name)
|
108
|
+
else
|
109
|
+
engine.connection_pool.columns_hash[table.name]
|
110
|
+
end
|
111
|
+
else
|
112
|
+
columns = engine.columns_hash
|
113
|
+
end
|
114
|
+
|
115
|
+
columns[order_attribute.name.to_s]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|