mochigome 0.1 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +3 -2
- data/lib/mochigome_ver.rb +1 -1
- data/lib/model_graph.rb +52 -28
- data/lib/query.rb +21 -13
- metadata +20 -5
data/Rakefile
CHANGED
@@ -58,8 +58,8 @@ gemspec = Gem::Specification.new do |s|
|
|
58
58
|
s.authors = ["David Mike Simon"]
|
59
59
|
s.email = "david.mike.simon@gmail.com"
|
60
60
|
s.homepage = "http://github.com/DavidMikeSimon/mochigome"
|
61
|
-
s.summary = "
|
62
|
-
s.description = "Report generator that
|
61
|
+
s.summary = "The do-what-I-mean report generator"
|
62
|
+
s.description = "Report generator that graphs over ActiveRecord associations"
|
63
63
|
s.files = `git ls-files .`.split("\n") - [".gitignore"]
|
64
64
|
s.platform = Gem::Platform::RUBY
|
65
65
|
s.require_path = 'lib'
|
@@ -69,6 +69,7 @@ gemspec = Gem::Specification.new do |s|
|
|
69
69
|
s.add_dependency('ruport')
|
70
70
|
s.add_dependency('nokogiri')
|
71
71
|
s.add_dependency('rgl')
|
72
|
+
s.add_dependency('activerecord')
|
72
73
|
end
|
73
74
|
|
74
75
|
Gem::PackageTask.new(gemspec) do |pkg|
|
data/lib/mochigome_ver.rb
CHANGED
data/lib/model_graph.rb
CHANGED
@@ -4,28 +4,30 @@ require 'rgl/traversal'
|
|
4
4
|
module Mochigome
|
5
5
|
private
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
7
|
+
class ModelGraph
|
8
|
+
def initialize
|
9
|
+
@graphed_models = Set.new
|
10
|
+
@table_to_model = {}
|
11
|
+
@assoc_graph = RGL::DirectedAdjacencyGraph.new
|
12
|
+
@edge_relation_funcs = {}
|
13
|
+
@shortest_paths = {}
|
14
|
+
end
|
13
15
|
|
14
16
|
# Take an expression and return a Set of all models it references
|
15
|
-
def
|
17
|
+
def expr_models(e)
|
16
18
|
r = Set.new
|
17
19
|
[:expr, :left, :right].each do |m|
|
18
20
|
r += expr_models(e.send(m)) if e.respond_to?(m)
|
19
21
|
end
|
20
22
|
if e.respond_to?(:relation)
|
21
|
-
model =
|
23
|
+
model = @table_to_model[e.relation.name]
|
22
24
|
raise ModelSetupError.new("Table->model lookup error") unless model
|
23
25
|
r.add model
|
24
26
|
end
|
25
27
|
r
|
26
28
|
end
|
27
29
|
|
28
|
-
def
|
30
|
+
def relation_over_path(path, rel = nil)
|
29
31
|
real_path = path.map{|e| (e.real_model? ? e : e.model)}.uniq
|
30
32
|
# Project ensures that we return a Rel, not a Table, even if path is empty
|
31
33
|
rel ||= real_path.first.arel_table.project
|
@@ -35,12 +37,12 @@ module Mochigome
|
|
35
37
|
rel
|
36
38
|
end
|
37
39
|
|
38
|
-
def
|
39
|
-
|
40
|
+
def relation_func(u, v)
|
41
|
+
@edge_relation_funcs[[u,v]] or
|
40
42
|
raise QueryError.new "No assoc from #{u.name} to #{v.name}"
|
41
43
|
end
|
42
44
|
|
43
|
-
def
|
45
|
+
def path_thru(models)
|
44
46
|
update_assoc_graph(models)
|
45
47
|
model_queue = models.dup
|
46
48
|
path = [model_queue.shift]
|
@@ -51,7 +53,7 @@ module Mochigome
|
|
51
53
|
real_src = src.real_model? ? src : src.model
|
52
54
|
real_tgt = tgt.real_model? ? tgt : tgt.model
|
53
55
|
unless real_src == real_tgt
|
54
|
-
seg =
|
56
|
+
seg = @shortest_paths[[real_src,real_tgt]]
|
55
57
|
unless seg
|
56
58
|
raise QueryError.new("No path: #{real_src.name} to #{real_tgt.name}")
|
57
59
|
end
|
@@ -68,20 +70,42 @@ module Mochigome
|
|
68
70
|
path
|
69
71
|
end
|
70
72
|
|
71
|
-
|
73
|
+
private
|
74
|
+
|
75
|
+
def update_assoc_graph(models)
|
72
76
|
model_queue = models.dup
|
73
77
|
added_models = []
|
74
78
|
until model_queue.empty?
|
75
79
|
model = model_queue.shift
|
76
80
|
next if model.is_a?(SubgroupModel)
|
77
|
-
next if
|
78
|
-
|
81
|
+
next if @graphed_models.include? model
|
82
|
+
@graphed_models.add model
|
79
83
|
added_models << model
|
80
84
|
|
81
|
-
if
|
82
|
-
|
85
|
+
if @table_to_model.has_key?(model.table_name)
|
86
|
+
# TODO Test this!
|
87
|
+
# Find the nearest common ancestor that derives from AR::Base
|
88
|
+
common = nil
|
89
|
+
[model, @table_to_model[model.table_name]].each do |tgt|
|
90
|
+
a = tgt.ancestors
|
91
|
+
a = a.select{|c| c.ancestors.include?(ActiveRecord::Base)}
|
92
|
+
if common.nil?
|
93
|
+
common = a
|
94
|
+
else
|
95
|
+
common = common & a
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
if common.empty? || common.first == ActiveRecord::Base
|
100
|
+
raise ModelSetupError.new(
|
101
|
+
"Unrelated models %s and %s both claim to use table %s" %
|
102
|
+
[model, @table_to_model[model.table_name], model.table_name]
|
103
|
+
)
|
104
|
+
end
|
105
|
+
@table_to_model[model.table_name] = common.first
|
106
|
+
else
|
107
|
+
@table_to_model[model.table_name] = model
|
83
108
|
end
|
84
|
-
@@table_to_model[model.table_name] = model
|
85
109
|
|
86
110
|
model.reflections.
|
87
111
|
reject{|name, assoc| assoc.through_reflection}.
|
@@ -91,25 +115,25 @@ module Mochigome
|
|
91
115
|
next if assoc.options[:polymorphic] # TODO How to deal with these? Check for matching has_X assoc?
|
92
116
|
foreign_model = assoc.klass
|
93
117
|
edge = [model, foreign_model]
|
94
|
-
next if
|
95
|
-
|
96
|
-
|
97
|
-
unless
|
118
|
+
next if @assoc_graph.has_edge?(*edge) # Ignore duplicate assocs
|
119
|
+
@assoc_graph.add_edge(*edge)
|
120
|
+
@edge_relation_funcs[edge] = model.arelified_assoc(name)
|
121
|
+
unless @graphed_models.include?(foreign_model)
|
98
122
|
model_queue.push(foreign_model)
|
99
123
|
end
|
100
124
|
end
|
101
125
|
end
|
102
126
|
|
103
127
|
added_models.each do |model|
|
104
|
-
next unless
|
105
|
-
path_tree =
|
128
|
+
next unless @assoc_graph.has_vertex?(model)
|
129
|
+
path_tree = @assoc_graph.bfs_search_tree_from(model).reverse
|
106
130
|
path_tree.depth_first_search do |tgt_model|
|
107
131
|
next if tgt_model == model
|
108
132
|
path = [tgt_model]
|
109
133
|
while (parent = path_tree.adjacent_vertices(path.first).first)
|
110
134
|
path.unshift parent
|
111
135
|
end
|
112
|
-
|
136
|
+
@shortest_paths[[model,tgt_model]] = path
|
113
137
|
end
|
114
138
|
|
115
139
|
# Use through reflections as a hint for preferred indirect paths
|
@@ -123,8 +147,8 @@ module Mochigome
|
|
123
147
|
# FIXME Can't handle polymorphic through reflection
|
124
148
|
end
|
125
149
|
edge = [model,foreign_model]
|
126
|
-
next if
|
127
|
-
|
150
|
+
next if @shortest_paths[edge].try(:size).try(:<, 3)
|
151
|
+
@shortest_paths[edge] = [model, join_model, foreign_model]
|
128
152
|
end
|
129
153
|
end
|
130
154
|
end
|
data/lib/query.rb
CHANGED
@@ -3,7 +3,6 @@ module Mochigome
|
|
3
3
|
def initialize(layer_types, options = {})
|
4
4
|
# TODO: Validate layer types: not empty, AR, act_as_mochigome_focus
|
5
5
|
@layer_types = layer_types
|
6
|
-
@layers_path = ModelGraph.path_thru(@layer_types)
|
7
6
|
|
8
7
|
@name = options.delete(:root_name).try(:to_s) || "report"
|
9
8
|
@access_filter = options.delete(:access_filter) || lambda {|cls| {}}
|
@@ -28,10 +27,8 @@ module Mochigome
|
|
28
27
|
agg_rel.join_on_path_thru([focus_model, data_model])
|
29
28
|
agg_rel.apply_access_filter_func(@access_filter)
|
30
29
|
|
31
|
-
|
32
|
-
|
33
|
-
key_path = key_path.select{|m| @layer_types.include?(m)}
|
34
|
-
key_cols = key_path.map{|m| m.arel_primary_key}
|
30
|
+
key_models = @ids_rel.spine_layers_thru(focus_model)
|
31
|
+
key_cols = key_models.map{|m| m.arel_primary_key}
|
35
32
|
|
36
33
|
agg_fields = data_model.mochigome_aggregation_settings.
|
37
34
|
options[:fields].reject{|a| a[:in_ruby]}
|
@@ -86,7 +83,7 @@ module Mochigome
|
|
86
83
|
Mochigome Version: #{Mochigome::VERSION}
|
87
84
|
Report Generated: #{Time.now}
|
88
85
|
Layers: #{@layer_types.map(&:name).join(" => ")}
|
89
|
-
AR Path: #{@
|
86
|
+
AR Path: #{@ids_rel.full_spine_path.map(&:name).join(" => ")}
|
90
87
|
eos
|
91
88
|
root.comment.gsub!(/(\n|^) +/, "\\1")
|
92
89
|
|
@@ -191,11 +188,12 @@ module Mochigome
|
|
191
188
|
|
192
189
|
class Relation
|
193
190
|
def initialize(layers)
|
191
|
+
@model_graph = ModelGraph.new
|
194
192
|
@spine_layers = layers
|
195
|
-
@spine =
|
193
|
+
@spine = @model_graph.path_thru(layers) or
|
196
194
|
raise QueryError.new("No valid path thru #{layers.inspect}") #TODO Test
|
197
195
|
@models = Set.new @spine
|
198
|
-
@rel =
|
196
|
+
@rel = @model_graph.relation_over_path(@spine)
|
199
197
|
|
200
198
|
@spine_layers.each{|m| select_model_id(m)}
|
201
199
|
end
|
@@ -208,6 +206,16 @@ module Mochigome
|
|
208
206
|
@rel.to_sql
|
209
207
|
end
|
210
208
|
|
209
|
+
def full_spine_path
|
210
|
+
@spine.dup
|
211
|
+
end
|
212
|
+
|
213
|
+
def spine_layers_thru(model)
|
214
|
+
r = @spine.take_while{|m| m != model}
|
215
|
+
r << model unless r.size == @spine.size
|
216
|
+
r.select{|m| @spine_layers.include? m}
|
217
|
+
end
|
218
|
+
|
211
219
|
def clone
|
212
220
|
c = super
|
213
221
|
c.instance_variable_set :@spine, @spine.dup
|
@@ -222,7 +230,7 @@ module Mochigome
|
|
222
230
|
# Route to it in as few steps as possible, closer to spine end if tie.
|
223
231
|
best_path = nil
|
224
232
|
(@spine.reverse + (@models.to_a - @spine)).each do |link_model|
|
225
|
-
path =
|
233
|
+
path = @model_graph.path_thru([link_model, model])
|
226
234
|
if path && (best_path.nil? || path.size < best_path.size)
|
227
235
|
best_path = path
|
228
236
|
end
|
@@ -233,7 +241,7 @@ module Mochigome
|
|
233
241
|
end
|
234
242
|
|
235
243
|
def join_on_path_thru(path)
|
236
|
-
join_on_path(
|
244
|
+
join_on_path(@model_graph.path_thru(path).uniq)
|
237
245
|
end
|
238
246
|
|
239
247
|
def join_on_path(path)
|
@@ -248,7 +256,7 @@ module Mochigome
|
|
248
256
|
end
|
249
257
|
|
250
258
|
def select_expr(e)
|
251
|
-
|
259
|
+
@model_graph.expr_models(e).each{|m| join_to_model(m)}
|
252
260
|
@rel = @rel.project(e)
|
253
261
|
end
|
254
262
|
|
@@ -264,7 +272,7 @@ module Mochigome
|
|
264
272
|
end
|
265
273
|
end
|
266
274
|
|
267
|
-
|
275
|
+
@model_graph.expr_models(cond).each{|m| join_to_model(m)}
|
268
276
|
@rel = @rel.where(cond)
|
269
277
|
end
|
270
278
|
|
@@ -289,7 +297,7 @@ module Mochigome
|
|
289
297
|
raise QueryError.new("Can't join from #{src}, not available") unless
|
290
298
|
@models.include?(src)
|
291
299
|
return if @models.include?(tgt) # TODO Maybe still apply join conditions?
|
292
|
-
@rel =
|
300
|
+
@rel = @model_graph.relation_func(src, tgt).call(@rel)
|
293
301
|
@models.add tgt
|
294
302
|
end
|
295
303
|
end
|
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mochigome
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- David Mike Simon
|
@@ -14,7 +15,7 @@ autorequire:
|
|
14
15
|
bindir: bin
|
15
16
|
cert_chain: []
|
16
17
|
|
17
|
-
date: 2012-04-
|
18
|
+
date: 2012-04-10 00:00:00 Z
|
18
19
|
dependencies:
|
19
20
|
- !ruby/object:Gem::Dependency
|
20
21
|
version_requirements: &id001 !ruby/object:Gem::Requirement
|
@@ -73,7 +74,21 @@ dependencies:
|
|
73
74
|
prerelease: false
|
74
75
|
type: :runtime
|
75
76
|
name: rgl
|
76
|
-
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
hash: 3
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
requirement: *id005
|
88
|
+
prerelease: false
|
89
|
+
type: :runtime
|
90
|
+
name: activerecord
|
91
|
+
description: Report generator that graphs over ActiveRecord associations
|
77
92
|
email: david.mike.simon@gmail.com
|
78
93
|
executables: []
|
79
94
|
|
@@ -159,6 +174,6 @@ rubyforge_project: "[none]"
|
|
159
174
|
rubygems_version: 1.8.6
|
160
175
|
signing_key:
|
161
176
|
specification_version: 3
|
162
|
-
summary:
|
177
|
+
summary: The do-what-I-mean report generator
|
163
178
|
test_files: []
|
164
179
|
|