mochigome 0.0.10 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|