activerecord-hierarchical_query 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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