mochigome 0.1 → 0.1.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/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
|
|