activerecord-hierarchical_query 0.0.2 → 0.0.5
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.
- checksums.yaml +4 -4
- data/README.md +59 -30
- data/lib/active_record/hierarchical_query.rb +11 -11
- data/lib/active_record/hierarchical_query/cte/columns.rb +2 -15
- data/lib/active_record/hierarchical_query/cte/cycle_detector.rb +54 -0
- data/lib/active_record/hierarchical_query/cte/non_recursive_term.rb +32 -11
- data/lib/active_record/hierarchical_query/cte/query_builder.rb +82 -0
- data/lib/active_record/hierarchical_query/cte/recursive_term.rb +27 -13
- data/lib/active_record/hierarchical_query/cte/union_term.rb +10 -6
- data/lib/active_record/hierarchical_query/join_builder.rb +31 -10
- data/lib/active_record/hierarchical_query/orderings.rb +113 -0
- data/lib/active_record/hierarchical_query/{builder.rb → query.rb} +66 -36
- data/lib/active_record/hierarchical_query/version.rb +1 -1
- data/lib/arel/nodes/postgresql.rb +26 -6
- data/spec/active_record/hierarchical_query_spec.rb +56 -32
- data/spec/database.yml +1 -9
- data/spec/schema.rb +2 -2
- data/spec/spec_helper.rb +2 -4
- data/spec/support/models.rb +4 -4
- metadata +28 -33
- data/lib/active_record/hierarchical_query/adapters.rb +0 -34
- data/lib/active_record/hierarchical_query/adapters/abstract.rb +0 -41
- data/lib/active_record/hierarchical_query/adapters/postgresql.rb +0 -19
- data/lib/active_record/hierarchical_query/cte/query.rb +0 -65
- data/lib/active_record/hierarchical_query/visitors/orderings.rb +0 -87
- data/lib/active_record/hierarchical_query/visitors/postgresql/cycle_detector.rb +0 -49
- data/lib/active_record/hierarchical_query/visitors/postgresql/orderings.rb +0 -70
- data/lib/active_record/hierarchical_query/visitors/visitor.rb +0 -17
@@ -1,34 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
|
3
|
-
require 'active_support/core_ext/hash/keys'
|
4
|
-
require 'active_support/core_ext/string/inflections'
|
5
|
-
|
6
|
-
module ActiveRecord
|
7
|
-
module HierarchicalQuery
|
8
|
-
module Adapters
|
9
|
-
SUPPORTED_ADAPTERS = %w(PostgreSQL)
|
10
|
-
|
11
|
-
ADAPTERS = Hash[
|
12
|
-
:PostgreSQL => :PostgreSQL,
|
13
|
-
:PostGIS => :PostgreSQL,
|
14
|
-
:OracleEnhanced => :Oracle
|
15
|
-
].stringify_keys
|
16
|
-
|
17
|
-
def self.autoload(name, path = name.to_s.underscore)
|
18
|
-
super name, "active_record/hierarchical_query/adapters/#{path}"
|
19
|
-
end
|
20
|
-
|
21
|
-
autoload :PostgreSQL, 'postgresql'
|
22
|
-
autoload :Oracle
|
23
|
-
|
24
|
-
def self.lookup(klass)
|
25
|
-
name = klass.connection.adapter_name
|
26
|
-
|
27
|
-
raise 'Your database %s does not support recursive queries' % name unless
|
28
|
-
ADAPTERS.key?(name)
|
29
|
-
|
30
|
-
const_get(ADAPTERS[name])
|
31
|
-
end
|
32
|
-
end # module Adapters
|
33
|
-
end # module HierarchicalQuery
|
34
|
-
end # module ActiveRecord
|
@@ -1,41 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
|
3
|
-
require 'active_record/hierarchical_query/visitors/visitor'
|
4
|
-
|
5
|
-
module ActiveRecord
|
6
|
-
module HierarchicalQuery
|
7
|
-
module Adapters
|
8
|
-
# @api private
|
9
|
-
class Abstract
|
10
|
-
attr_reader :query,
|
11
|
-
:table
|
12
|
-
|
13
|
-
delegate :klass, :to => :query
|
14
|
-
|
15
|
-
class << self
|
16
|
-
def visitors(*visitors_classes)
|
17
|
-
define_method :visitors do
|
18
|
-
@visitors ||= visitors_classes.map { |klass| klass.new(@query) }
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
# @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
|
24
|
-
def initialize(query)
|
25
|
-
@query = query
|
26
|
-
@table = klass.arel_table
|
27
|
-
end
|
28
|
-
|
29
|
-
# @example
|
30
|
-
# visit(:recursive_term, recursive_term.arel)
|
31
|
-
def visit(kind, object)
|
32
|
-
method_name = "visit_#{kind}"
|
33
|
-
|
34
|
-
visitors.reduce(object) do |*, visitor|
|
35
|
-
visitor.send(method_name, object) if visitor.respond_to?(method_name)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
|
3
|
-
require 'arel/nodes/postgresql'
|
4
|
-
|
5
|
-
require 'active_record/hierarchical_query/adapters/abstract'
|
6
|
-
require 'active_record/hierarchical_query/visitors/postgresql/cycle_detector'
|
7
|
-
require 'active_record/hierarchical_query/visitors/postgresql/orderings'
|
8
|
-
|
9
|
-
module ActiveRecord
|
10
|
-
module HierarchicalQuery
|
11
|
-
module Adapters
|
12
|
-
# @api private
|
13
|
-
class PostgreSQL < Abstract
|
14
|
-
visitors Visitors::PostgreSQL::CycleDetector,
|
15
|
-
Visitors::PostgreSQL::Orderings
|
16
|
-
end # class PostgreSQL
|
17
|
-
end # module Adapters
|
18
|
-
end # module HierarchicalQuery
|
19
|
-
end # module ActiveRecord
|
@@ -1,65 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
|
3
|
-
require 'active_record/hierarchical_query/adapters'
|
4
|
-
require 'active_record/hierarchical_query/cte/columns'
|
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 Query
|
12
|
-
attr_reader :builder,
|
13
|
-
:adapter,
|
14
|
-
:columns
|
15
|
-
|
16
|
-
delegate :klass, :table, :to => :builder
|
17
|
-
|
18
|
-
# @param [ActiveRecord::HierarchicalQuery::Builder] builder
|
19
|
-
def initialize(builder)
|
20
|
-
@builder = builder
|
21
|
-
@adapter = Adapters.lookup(klass).new(self)
|
22
|
-
@columns = Columns.new(self)
|
23
|
-
end
|
24
|
-
|
25
|
-
# @return [Arel::SelectManager]
|
26
|
-
def arel
|
27
|
-
adapter.visit(:cte, build_arel)
|
28
|
-
end
|
29
|
-
|
30
|
-
# @return [Arel::Table]
|
31
|
-
def recursive_table
|
32
|
-
@recursive_table ||= Arel::Table.new("#{table.name}__recursive")
|
33
|
-
end
|
34
|
-
|
35
|
-
def join_conditions
|
36
|
-
builder.connect_by_value[recursive_table, table]
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
# "categories__recursive" AS (
|
41
|
-
# SELECT ... FROM "categories"
|
42
|
-
# UNION ALL
|
43
|
-
# SELECT ... FROM "categories"
|
44
|
-
# INNER JOIN "categories__recursive" ON ...
|
45
|
-
# )
|
46
|
-
def with_query
|
47
|
-
Arel::Nodes::As.new(recursive_table, union_term.arel)
|
48
|
-
end
|
49
|
-
|
50
|
-
def union_term
|
51
|
-
UnionTerm.new(self)
|
52
|
-
end
|
53
|
-
|
54
|
-
def build_arel
|
55
|
-
Arel::SelectManager.new(table.engine).
|
56
|
-
with(:recursive, with_query).
|
57
|
-
from(recursive_table).
|
58
|
-
project(recursive_table[Arel.star]).
|
59
|
-
take(builder.limit_value).
|
60
|
-
skip(builder.offset_value)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
@@ -1,87 +0,0 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
module HierarchicalQuery
|
3
|
-
module Visitors
|
4
|
-
class Orderings < Visitor
|
5
|
-
ORDERING_COLUMN_ALIAS = '__order_column'
|
6
|
-
|
7
|
-
delegate :recursive_table, :to => :query
|
8
|
-
|
9
|
-
def visit_joined_relation(relation)
|
10
|
-
visit(relation) do
|
11
|
-
relation.order(order_clause)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def visit_cte(select_manager)
|
16
|
-
if should_order_cte?
|
17
|
-
select_manager.order(order_clause)
|
18
|
-
else
|
19
|
-
select_manager
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
protected
|
24
|
-
def visit(object)
|
25
|
-
if orderings.any?
|
26
|
-
yield
|
27
|
-
else
|
28
|
-
object
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def order_clause
|
33
|
-
recursive_table[column_name].asc
|
34
|
-
end
|
35
|
-
|
36
|
-
def should_order_cte?
|
37
|
-
orderings.any? && builder.limit_value || builder.offset_value
|
38
|
-
end
|
39
|
-
|
40
|
-
def orderings
|
41
|
-
@orderings ||= builder.order_values.each_with_object([]) do |value, orderings|
|
42
|
-
orderings.concat Array.wrap(as_orderings(value))
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def column_name
|
47
|
-
ORDERING_COLUMN_ALIAS
|
48
|
-
end
|
49
|
-
|
50
|
-
def as_orderings(value)
|
51
|
-
case value
|
52
|
-
when Arel::Nodes::Ordering
|
53
|
-
value
|
54
|
-
|
55
|
-
when Arel::Nodes::Node, Arel::Attributes::Attribute
|
56
|
-
value.asc
|
57
|
-
|
58
|
-
when Symbol
|
59
|
-
table[value].asc
|
60
|
-
|
61
|
-
when Hash
|
62
|
-
value.map { |field, dir| table[field].send(dir) }
|
63
|
-
|
64
|
-
when String
|
65
|
-
value.split(',').map do |expr|
|
66
|
-
string_as_ordering(expr)
|
67
|
-
end
|
68
|
-
|
69
|
-
else
|
70
|
-
raise 'Unknown expression in ORDER BY SIBLINGS clause'
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def string_as_ordering(expr)
|
75
|
-
expr.strip!
|
76
|
-
|
77
|
-
if expr.gsub!(/\bdesc\z/i, '')
|
78
|
-
Arel.sql(expr).desc
|
79
|
-
else
|
80
|
-
expr.gsub!(/\basc\z/i, '')
|
81
|
-
Arel.sql(expr).asc
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
module HierarchicalQuery
|
3
|
-
module Visitors
|
4
|
-
module PostgreSQL
|
5
|
-
class CycleDetector < Visitor
|
6
|
-
COLUMN_NAME = '__path'.freeze
|
7
|
-
|
8
|
-
def visit_non_recursive(arel)
|
9
|
-
if enabled?
|
10
|
-
arel.project Arel::Nodes::PostgresArray.new([primary_key]).as(column_name)
|
11
|
-
end
|
12
|
-
|
13
|
-
arel
|
14
|
-
end
|
15
|
-
|
16
|
-
def visit_recursive(arel)
|
17
|
-
if enabled?
|
18
|
-
arel.project Arel::Nodes::ArrayConcat.new(parent_column, primary_key)
|
19
|
-
arel.constraints << Arel::Nodes::Not.new(primary_key.eq(any(parent_column)))
|
20
|
-
end
|
21
|
-
|
22
|
-
arel
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
def enabled?
|
27
|
-
builder.nocycle_value
|
28
|
-
end
|
29
|
-
|
30
|
-
def column_name
|
31
|
-
COLUMN_NAME
|
32
|
-
end
|
33
|
-
|
34
|
-
def parent_column
|
35
|
-
query.recursive_table[column_name]
|
36
|
-
end
|
37
|
-
|
38
|
-
def primary_key
|
39
|
-
table[klass.primary_key]
|
40
|
-
end
|
41
|
-
|
42
|
-
def any(argument)
|
43
|
-
Arel::Nodes::NamedFunction.new('ANY', [argument])
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
@@ -1,70 +0,0 @@
|
|
1
|
-
require 'active_record/hierarchical_query/visitors/orderings'
|
2
|
-
|
3
|
-
module ActiveRecord
|
4
|
-
module HierarchicalQuery
|
5
|
-
module Visitors
|
6
|
-
module PostgreSQL
|
7
|
-
class Orderings < Visitors::Orderings
|
8
|
-
NATURAL_SORT_TYPES = Set[
|
9
|
-
:integer, :float, :decimal,
|
10
|
-
:datetime, :timestamp, :time, :date,
|
11
|
-
:boolean, :itet, :cidr, :ltree
|
12
|
-
]
|
13
|
-
|
14
|
-
delegate :first, :to => :orderings
|
15
|
-
|
16
|
-
def visit_non_recursive(arel)
|
17
|
-
project(arel) do
|
18
|
-
Arel::Nodes::PostgresArray.new([row_number_expression]).as(column_name)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def visit_recursive(arel)
|
23
|
-
project(arel) do
|
24
|
-
Arel::Nodes::ArrayConcat.new(recursive_table[column_name], row_number_expression)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
def project(arel)
|
30
|
-
visit(arel) { arel.project(yield) }
|
31
|
-
end
|
32
|
-
|
33
|
-
def row_number_expression
|
34
|
-
if raw_ordering?
|
35
|
-
order_attribute
|
36
|
-
else
|
37
|
-
Arel.sql("ROW_NUMBER() OVER (ORDER BY #{orderings.map(&:to_sql).join(', ')})")
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def raw_ordering?
|
42
|
-
ordered_by_attribute? &&
|
43
|
-
(column = order_column) &&
|
44
|
-
NATURAL_SORT_TYPES.include?(column.type)
|
45
|
-
end
|
46
|
-
|
47
|
-
def ordered_by_attribute?
|
48
|
-
orderings.one? && first.ascending? && order_attribute.is_a?(Arel::Attributes::Attribute)
|
49
|
-
end
|
50
|
-
|
51
|
-
def order_attribute
|
52
|
-
first.expr
|
53
|
-
end
|
54
|
-
|
55
|
-
def order_column
|
56
|
-
table = order_attribute.relation
|
57
|
-
|
58
|
-
if table.engine == ActiveRecord::Base
|
59
|
-
columns = table.engine.connection_pool.columns_hash[table.name]
|
60
|
-
else
|
61
|
-
columns = table.engine.columns_hash
|
62
|
-
end
|
63
|
-
|
64
|
-
columns[order_attribute.name.to_s]
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
module HierarchicalQuery
|
3
|
-
module Visitors
|
4
|
-
# @api private
|
5
|
-
class Visitor
|
6
|
-
attr_reader :query
|
7
|
-
|
8
|
-
delegate :builder, :to => :query
|
9
|
-
delegate :klass, :table, :to => :builder
|
10
|
-
|
11
|
-
def initialize(query)
|
12
|
-
@query = query
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|