through_hierarchy 0.0.2 → 0.1.0
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/lib/through_hierarchy/associations/association.rb +31 -131
- data/lib/through_hierarchy/associations/has_one.rb +13 -2
- data/lib/through_hierarchy/associations/has_uniq.rb +33 -0
- data/lib/through_hierarchy/base.rb +7 -0
- data/lib/through_hierarchy/builder.rb +0 -5
- data/lib/through_hierarchy/exceptions.rb +10 -1
- data/lib/through_hierarchy/hierarchicals/hierarchical.rb +157 -0
- data/lib/through_hierarchy/hierarchicals/instance.rb +76 -0
- data/lib/through_hierarchy/hierarchy.rb +8 -4
- data/lib/through_hierarchy.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24802dbaa712d4bce228409d6559bd95e8d07088
|
4
|
+
data.tar.gz: 94bc36b57d75022bcd256dae8edfd4710652aedf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af92986a50caae9db2e72c5fd5e2999ed13130faef98e66fe63c97ae6281673b095a82e2a3012909bb94beabe677cd89e7e6aa518272a82554efb5fa9f77c312
|
7
|
+
data.tar.gz: efac0a679e68aae89833613b84cd1b4f636e8de80c1ef9546dc7c87386f1bd0d64516eea522936709289ea73737abe45c206788ee883c372402956ea78c22d6e
|
@@ -5,34 +5,23 @@ module ThroughHierarchy
|
|
5
5
|
@name = name
|
6
6
|
@model = model
|
7
7
|
@members = members
|
8
|
-
@as = options[:as].to_s
|
9
|
-
@scope = options[:scope]
|
10
|
-
@foreign_class_name = options[:class_name] || @name.to_s.classify
|
11
|
-
@uniq = options[:uniq]
|
12
8
|
|
9
|
+
set_options(options)
|
13
10
|
validate_options
|
11
|
+
|
12
|
+
@associated = Hierarchicals::Hierarchical.new(foreign_arel_table, model, members, as: @polymorphic_name)
|
14
13
|
end
|
15
14
|
|
16
15
|
def find(instance)
|
17
|
-
|
18
|
-
|
19
|
-
results = foreign_class.where(arel_instance_filters(instance))
|
16
|
+
results = get_matches(instance)
|
20
17
|
results = results.instance_exec(&@scope) if @scope.present?
|
21
18
|
return results
|
22
19
|
end
|
23
20
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
# joins = joins.join(arel_uniq_subquery).on(
|
29
|
-
# arel_hierarchy_rank.eq(uniq_subquery_arel_table[best_hierarchy_match_name]).
|
30
|
-
# and(foreign_arel_table[@uniq].eq(uniq_subquery_arel_table[@uniq]))
|
31
|
-
# ) if @uniq.present?
|
32
|
-
|
33
|
-
# results = @model.joins(joins.join_sources)
|
34
|
-
# results = results.merge(foreign_class.instance_exec(&@scope)) if @scope.present?
|
35
|
-
# return results
|
21
|
+
def join
|
22
|
+
results = get_joins
|
23
|
+
results = results.merge(foreign_class.instance_exec(&@scope)) if @scope.present?
|
24
|
+
return results
|
36
25
|
end
|
37
26
|
|
38
27
|
def create(member, attributes)
|
@@ -41,132 +30,43 @@ module ThroughHierarchy
|
|
41
30
|
|
42
31
|
private
|
43
32
|
|
33
|
+
def set_options(options)
|
34
|
+
@polymorphic_name = options[:as].to_s
|
35
|
+
@scope = options[:scope]
|
36
|
+
@foreign_class_name = options[:class_name] || @name.to_s.classify
|
37
|
+
end
|
38
|
+
|
44
39
|
def validate_options
|
45
|
-
@
|
40
|
+
@polymorphic_name.present? or raise ThroughHierarchyDefinitionError, "Must provide polymorphic `:as` options for through_hierarchy"
|
46
41
|
@model.is_a?(Class) or raise ThroughHierarchyDefinitionError, "Expected: class, got: #{@model.class}"
|
47
42
|
@model < ActiveRecord::Base or raise ThroughHierarchyDefinitionError, "Expected: ActiveRecord::Base descendant, got: #{@model}"
|
48
43
|
@scope.blank? || @scope.is_a?(Proc) or raise ThroughHierarchyDefinitionError, "Expected scope to be a Proc, got #{@scope.class}"
|
49
44
|
end
|
50
45
|
|
51
|
-
def
|
52
|
-
@
|
53
|
-
end
|
54
|
-
|
55
|
-
def foreign_arel_table
|
56
|
-
foreign_class.arel_table
|
57
|
-
end
|
58
|
-
|
59
|
-
def sql_hierarchy_rank
|
60
|
-
"CASE `#{foreign_class.table_name}`.`#{foreign_type_field}` " +
|
61
|
-
hierarchy_models.map.with_index{|m, ii| "WHEN #{@model.sanitize(model_type_constraint(m))} THEN #{ii} "}.join +
|
62
|
-
"END"
|
63
|
-
end
|
64
|
-
|
65
|
-
def arel_hierarchy_rank
|
66
|
-
Arel.sql(sql_hierarchy_rank)
|
67
|
-
end
|
68
|
-
|
69
|
-
# TODO: generate this dynamically based on existing columns and selects
|
70
|
-
def best_hierarchy_match_name
|
71
|
-
"through_hierarchy_match"
|
72
|
-
end
|
73
|
-
|
74
|
-
def arel_best_hierarchy_member
|
75
|
-
arel_hierarchy_rank.minimum.as(best_hierarchy_match_name)
|
76
|
-
end
|
77
|
-
|
78
|
-
# TODO: for model level joins, subquery needs to join to all hierarchy tables
|
79
|
-
def arel_uniq_subquery(instance = nil)
|
80
|
-
foreign_arel_table.
|
81
|
-
project(foreign_arel_table[Arel.star], arel_best_hierarchy_member).
|
82
|
-
where(instance.present? ? arel_instance_filters(instance) : arel_model_filters).
|
83
|
-
group(foreign_arel_table[@uniq]).
|
84
|
-
as(uniq_subquery_alias)
|
85
|
-
end
|
86
|
-
|
87
|
-
def uniq_subquery_alias
|
88
|
-
"through_hierarchy_subtable"
|
89
|
-
end
|
90
|
-
|
91
|
-
def uniq_subquery_arel_table
|
92
|
-
Arel::Table.new(uniq_subquery_alias)
|
93
|
-
end
|
94
|
-
|
95
|
-
# TODO: build join sources for model-level query
|
96
|
-
def uniq_subquery_join_sources(instance = nil)
|
97
|
-
foreign_arel_table.
|
98
|
-
join(arel_uniq_subquery(instance)).
|
99
|
-
on(
|
100
|
-
arel_hierarchy_rank.eq(uniq_subquery_arel_table[best_hierarchy_match_name]).
|
101
|
-
and(foreign_arel_table[@uniq].eq(uniq_subquery_arel_table[@uniq]))
|
102
|
-
).join_sources
|
103
|
-
end
|
104
|
-
|
105
|
-
def uniq_find(instance)
|
106
|
-
join_sources = uniq_subquery_join_sources(instance)
|
107
|
-
|
108
|
-
return foreign_class.
|
109
|
-
joins(uniq_subquery_join_sources(instance)).
|
110
|
-
where(arel_instance_filters(instance)).
|
111
|
-
order(foreign_arel_table[@uniq])
|
112
|
-
end
|
113
|
-
|
114
|
-
def hierarchy_models
|
115
|
-
[@model] + @members.map{|m| @model.reflect_on_association(m).klass}
|
116
|
-
end
|
117
|
-
|
118
|
-
def hierarchy_instances(instance)
|
119
|
-
[instance] + @members.map{|m| instance.association(m).load_target}
|
120
|
-
end
|
121
|
-
|
122
|
-
def foreign_key_field
|
123
|
-
@as.foreign_key
|
124
|
-
end
|
125
|
-
|
126
|
-
def foreign_type_field
|
127
|
-
@as + "_type"
|
128
|
-
end
|
129
|
-
|
130
|
-
def arel_foreign_key_field
|
131
|
-
foreign_arel_table[foreign_key_field]
|
132
|
-
end
|
133
|
-
|
134
|
-
def arel_foreign_type_field
|
135
|
-
foreign_arel_table[foreign_type_field]
|
136
|
-
end
|
137
|
-
|
138
|
-
def arel_model_filters
|
139
|
-
hierarchy_models.map{|model| arel_model_filter(model)}.reduce{|q, cond| q.or(cond)}
|
140
|
-
end
|
141
|
-
|
142
|
-
def arel_instance_filters(instance)
|
143
|
-
hierarchy_instances(instance).map{|instance| arel_instance_filter(instance)}.reduce{|q, cond| q.or(cond)}
|
46
|
+
def associated_instance(instance)
|
47
|
+
@associated.with_instance(instance)
|
144
48
|
end
|
145
49
|
|
146
|
-
def
|
147
|
-
|
148
|
-
and(arel_foreign_key_field.eq(model_key_constraint(model)))
|
50
|
+
def get_matches(instance)
|
51
|
+
return foreign_class.where(associated_instance(instance).filters)
|
149
52
|
end
|
150
53
|
|
151
|
-
|
152
|
-
|
153
|
-
|
54
|
+
# TODO: we might generate fewer join sources if we figure out which members
|
55
|
+
# are :through associations. Currently those generate redundant joins.
|
56
|
+
def get_joins
|
57
|
+
join_sources = @model.arel_table.
|
58
|
+
join(@associated.source).
|
59
|
+
on(@associated.filters).
|
60
|
+
join_sources
|
61
|
+
return @model.joins(@members + join_sources)
|
154
62
|
end
|
155
63
|
|
156
|
-
def
|
157
|
-
|
158
|
-
end
|
159
|
-
|
160
|
-
def instance_key_constraint(resource)
|
161
|
-
resource.attributes[resource.class.primary_key]
|
162
|
-
end
|
163
|
-
|
164
|
-
def model_type_constraint(model_class)
|
165
|
-
model_class.base_class.to_s
|
64
|
+
def foreign_class
|
65
|
+
@foreign_class_name.constantize
|
166
66
|
end
|
167
67
|
|
168
|
-
def
|
169
|
-
|
68
|
+
def foreign_arel_table
|
69
|
+
foreign_class.arel_table
|
170
70
|
end
|
171
71
|
end
|
172
72
|
end
|
@@ -2,9 +2,20 @@ module ThroughHierarchy
|
|
2
2
|
module Associations
|
3
3
|
class HasOne < Association
|
4
4
|
def find(instance)
|
5
|
-
|
6
|
-
|
5
|
+
matches = super
|
6
|
+
# ensure we order by hierarchy rank, but preserve scope orders
|
7
|
+
matches.reorder(@associated.hierarchy_rank).order(matches.orders).first
|
7
8
|
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def get_joins
|
13
|
+
arel = @associated.join_best_rank
|
14
|
+
result = @model.joins(arel.join_sources).order(arel.orders)
|
15
|
+
arel.constraints.each{|cc| result = result.where(cc)}
|
16
|
+
return result
|
17
|
+
end
|
18
|
+
|
8
19
|
end
|
9
20
|
end
|
10
21
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ThroughHierarchy
|
2
|
+
module Associations
|
3
|
+
class HasUniq < Association
|
4
|
+
private
|
5
|
+
|
6
|
+
def set_options(options)
|
7
|
+
super
|
8
|
+
@uniq = options[:uniq]
|
9
|
+
end
|
10
|
+
|
11
|
+
# Use subquery method to select best hierarchy match for each @uniq
|
12
|
+
# Order by @uniq can result in better performance than default order (id)
|
13
|
+
def get_matches(instance)
|
14
|
+
associated_instance = @associated.with_instance(instance)
|
15
|
+
arel = @associated.with_instance(instance).select_best_rank(group_by: @uniq)
|
16
|
+
result = foreign_class.
|
17
|
+
where(associated_instance.filters).
|
18
|
+
joins(arel.join_sources).
|
19
|
+
order(arel.orders)
|
20
|
+
arel.constraints.each{|cc| result = result.where(cc)}
|
21
|
+
return result
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_joins
|
25
|
+
arel = @associated.join_best_rank(group_by: @uniq)
|
26
|
+
result = @model.joins(arel.join_sources).order(arel.orders)
|
27
|
+
arel.constraints.each{|cc| result = result.where(cc)}
|
28
|
+
return result
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -17,6 +17,13 @@ module ThroughHierarchy
|
|
17
17
|
def through_hierarchy(members, &blk)
|
18
18
|
Hierarchy.new(self, members).instance_eval(&blk)
|
19
19
|
end
|
20
|
+
|
21
|
+
def joins_through_hierarchy(name)
|
22
|
+
hierarchical_associations.key?(name) or raise ThroughHierarchyAssociationMissingError, "No association named #{name} was found. Perhaps you misspelled it?"
|
23
|
+
hierarchical_associations[name].join
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO: create_through_hierarchy(member = self, attributes)
|
20
27
|
end
|
21
28
|
end
|
22
29
|
end
|
@@ -18,11 +18,6 @@ module ThroughHierarchy
|
|
18
18
|
return @hierarchical_association_cache[name] if !reload && @hierarchical_association_cache.key?(name)
|
19
19
|
@hierarchical_association_cache[name] = self.hierarchical_associations[name].find(self)
|
20
20
|
end
|
21
|
-
|
22
|
-
# TODO: join_through_hierarchy
|
23
|
-
|
24
|
-
# TODO: create_from_hierarchy(member, attributes)
|
25
|
-
|
26
21
|
end
|
27
22
|
end
|
28
23
|
end
|
@@ -4,4 +4,13 @@ module ThroughHierarchy
|
|
4
4
|
|
5
5
|
class ThroughHierarchyDefinitionError < ThroughHierarchyError
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
|
+
class ThroughHierarchyAssociationMissingError < ThroughHierarchyError
|
9
|
+
end
|
10
|
+
|
11
|
+
class ThroughHierarchySourceError < ThroughHierarchyError
|
12
|
+
end
|
13
|
+
|
14
|
+
class ThroughHierarchyInstanceError < ThroughHierarchyError
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module ThroughHierarchy
|
2
|
+
module Hierarchicals
|
3
|
+
class Hierarchical
|
4
|
+
attr_reader :source
|
5
|
+
|
6
|
+
# source should be an Arel::Table or Arel::TableAlias
|
7
|
+
# TODO: parent only on derived tables. Make that a separate class or module.
|
8
|
+
def initialize(source, target, hierarchy, as:, parent: nil)
|
9
|
+
@source = source
|
10
|
+
set_target(target)
|
11
|
+
@hierarchy = hierarchy
|
12
|
+
@polymorphic_name = as.to_s
|
13
|
+
@parent = parent
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_target(target)
|
17
|
+
@target = target
|
18
|
+
@model = @target
|
19
|
+
end
|
20
|
+
|
21
|
+
# Initialize a new copy of self bound to a specific instance
|
22
|
+
def with_instance(instance)
|
23
|
+
instance.is_a?(@model) or raise ThroughHierarchyInstanceError, "#{instance} is not an instance of #{@model}"
|
24
|
+
Instance.new(@source, instance, @hierarchy, as: @polymorphic_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Intialize a copy of self with a new / derived source table
|
28
|
+
def spawn(source)
|
29
|
+
return self.class.new(source, @target, @hierarchy, as: @polymorphic_name, parent: self)
|
30
|
+
end
|
31
|
+
|
32
|
+
def hierarchy_models
|
33
|
+
[@model] + @hierarchy.map{|m| @model.reflect_on_association(m).klass}
|
34
|
+
end
|
35
|
+
|
36
|
+
# TODO: some of these may be :through others, so this may generate redundant joins
|
37
|
+
def hierarchy_joins
|
38
|
+
@hierarchy
|
39
|
+
end
|
40
|
+
|
41
|
+
def and_conditions(conditions)
|
42
|
+
conditions.reduce{|q, cond| q.and(cond)}
|
43
|
+
end
|
44
|
+
|
45
|
+
def or_conditions(conditions)
|
46
|
+
conditions.reduce{|q, cond| q.or(cond)}
|
47
|
+
end
|
48
|
+
|
49
|
+
def filters
|
50
|
+
or_conditions(hierarchy_models.map{|model| filter(model)})
|
51
|
+
end
|
52
|
+
|
53
|
+
def filter(model)
|
54
|
+
foreign_type_column.eq(model_type(model)).
|
55
|
+
and(foreign_key_column.eq(model_key(model)))
|
56
|
+
end
|
57
|
+
|
58
|
+
# Sort order for hierarchy shadowing queries
|
59
|
+
def hierarchy_rank
|
60
|
+
Arel.sql(
|
61
|
+
"CASE `#{@source.name}`.`#{foreign_type_name}` " +
|
62
|
+
hierarchy_models.map.with_index do |model, ii|
|
63
|
+
"WHEN #{model.sanitize(model.base_class.to_s)} THEN #{ii} "
|
64
|
+
end.join +
|
65
|
+
"END"
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def foreign_key_name
|
70
|
+
@polymorphic_name.foreign_key
|
71
|
+
end
|
72
|
+
|
73
|
+
def foreign_type_name
|
74
|
+
@polymorphic_name + "_type"
|
75
|
+
end
|
76
|
+
|
77
|
+
def foreign_key_column
|
78
|
+
@source[foreign_key_name]
|
79
|
+
end
|
80
|
+
|
81
|
+
def foreign_type_column
|
82
|
+
@source[foreign_type_name]
|
83
|
+
end
|
84
|
+
|
85
|
+
def model_type(model)
|
86
|
+
model.base_class.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
def model_key(model)
|
90
|
+
model.arel_table[model.primary_key]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Join @model to @source only on best hierarchy matches
|
94
|
+
### FASTER METHOD: join source to source alias on source.rank < alias.rank where alias does not exist
|
95
|
+
# This performs OK.
|
96
|
+
def join_best_rank(group_by: nil)
|
97
|
+
better_rank = spawn(@source.alias("better_hierarchy"))
|
98
|
+
@model.joins(@hierarchy).arel.
|
99
|
+
join(@source).on(filters).
|
100
|
+
join(better_rank.source, Arel::Nodes::OuterJoin).
|
101
|
+
on(
|
102
|
+
better_rank.filters.
|
103
|
+
and(better_rank.hierarchy_rank.lt(hierarchy_rank))
|
104
|
+
).
|
105
|
+
where(better_rank.source[:id].eq(nil))
|
106
|
+
end
|
107
|
+
|
108
|
+
# # TODO: generate this dynamically based on existing columns and selects
|
109
|
+
# def best_rank_column_name
|
110
|
+
# "through_hierarchy_best_rank"
|
111
|
+
# end
|
112
|
+
|
113
|
+
# def best_rank_column
|
114
|
+
# @source[best_rank_column_name]
|
115
|
+
# end
|
116
|
+
|
117
|
+
# def best_rank
|
118
|
+
# hierarchy_rank.minimum.as(best_rank_column_name)
|
119
|
+
# end
|
120
|
+
|
121
|
+
# def best_rank_table_name
|
122
|
+
# "through_hierarchy_best_rank"
|
123
|
+
# end
|
124
|
+
|
125
|
+
# SLOW METHOD: subquery, gorup, min(priority). This performs abysmally.
|
126
|
+
# # TODO: replace model_key(@model) with target_key?
|
127
|
+
# def join_best_rank(group_by: nil)
|
128
|
+
# sub = best_rank_subquery(*group_by)
|
129
|
+
# @model.joins(@hierarchy).arel.
|
130
|
+
# join(sub.source).
|
131
|
+
# on(sub.source["model_key"].eq(model_key(@model))).
|
132
|
+
# join(@source).on(
|
133
|
+
# and_conditions([
|
134
|
+
# filters,
|
135
|
+
# hierarchy_rank.eq(sub.best_rank_column),
|
136
|
+
# *[*group_by].map{|gg| @source[gg].eq(sub.source[gg])}
|
137
|
+
# ])
|
138
|
+
# ).
|
139
|
+
# order(model_key(@model), *group_by)
|
140
|
+
# end
|
141
|
+
|
142
|
+
# # TODO: does ordering the subquery increase performance?
|
143
|
+
# # TODO: override model_key in spawn to refer to projected column
|
144
|
+
# def best_rank_subquery(*group_bys)
|
145
|
+
# @source.respond_to?(:project) or raise ThroughHierarchySourceError, "#{@source} cannot be converted into a subquery"
|
146
|
+
# group_nodes = group_bys.map{|gg|@source[gg]}
|
147
|
+
# subq = @model.joins(@hierarchy).arel.
|
148
|
+
# project(model_key(@model).as("model_key"), *group_nodes, best_rank).
|
149
|
+
# join(@source).on(filters).
|
150
|
+
# group(model_key(@model), *group_nodes).
|
151
|
+
# as(best_rank_table_name)
|
152
|
+
# spawn(subq)
|
153
|
+
# end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module ThroughHierarchy
|
2
|
+
module Hierarchicals
|
3
|
+
class Instance < Hierarchical
|
4
|
+
def set_target(target)
|
5
|
+
@target = target
|
6
|
+
@instance = @target
|
7
|
+
@model = @target.class
|
8
|
+
end
|
9
|
+
|
10
|
+
def hierarchy_instances
|
11
|
+
[@instance] + @hierarchy.map{|m| @instance.association(m).load_target}
|
12
|
+
end
|
13
|
+
|
14
|
+
def filters
|
15
|
+
or_conditions(hierarchy_instances.map{|instance| filter(instance)})
|
16
|
+
end
|
17
|
+
|
18
|
+
def filter(instance)
|
19
|
+
foreign_type_column.eq(instance_type(instance)).
|
20
|
+
and(foreign_key_column.eq(instance_key(instance)))
|
21
|
+
end
|
22
|
+
|
23
|
+
def instance_type(instance)
|
24
|
+
instance.class.base_class.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def instance_key(instance)
|
28
|
+
instance.attributes[@model.primary_key]
|
29
|
+
end
|
30
|
+
|
31
|
+
def best_rank_column_name
|
32
|
+
"through_hierarchy_best_rank"
|
33
|
+
end
|
34
|
+
|
35
|
+
def best_rank_column
|
36
|
+
@source[best_rank_column_name]
|
37
|
+
end
|
38
|
+
|
39
|
+
def best_rank
|
40
|
+
hierarchy_rank.minimum.as(best_rank_column_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def best_rank_table_name
|
44
|
+
"through_hierarchy_best_rank"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Select only sources with best hierarchy rank for target instance
|
48
|
+
# Uses subquery grouped by specified column to compute best rank
|
49
|
+
# TODO: experiment with the model-style double-join method instead
|
50
|
+
def select_best_rank(group_by:)
|
51
|
+
sub = best_rank_subquery(group_by)
|
52
|
+
@source.
|
53
|
+
join(sub.source).
|
54
|
+
on(
|
55
|
+
hierarchy_rank.eq(sub.best_rank_column).
|
56
|
+
and(@source[group_by].eq(sub.source[group_by]))
|
57
|
+
).
|
58
|
+
order(@source[group_by])
|
59
|
+
end
|
60
|
+
|
61
|
+
# Return a new Hierarchical::Instance representing a subquery that contains
|
62
|
+
# only best-rank sources.
|
63
|
+
def best_rank_subquery(group_by)
|
64
|
+
@source.respond_to?(:project) or raise ThroughHierarchySourceError, "#{@source} cannot be converted into a subquery"
|
65
|
+
subq = source.
|
66
|
+
project(foreign_type_column, foreign_key_column, group_by, best_rank).
|
67
|
+
where(filters).
|
68
|
+
group(source[group_by]).
|
69
|
+
as(best_rank_table_name)
|
70
|
+
|
71
|
+
spawn(subq)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -4,7 +4,7 @@ module ThroughHierarchy
|
|
4
4
|
@klass = klass
|
5
5
|
@members = members
|
6
6
|
|
7
|
-
|
7
|
+
validate_hierarchy
|
8
8
|
end
|
9
9
|
|
10
10
|
def has_one(name, scope = nil, **options)
|
@@ -15,15 +15,19 @@ module ThroughHierarchy
|
|
15
15
|
|
16
16
|
def has_many(name, scope = nil, **options)
|
17
17
|
options.merge!(scope: scope) if scope.present?
|
18
|
-
|
18
|
+
if options.key?(:uniq)
|
19
|
+
assoc = ::ThroughHierarchy::Associations::HasUniq.new(name, @klass, @members, options)
|
20
|
+
else
|
21
|
+
assoc = ::ThroughHierarchy::Associations::HasMany.new(name, @klass, @members, options)
|
22
|
+
end
|
19
23
|
::ThroughHierarchy::Builder.new(@klass).add_association(name, assoc)
|
20
24
|
end
|
21
25
|
|
22
26
|
private
|
23
27
|
|
24
|
-
def
|
28
|
+
def validate_hierarchy
|
25
29
|
@members.is_a?(::Array) or ::Kernel.raise ::ThroughHierarchy::ThroughHierarchyDefinitionError, "Hierarchy members: expected: Array, got: #{@members.class}"
|
26
|
-
@members.all?{|member| @klass.reflect_on_association(member).present?
|
30
|
+
@members.all?{|member| @klass.reflect_on_association(member).present? or ::Kernel.raise ::ThroughHierarchy::ThroughHierarchyDefinitionError, "No association named #{member} was found. Perhaps you misspelled it?"}
|
27
31
|
end
|
28
32
|
end
|
29
33
|
end
|
data/lib/through_hierarchy.rb
CHANGED
@@ -4,6 +4,9 @@ require 'through_hierarchy/builder.rb'
|
|
4
4
|
require 'through_hierarchy/associations/association.rb'
|
5
5
|
require 'through_hierarchy/associations/has_one.rb'
|
6
6
|
require 'through_hierarchy/associations/has_many.rb'
|
7
|
+
require 'through_hierarchy/associations/has_uniq.rb'
|
7
8
|
require 'through_hierarchy/exceptions.rb'
|
9
|
+
require 'through_hierarchy/hierarchicals/hierarchical.rb'
|
10
|
+
require 'through_hierarchy/hierarchicals/instance.rb'
|
8
11
|
|
9
12
|
ActiveRecord::Base.include(ThroughHierarchy::Base)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: through_hierarchy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Schwartz
|
@@ -20,9 +20,12 @@ files:
|
|
20
20
|
- lib/through_hierarchy/associations/association.rb
|
21
21
|
- lib/through_hierarchy/associations/has_many.rb
|
22
22
|
- lib/through_hierarchy/associations/has_one.rb
|
23
|
+
- lib/through_hierarchy/associations/has_uniq.rb
|
23
24
|
- lib/through_hierarchy/base.rb
|
24
25
|
- lib/through_hierarchy/builder.rb
|
25
26
|
- lib/through_hierarchy/exceptions.rb
|
27
|
+
- lib/through_hierarchy/hierarchicals/hierarchical.rb
|
28
|
+
- lib/through_hierarchy/hierarchicals/instance.rb
|
26
29
|
- lib/through_hierarchy/hierarchy.rb
|
27
30
|
homepage: https://github.com/ozydingo/through_hierarchy
|
28
31
|
licenses:
|