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