mochigome 0.0.10 → 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.
- data/lib/arel_rails2_hacks.rb +1 -0
- data/lib/mochigome.rb +2 -1
- data/lib/mochigome_ver.rb +1 -1
- data/lib/model_graph.rb +132 -0
- data/lib/query.rb +186 -294
- data/lib/subgroup_model.rb +1 -1
- data/test/app_root/app/models/sale.rb +1 -1
- data/test/unit/query_test.rb +7 -23
- metadata +5 -5
data/lib/arel_rails2_hacks.rb
CHANGED
@@ -39,6 +39,7 @@ unless ActiveRecord::ConnectionAdapters::ConnectionPool.methods.include?("table_
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
+
# FIXME: Shouldn't use select_rows anymore
|
42
43
|
class ActiveRecord::ConnectionAdapters::SQLiteAdapter
|
43
44
|
def select_rows(sql, name = nil)
|
44
45
|
execute(sql, name).map do |row|
|
data/lib/mochigome.rb
CHANGED
data/lib/mochigome_ver.rb
CHANGED
data/lib/model_graph.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'rgl/adjacency'
|
2
|
+
require 'rgl/traversal'
|
3
|
+
|
4
|
+
module Mochigome
|
5
|
+
private
|
6
|
+
|
7
|
+
module ModelGraph
|
8
|
+
@@graphed_models = Set.new
|
9
|
+
@@table_to_model = {}
|
10
|
+
@@assoc_graph = RGL::DirectedAdjacencyGraph.new
|
11
|
+
@@edge_relation_funcs = {}
|
12
|
+
@@shortest_paths = {}
|
13
|
+
|
14
|
+
# Take an expression and return a Set of all models it references
|
15
|
+
def self.expr_models(e)
|
16
|
+
r = Set.new
|
17
|
+
[:expr, :left, :right].each do |m|
|
18
|
+
r += expr_models(e.send(m)) if e.respond_to?(m)
|
19
|
+
end
|
20
|
+
if e.respond_to?(:relation)
|
21
|
+
model = @@table_to_model[e.relation.name]
|
22
|
+
raise ModelSetupError.new("Table->model lookup error") unless model
|
23
|
+
r.add model
|
24
|
+
end
|
25
|
+
r
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.relation_over_path(path, rel = nil)
|
29
|
+
real_path = path.map{|e| (e.real_model? ? e : e.model)}.uniq
|
30
|
+
# Project ensures that we return a Rel, not a Table, even if path is empty
|
31
|
+
rel ||= real_path.first.arel_table.project
|
32
|
+
(0..(real_path.size-2)).each do |i|
|
33
|
+
rel = relation_func(real_path[i], real_path[i+1]).call(rel)
|
34
|
+
end
|
35
|
+
rel
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.relation_func(u, v)
|
39
|
+
@@edge_relation_funcs[[u,v]] or
|
40
|
+
raise QueryError.new "No assoc from #{u.name} to #{v.name}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.path_thru(models)
|
44
|
+
update_assoc_graph(models)
|
45
|
+
model_queue = models.dup
|
46
|
+
path = [model_queue.shift]
|
47
|
+
until model_queue.empty?
|
48
|
+
src = path.last
|
49
|
+
tgt = model_queue.shift
|
50
|
+
next if src == tgt
|
51
|
+
real_src = src.real_model? ? src : src.model
|
52
|
+
real_tgt = tgt.real_model? ? tgt : tgt.model
|
53
|
+
unless real_src == real_tgt
|
54
|
+
seg = @@shortest_paths[[real_src,real_tgt]]
|
55
|
+
unless seg
|
56
|
+
raise QueryError.new("No path: #{real_src.name} to #{real_tgt.name}")
|
57
|
+
end
|
58
|
+
path.concat seg.take(seg.size-1).drop(1)
|
59
|
+
end
|
60
|
+
path << tgt
|
61
|
+
end
|
62
|
+
unless path.uniq.size == path.size
|
63
|
+
raise QueryError.new(
|
64
|
+
"Path thru #{models.map(&:name).join('-')} doubles back: " +
|
65
|
+
path.map(&:name).join('-')
|
66
|
+
)
|
67
|
+
end
|
68
|
+
path
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.update_assoc_graph(models)
|
72
|
+
model_queue = models.dup
|
73
|
+
added_models = []
|
74
|
+
until model_queue.empty?
|
75
|
+
model = model_queue.shift
|
76
|
+
next if model.is_a?(SubgroupModel)
|
77
|
+
next if @@graphed_models.include? model
|
78
|
+
@@graphed_models.add model
|
79
|
+
added_models << model
|
80
|
+
|
81
|
+
if @@table_to_model.has_key?(model.table_name)
|
82
|
+
raise ModelError.new("Table #{model.table_name} used twice")
|
83
|
+
end
|
84
|
+
@@table_to_model[model.table_name] = model
|
85
|
+
|
86
|
+
model.reflections.
|
87
|
+
reject{|name, assoc| assoc.through_reflection}.
|
88
|
+
each do |name, assoc|
|
89
|
+
# TODO: What about self associations?
|
90
|
+
# TODO: What about associations to the same model on different keys?
|
91
|
+
next if assoc.options[:polymorphic] # TODO How to deal with these? Check for matching has_X assoc?
|
92
|
+
foreign_model = assoc.klass
|
93
|
+
edge = [model, foreign_model]
|
94
|
+
next if @@assoc_graph.has_edge?(*edge) # Ignore duplicate assocs
|
95
|
+
@@assoc_graph.add_edge(*edge)
|
96
|
+
@@edge_relation_funcs[edge] = model.arelified_assoc(name)
|
97
|
+
unless @@graphed_models.include?(foreign_model)
|
98
|
+
model_queue.push(foreign_model)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
added_models.each do |model|
|
104
|
+
next unless @@assoc_graph.has_vertex?(model)
|
105
|
+
path_tree = @@assoc_graph.bfs_search_tree_from(model).reverse
|
106
|
+
path_tree.depth_first_search do |tgt_model|
|
107
|
+
next if tgt_model == model
|
108
|
+
path = [tgt_model]
|
109
|
+
while (parent = path_tree.adjacent_vertices(path.first).first)
|
110
|
+
path.unshift parent
|
111
|
+
end
|
112
|
+
@@shortest_paths[[model,tgt_model]] = path
|
113
|
+
end
|
114
|
+
|
115
|
+
# Use through reflections as a hint for preferred indirect paths
|
116
|
+
model.reflections.
|
117
|
+
select{|name, assoc| assoc.through_reflection}.
|
118
|
+
each do |name, assoc|
|
119
|
+
begin
|
120
|
+
foreign_model = assoc.klass
|
121
|
+
join_model = assoc.through_reflection.klass
|
122
|
+
rescue NameError
|
123
|
+
# FIXME Can't handle polymorphic through reflection
|
124
|
+
end
|
125
|
+
edge = [model,foreign_model]
|
126
|
+
next if @@shortest_paths[edge].try(:size).try(:<, 3)
|
127
|
+
@@shortest_paths[edge] = [model, join_model, foreign_model]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/query.rb
CHANGED
@@ -1,243 +1,116 @@
|
|
1
|
-
require 'rgl/adjacency'
|
2
|
-
require 'rgl/traversal'
|
3
|
-
|
4
1
|
module Mochigome
|
5
2
|
class Query
|
6
3
|
def initialize(layer_types, options = {})
|
7
4
|
# TODO: Validate layer types: not empty, AR, act_as_mochigome_focus
|
8
5
|
@layer_types = layer_types
|
9
|
-
@layers_path =
|
10
|
-
@layers_path or raise QueryError.new("No valid path thru layer list") #TODO Test
|
6
|
+
@layers_path = ModelGraph.path_thru(@layer_types)
|
11
7
|
|
12
8
|
@name = options.delete(:root_name).try(:to_s) || "report"
|
13
9
|
@access_filter = options.delete(:access_filter) || lambda {|cls| {}}
|
10
|
+
# TODO: Validate that aggregate_sources is in the correct format
|
14
11
|
aggregate_sources = options.delete(:aggregate_sources) || []
|
15
12
|
unless options.empty?
|
16
13
|
raise QueryError.new("Unknown options: #{options.keys.inspect}")
|
17
14
|
end
|
18
15
|
|
19
|
-
@ids_rel =
|
20
|
-
|
21
|
-
@ids_rel = access_filtered_relation(@ids_rel, @layers_path)
|
16
|
+
@ids_rel = Relation.new(@layer_types)
|
17
|
+
@ids_rel.apply_access_filter_func(@access_filter)
|
22
18
|
|
23
|
-
|
24
|
-
aggs_by_model = {}
|
19
|
+
@aggregate_rels = {}
|
25
20
|
aggregate_sources.each do |a|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
focus_cls, data_cls = a, a
|
21
|
+
focus_model, data_model = case a
|
22
|
+
when Array then [a.first, a.second]
|
23
|
+
else [a, a]
|
30
24
|
end
|
31
|
-
aggs_by_model[focus_cls] ||= []
|
32
|
-
aggs_by_model[focus_cls] << data_cls
|
33
|
-
end
|
34
25
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
52
|
-
agg_path = nil
|
53
|
-
key_path = nil
|
54
|
-
f2d_path.each do |link_model|
|
55
|
-
remainder = f2d_path.drop_while{|m| m != link_model}
|
56
|
-
next if (remainder.drop(1) & @layers_path).size > 0
|
57
|
-
if @layers_path.include?(link_model)
|
58
|
-
agg_path = remainder
|
59
|
-
key_path = @layers_path.take(@layers_path.index(focus_model)+1)
|
60
|
-
break
|
61
|
-
else
|
62
|
-
# Route it from the closest layer model
|
63
|
-
@layers_path.reverse.each do |layer|
|
64
|
-
p = self.class.path_thru([layer, link_model]) + remainder.drop(1) # TODO: Handle path_thru returning nil
|
65
|
-
next if (p.drop(1) & @layers_path).size > 0
|
66
|
-
next if p.uniq.size != p.size
|
67
|
-
if agg_path.nil? || p.size < agg_path.size
|
68
|
-
agg_path = p
|
69
|
-
key_path = @layers_path
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
26
|
+
agg_rel = Relation.new(@layer_types) # TODO Only go as far as focus
|
27
|
+
agg_rel.join_to_model(focus_model)
|
28
|
+
agg_rel.join_on_path_thru([focus_model, data_model])
|
29
|
+
agg_rel.apply_access_filter_func(@access_filter)
|
30
|
+
|
31
|
+
focus_idx = @layers_path.index(focus_model)
|
32
|
+
key_path = focus_idx ? @layers_path.take(focus_idx+1) : @layers_path
|
33
|
+
key_path = key_path.select{|m| @layer_types.include?(m)}
|
34
|
+
key_cols = key_path.map{|m| m.arel_primary_key}
|
35
|
+
|
36
|
+
agg_fields = data_model.mochigome_aggregation_settings.
|
37
|
+
options[:fields].reject{|a| a[:in_ruby]}
|
38
|
+
agg_fields.each_with_index do |a, i|
|
39
|
+
d_expr = a[:value_proc].call(data_model.arel_table)
|
40
|
+
agg_rel.select_expr(d_expr.as("d%03u" % i))
|
41
|
+
end
|
74
42
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
cand = nil
|
85
|
-
agg_joined_models.each do |agg_join_src_m|
|
86
|
-
p = self.class.path_thru([agg_join_src_m, m])
|
87
|
-
if p && (cand.nil? || p.size < cand.size)
|
88
|
-
cand = p
|
89
|
-
end
|
90
|
-
end
|
91
|
-
if cand
|
92
|
-
agg_data_rel = self.class.relation_over_path(cand, agg_data_rel)
|
93
|
-
agg_joined_models += cand
|
94
|
-
else
|
95
|
-
raise QueryError.new("Can't join from query to agg join model #{m.name}") # TODO: Test this
|
96
|
-
end
|
97
|
-
end
|
43
|
+
@aggregate_rels[focus_model] ||= {}
|
44
|
+
@aggregate_rels[focus_model][data_model] = (0..key_cols.length).map{|n|
|
45
|
+
lambda {|cond|
|
46
|
+
data_rel = agg_rel.clone
|
47
|
+
data_rel.apply_condition(cond)
|
48
|
+
data_cols = key_cols.take(n) + [data_model.arel_primary_key]
|
49
|
+
inner_rel = data_rel.to_arel
|
50
|
+
data_cols.each_with_index do |col, i|
|
51
|
+
inner_rel.project(col.as("g%03u" % i)).group(col)
|
98
52
|
end
|
99
|
-
d_expr = a[:value_proc].call(data_model.arel_table)
|
100
|
-
agg_data_rel.project(d_expr.as("d#{i}"))
|
101
|
-
end
|
102
53
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
agg_fields.each_with_index do |a, i|
|
120
|
-
a_rel.project(a[:agg_proc].call(d_tbl["d#{i}"]))
|
121
|
-
end
|
122
|
-
key_cols.take(n).each_with_index do |col, i|
|
123
|
-
outer_name = "og#{i}"
|
124
|
-
a_rel.project(d_tbl["g#{i}"].as(outer_name)).group(outer_name)
|
125
|
-
end
|
126
|
-
a_rel
|
127
|
-
}
|
54
|
+
# FIXME: This subtable won't be necessary for all aggregation funcs.
|
55
|
+
# When we can avoid it, we should, for performance.
|
56
|
+
rel = Arel::SelectManager.new(
|
57
|
+
Arel::Table.engine,
|
58
|
+
Arel.sql("(#{inner_rel.to_sql}) as mochigome_data")
|
59
|
+
)
|
60
|
+
d_tbl = Arel::Table.new("mochigome_data")
|
61
|
+
agg_fields.each_with_index do |a, i|
|
62
|
+
name = "d%03u" % i
|
63
|
+
rel.project(a[:agg_proc].call(d_tbl[name]).as(name))
|
64
|
+
end
|
65
|
+
key_cols.take(n).each_with_index do |col, i|
|
66
|
+
name = "g%03u" % i
|
67
|
+
rel.project(d_tbl[name].as(name)).group(name)
|
68
|
+
end
|
69
|
+
rel
|
128
70
|
}
|
129
|
-
|
130
|
-
|
71
|
+
}
|
131
72
|
end
|
132
73
|
end
|
133
74
|
|
134
75
|
def run(cond = nil)
|
135
|
-
root =
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
end
|
140
|
-
if cond.is_a?(Array)
|
141
|
-
return root if cond.empty?
|
142
|
-
cond = cond.inject(nil) do |expr, obj|
|
143
|
-
subexpr = obj.class.arel_primary_key.eq(obj.id)
|
144
|
-
expr ? expr.or(subexpr) : subexpr
|
145
|
-
end
|
146
|
-
end
|
147
|
-
if cond
|
148
|
-
self.class.expr_tables(cond).each do |t|
|
149
|
-
raise QueryError.new("Condition table #{t} not in layer list") unless
|
150
|
-
@layers_path.any?{|m| m.real_model? && m.table_name == t}
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
q = @ids_rel.dup
|
155
|
-
q.where(cond) if cond
|
156
|
-
ids_table = @layer_types.first.connection.select_rows(q.to_sql)
|
157
|
-
ids_table = ids_table.map do |row|
|
158
|
-
# FIXME: Should do this conversion based on type of column
|
159
|
-
row.map{|cell| cell =~ /^\d+$/ ? cell.to_i : cell}
|
160
|
-
end
|
161
|
-
|
162
|
-
fill_layers(ids_table, {[] => root}, @layer_types)
|
76
|
+
root = create_node_tree(cond)
|
77
|
+
load_aggregate_data(root, cond)
|
78
|
+
return root
|
79
|
+
end
|
163
80
|
|
164
|
-
|
165
|
-
super_types = @layer_types.take_while{|m| m != focus_model}
|
166
|
-
super_cols = super_types.map{|m| @layers_path.find_index(m)}
|
167
|
-
data_model_rels.each do |data_model, rel_funcs|
|
168
|
-
aggs = data_model.mochigome_aggregation_settings.options[:fields]
|
169
|
-
aggs_count = aggs.reject{|a| a[:in_ruby]}.size
|
170
|
-
rel_funcs.each do |rel_func|
|
171
|
-
q = rel_func.call(cond)
|
172
|
-
data_tree = {}
|
173
|
-
# Each row has aggs_count data fields, followed by the id fields
|
174
|
-
# from least specific to most.
|
175
|
-
@layer_types.first.connection.select_rows(q.to_sql).each do |row|
|
176
|
-
if row.size == aggs_count
|
177
|
-
data_tree = row.take(aggs_count)
|
178
|
-
else
|
179
|
-
c = data_tree
|
180
|
-
super_cols.each_with_index do |sc_num, sc_idx|
|
181
|
-
break if aggs_count+sc_idx >= row.size-1
|
182
|
-
col_num = aggs_count + sc_num
|
183
|
-
c = (c[row[col_num]] ||= {})
|
184
|
-
end
|
185
|
-
c[row.last] = row.take(aggs_count)
|
186
|
-
end
|
187
|
-
end
|
188
|
-
insert_aggregate_data_fields(root, data_tree, data_model)
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
81
|
+
private
|
192
82
|
|
83
|
+
def create_node_tree(cond)
|
84
|
+
root = DataNode.new(:report, @name)
|
193
85
|
root.comment = <<-eos
|
194
86
|
Mochigome Version: #{Mochigome::VERSION}
|
195
87
|
Report Generated: #{Time.now}
|
196
88
|
Layers: #{@layer_types.map(&:name).join(" => ")}
|
197
89
|
AR Path: #{@layers_path.map(&:name).join(" => ")}
|
198
90
|
eos
|
199
|
-
root.comment.gsub!(
|
200
|
-
root.comment.lstrip!
|
201
|
-
|
202
|
-
return root
|
203
|
-
end
|
91
|
+
root.comment.gsub!(/(\n|^) +/, "\\1")
|
204
92
|
|
205
|
-
|
93
|
+
r = @ids_rel.dup
|
94
|
+
r.apply_condition(cond)
|
95
|
+
ids_table = @layer_types.first.connection.select_all(r.to_sql)
|
96
|
+
fill_layers(ids_table, {[] => root}, @layer_types)
|
206
97
|
|
207
|
-
|
208
|
-
joined = Set.new
|
209
|
-
models.uniq.each do |model|
|
210
|
-
h = @access_filter.call(model)
|
211
|
-
h.delete(:join_paths).try :each do |path|
|
212
|
-
(0..(path.size-2)).each do |i|
|
213
|
-
next if models.include?(path[i+1]) or joined.include?(path[i+1])
|
214
|
-
r = self.class.relation_func(path[i], path[i+1]).call(r)
|
215
|
-
joined.add path[i+1]
|
216
|
-
end
|
217
|
-
end
|
218
|
-
if h[:condition]
|
219
|
-
r = r.where(h.delete(:condition))
|
220
|
-
end
|
221
|
-
unless h.empty?
|
222
|
-
raise QueryError.new("Unknown assoc filter keys #{h.keys.inspect}")
|
223
|
-
end
|
224
|
-
end
|
225
|
-
r
|
98
|
+
root
|
226
99
|
end
|
227
100
|
|
228
|
-
def fill_layers(ids_table, parents, types,
|
229
|
-
return if types.size
|
101
|
+
def fill_layers(ids_table, parents, types, type_idx = 0)
|
102
|
+
return if type_idx >= types.size
|
230
103
|
|
231
|
-
model = types
|
232
|
-
col_num = @layers_path.find_index(model)
|
104
|
+
model = types[type_idx]
|
233
105
|
layer_ids = Set.new
|
234
106
|
cur_to_parent = {}
|
235
107
|
|
108
|
+
parent_types = types.take(type_idx)
|
236
109
|
ids_table.each do |row|
|
237
|
-
cur_id = row[
|
110
|
+
cur_id = row["#{model.name}_id"]
|
238
111
|
layer_ids.add cur_id
|
239
112
|
cur_to_parent[cur_id] ||= Set.new
|
240
|
-
cur_to_parent[cur_id].add
|
113
|
+
cur_to_parent[cur_id].add parent_types.map{|m| row["#{m.name}_id"]}
|
241
114
|
end
|
242
115
|
|
243
116
|
layer = {}
|
@@ -260,7 +133,36 @@ module Mochigome
|
|
260
133
|
end
|
261
134
|
end
|
262
135
|
|
263
|
-
fill_layers(ids_table, layer, types
|
136
|
+
fill_layers(ids_table, layer, types, type_idx + 1)
|
137
|
+
end
|
138
|
+
|
139
|
+
def load_aggregate_data(node, cond)
|
140
|
+
@aggregate_rels.each do |focus_model, data_model_rels|
|
141
|
+
# TODO Actually get the key types found in init for this aggregation
|
142
|
+
super_types = @layer_types.take_while{|m| m != focus_model}
|
143
|
+
data_model_rels.each do |data_model, rel_funcs|
|
144
|
+
aggs = data_model.mochigome_aggregation_settings.options[:fields]
|
145
|
+
aggs_count = aggs.reject{|a| a[:in_ruby]}.size
|
146
|
+
rel_funcs.each do |rel_func|
|
147
|
+
q = rel_func.call(cond)
|
148
|
+
data_tree = {}
|
149
|
+
@layer_types.first.connection.select_all(q.to_sql).each do |row|
|
150
|
+
group_values = row.keys.select{|k| k.start_with?("g")}.sort.map{|k| row[k]}
|
151
|
+
data_values = row.keys.select{|k| k.start_with?("d")}.sort.map{|k| row[k]}
|
152
|
+
if group_values.empty?
|
153
|
+
data_tree = data_values
|
154
|
+
else
|
155
|
+
c = data_tree
|
156
|
+
group_values.take(group_values.size-1).each do |group_id|
|
157
|
+
c = (c[group_id] ||= {})
|
158
|
+
end
|
159
|
+
c[group_values.last] = data_values
|
160
|
+
end
|
161
|
+
end
|
162
|
+
insert_aggregate_data_fields(node, data_tree, data_model)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
264
166
|
end
|
265
167
|
|
266
168
|
def insert_aggregate_data_fields(node, table, data_model)
|
@@ -283,122 +185,112 @@ module Mochigome
|
|
283
185
|
end
|
284
186
|
end
|
285
187
|
end
|
188
|
+
end
|
286
189
|
|
287
|
-
|
190
|
+
private
|
288
191
|
|
289
|
-
|
290
|
-
def
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
r
|
192
|
+
class Relation
|
193
|
+
def initialize(layers)
|
194
|
+
@spine_layers = layers
|
195
|
+
@spine = ModelGraph.path_thru(layers) or
|
196
|
+
raise QueryError.new("No valid path thru #{layers.inspect}") #TODO Test
|
197
|
+
@models = Set.new @spine
|
198
|
+
@rel = ModelGraph.relation_over_path(@spine)
|
199
|
+
|
200
|
+
@spine_layers.each{|m| select_model_id(m)}
|
299
201
|
end
|
300
202
|
|
301
|
-
|
302
|
-
|
303
|
-
@@edge_relation_funcs = {}
|
304
|
-
@@shortest_paths = {}
|
305
|
-
|
306
|
-
def self.relation_over_path(path, rel = nil)
|
307
|
-
real_path = path.map{|e| (e.real_model? ? e : e.model)}.uniq
|
308
|
-
# Project ensures that we return a Rel, not a Table, even if path is empty
|
309
|
-
rel ||= real_path.first.arel_table.project
|
310
|
-
(0..(real_path.size-2)).each do |i|
|
311
|
-
rel = relation_func(real_path[i], real_path[i+1]).call(rel)
|
312
|
-
end
|
313
|
-
rel
|
203
|
+
def to_arel
|
204
|
+
@rel.clone
|
314
205
|
end
|
315
206
|
|
316
|
-
def
|
317
|
-
|
318
|
-
raise QueryError.new "No assoc from #{u.name} to #{v.name}"
|
207
|
+
def to_sql
|
208
|
+
@rel.to_sql
|
319
209
|
end
|
320
210
|
|
321
|
-
def
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
211
|
+
def clone
|
212
|
+
c = super
|
213
|
+
c.instance_variable_set :@spine, @spine.dup
|
214
|
+
c.instance_variable_set :@models, @models.dup
|
215
|
+
c.instance_variable_set :@rel, @rel.project
|
216
|
+
c
|
217
|
+
end
|
218
|
+
|
219
|
+
def join_to_model(model)
|
220
|
+
return if @models.include?(model)
|
221
|
+
|
222
|
+
# Route to it in as few steps as possible, closer to spine end if tie.
|
223
|
+
best_path = nil
|
224
|
+
(@spine.reverse + (@models.to_a - @spine)).each do |link_model|
|
225
|
+
path = ModelGraph.path_thru([link_model, model])
|
226
|
+
if path && (best_path.nil? || path.size < best_path.size)
|
227
|
+
best_path = path
|
336
228
|
end
|
337
|
-
path << tgt
|
338
229
|
end
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
230
|
+
|
231
|
+
raise QueryError.new("No path to #{model}") unless best_path
|
232
|
+
join_on_path(best_path)
|
233
|
+
end
|
234
|
+
|
235
|
+
def join_on_path_thru(path)
|
236
|
+
join_on_path(ModelGraph.path_thru(path).uniq)
|
237
|
+
end
|
238
|
+
|
239
|
+
def join_on_path(path)
|
240
|
+
path = path.map{|e| (e.real_model? ? e : e.model)}.uniq
|
241
|
+
(0..(path.size-2)).map{|i| [path[i], path[i+1]]}.each do |src, tgt|
|
242
|
+
add_join_link src, tgt
|
344
243
|
end
|
345
|
-
path
|
346
244
|
end
|
347
245
|
|
348
|
-
def
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
next if @@assoc_graph.has_edge?(*edge) # Ignore duplicate assocs
|
367
|
-
@@assoc_graph.add_edge(*edge)
|
368
|
-
@@edge_relation_funcs[edge] = model.arelified_assoc(name)
|
369
|
-
unless @@graphed_models.include?(foreign_model)
|
370
|
-
model_queue.push(foreign_model)
|
371
|
-
end
|
246
|
+
def select_model_id(m)
|
247
|
+
@rel = @rel.project(m.arel_primary_key.as("#{m.name}_id"))
|
248
|
+
end
|
249
|
+
|
250
|
+
def select_expr(e)
|
251
|
+
ModelGraph.expr_models(e).each{|m| join_to_model(m)}
|
252
|
+
@rel = @rel.project(e)
|
253
|
+
end
|
254
|
+
|
255
|
+
def apply_condition(cond)
|
256
|
+
return unless cond
|
257
|
+
if cond.is_a?(ActiveRecord::Base)
|
258
|
+
cond = [cond]
|
259
|
+
end
|
260
|
+
if cond.is_a?(Array)
|
261
|
+
cond = cond.inject(nil) do |expr, obj|
|
262
|
+
subexpr = obj.class.arel_primary_key.eq(obj.id)
|
263
|
+
expr ? expr.or(subexpr) : subexpr
|
372
264
|
end
|
373
265
|
end
|
374
266
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
path_tree.depth_first_search do |tgt_model|
|
379
|
-
next if tgt_model == model
|
380
|
-
path = [tgt_model]
|
381
|
-
while (parent = path_tree.adjacent_vertices(path.first).first)
|
382
|
-
path.unshift parent
|
383
|
-
end
|
384
|
-
@@shortest_paths[[model,tgt_model]] = path
|
385
|
-
end
|
267
|
+
ModelGraph.expr_models(cond).each{|m| join_to_model(m)}
|
268
|
+
@rel = @rel.where(cond)
|
269
|
+
end
|
386
270
|
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
each do |
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
next if @@shortest_paths[edge].try(:size).try(:<, 3)
|
399
|
-
@@shortest_paths[edge] = [model, join_model, foreign_model]
|
271
|
+
def apply_access_filter_func(func)
|
272
|
+
@models.each do |m|
|
273
|
+
h = func.call(m)
|
274
|
+
h.delete(:join_paths).try :each do |path|
|
275
|
+
join_on_path path
|
276
|
+
end
|
277
|
+
if h[:condition]
|
278
|
+
apply_condition h.delete(:condition)
|
279
|
+
end
|
280
|
+
unless h.empty?
|
281
|
+
raise QueryError.new("Unknown assoc filter keys #{h.keys.inspect}")
|
400
282
|
end
|
401
283
|
end
|
402
284
|
end
|
285
|
+
|
286
|
+
private
|
287
|
+
|
288
|
+
def add_join_link(src, tgt)
|
289
|
+
raise QueryError.new("Can't join from #{src}, not available") unless
|
290
|
+
@models.include?(src)
|
291
|
+
return if @models.include?(tgt) # TODO Maybe still apply join conditions?
|
292
|
+
@rel = ModelGraph.relation_func(src, tgt).call(@rel)
|
293
|
+
@models.add tgt
|
294
|
+
end
|
403
295
|
end
|
404
296
|
end
|
data/lib/subgroup_model.rb
CHANGED
data/test/unit/query_test.rb
CHANGED
@@ -65,13 +65,6 @@ describe Mochigome::Query do
|
|
65
65
|
assert_empty obj.children
|
66
66
|
end
|
67
67
|
|
68
|
-
it "returns an empty DataNode if given an empty array" do
|
69
|
-
q = Mochigome::Query.new([Category, Product])
|
70
|
-
data_node = q.run([])
|
71
|
-
assert_empty data_node
|
72
|
-
assert_no_children data_node
|
73
|
-
end
|
74
|
-
|
75
68
|
it "returns all possible results if no conditions given" do
|
76
69
|
q = Mochigome::Query.new([Category, Product])
|
77
70
|
data_node = q.run()
|
@@ -88,8 +81,7 @@ describe Mochigome::Query do
|
|
88
81
|
|
89
82
|
it "can build a one-layer DataNode when given an arbitrary Arel condition" do
|
90
83
|
q = Mochigome::Query.new([Product])
|
91
|
-
|
92
|
-
data_node = q.run(tbl[:name].eq(@product_a.name))
|
84
|
+
data_node = q.run(Product.arel_table[:name].eq(@product_a.name))
|
93
85
|
assert_equal_children [@product_a], data_node
|
94
86
|
assert_no_children data_node/0
|
95
87
|
end
|
@@ -447,18 +439,11 @@ describe Mochigome::Query do
|
|
447
439
|
end
|
448
440
|
end
|
449
441
|
|
450
|
-
it "will not allow a query on targets not in the layer list" do
|
451
|
-
q = Mochigome::Query.new([Product])
|
452
|
-
assert_raises Mochigome::QueryError do
|
453
|
-
q.run(@category1)
|
454
|
-
end
|
455
|
-
end
|
456
|
-
|
457
442
|
it "can use a provided access filter function to limit query results" do
|
458
443
|
af = proc do |cls|
|
459
444
|
return {} unless cls == Product
|
460
445
|
return {
|
461
|
-
:condition =>
|
446
|
+
:condition => Product.arel_table[:category_id].gt(0)
|
462
447
|
}
|
463
448
|
end
|
464
449
|
q = Mochigome::Query.new([Product], :access_filter => af)
|
@@ -472,7 +457,7 @@ describe Mochigome::Query do
|
|
472
457
|
return {} unless cls == Product
|
473
458
|
return {
|
474
459
|
:join_paths => [[Product, StoreProduct, Store]],
|
475
|
-
:condition =>
|
460
|
+
:condition => Store.arel_table[:name].matches("Jo%")
|
476
461
|
}
|
477
462
|
end
|
478
463
|
q = Mochigome::Query.new([Product], :access_filter => af)
|
@@ -486,18 +471,17 @@ describe Mochigome::Query do
|
|
486
471
|
return {} unless cls == Product
|
487
472
|
return {
|
488
473
|
:join_paths => [[Product, StoreProduct, Store]],
|
489
|
-
:condition =>
|
474
|
+
:condition => Store.arel_table[:name].matches("Jo%")
|
490
475
|
}
|
491
476
|
end
|
492
477
|
q = Mochigome::Query.new([Product, Store], :access_filter => af)
|
493
478
|
assert_equal 1, q.instance_variable_get(:@ids_rel).to_sql.scan(/join .stores./i).size
|
494
479
|
end
|
495
480
|
|
496
|
-
it "
|
481
|
+
it "automatically joins if run given a condition on a new table" do
|
497
482
|
q = Mochigome::Query.new([Product, Store])
|
498
|
-
|
499
|
-
|
500
|
-
end
|
483
|
+
dn = q.run(Category.arel_table[:name].eq(@category1.name))
|
484
|
+
assert_equal 2, dn.children.size
|
501
485
|
end
|
502
486
|
|
503
487
|
# TODO: Test that access filter join paths are followed, rather than closest path
|
metadata
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mochigome
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 9
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
|
10
|
-
version: 0.0.10
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
11
10
|
platform: ruby
|
12
11
|
authors:
|
13
12
|
- David Mike Simon
|
@@ -15,7 +14,7 @@ autorequire:
|
|
15
14
|
bindir: bin
|
16
15
|
cert_chain: []
|
17
16
|
|
18
|
-
date: 2012-04-
|
17
|
+
date: 2012-04-09 00:00:00 Z
|
19
18
|
dependencies:
|
20
19
|
- !ruby/object:Gem::Dependency
|
21
20
|
version_requirements: &id001 !ruby/object:Gem::Requirement
|
@@ -97,6 +96,7 @@ files:
|
|
97
96
|
- lib/mochigome.rb
|
98
97
|
- lib/mochigome_ver.rb
|
99
98
|
- lib/model_extensions.rb
|
99
|
+
- lib/model_graph.rb
|
100
100
|
- lib/query.rb
|
101
101
|
- lib/subgroup_model.rb
|
102
102
|
- test/app_root/app/controllers/application_controller.rb
|