cuatlan-activerecord-hierarchical_query 1.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,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