through_hierarchy 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|