cuatlan-activerecord-hierarchical_query 1.0.1

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