bud 0.0.7 → 0.0.8

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/bin/budplot CHANGED
@@ -4,7 +4,9 @@ require 'bud'
4
4
  require 'bud/bud_meta'
5
5
  require 'bud/depanalysis'
6
6
  require 'bud/graphs'
7
+ require 'bud/meta_algebra'
7
8
  require 'bud/viz_util'
9
+ require 'getopt/std'
8
10
 
9
11
  include VizUtil
10
12
 
@@ -42,12 +44,34 @@ def make_instance(mods)
42
44
 
43
45
  def_lines = ["class FooBar",
44
46
  "include Bud",
47
+ "include MetaAlgebra",
48
+ "include MetaReports",
45
49
  mods.map {|m| "include #{m}"},
46
50
  "end"
47
51
  ]
48
52
  class_def = def_lines.flatten.join("\n")
49
53
  eval(class_def)
50
- FooBar.new
54
+ f =FooBar.new
55
+ 3.times{ f.tick }
56
+ f
57
+ end
58
+
59
+ def trace_counts(begins)
60
+ complexity = {:data => {}, :coord => {}}
61
+ if !begins[:start].nil?
62
+ begins[:start].each_pair do |k, v|
63
+ if @data and @data[k]
64
+ complexity[:data][k] = @data[k].length
65
+ end
66
+ end
67
+
68
+ begins[:finish].each_pair do |k, v|
69
+ if @data and @data[k]
70
+ complexity[:coord][k] = @data[k].length
71
+ end
72
+ end
73
+ end
74
+ complexity
51
75
  end
52
76
 
53
77
  def process(mods)
@@ -79,12 +103,44 @@ def process(mods)
79
103
  end
80
104
 
81
105
  viz_name = "bud_doc/" + mods.join("_") + "_viz"
82
- write_index(inp, outp, priv, viz_name)
83
- graph_from_instance(d, "#{viz_name}_collapsed", "bud_doc", true)
84
- graph_from_instance(d, "#{viz_name}_expanded", "bud_doc", false)
106
+ graph_from_instance(d, "#{viz_name}_collapsed", "bud_doc", true, nil, @data)
107
+ graph_from_instance(d, "#{viz_name}_expanded", "bud_doc", false, nil, @data)
108
+ begins = graph_from_instance(d, "#{viz_name}_expanded_dot", "bud_doc", false, "dot", @data)
109
+
110
+
111
+ complexity = trace_counts(begins)
112
+ # try to figure out the degree of the async edges
113
+ deg = find_degrees(d, @data)
114
+ unless deg.nil?
115
+ deg.each_pair do |k, v|
116
+ puts "DEGREE: #{k} = #{v.keys.length}"
117
+ end
118
+ end
119
+
120
+ write_index(inp, outp, priv, viz_name, complexity)
85
121
  end
86
122
 
87
- def write_index(inp, outp, priv, viz_name)
123
+ def find_degrees(inst, data)
124
+ degree = {}
125
+ return if data.nil?
126
+ data.each_pair do |k, v|
127
+ tab = inst.tables[k.gsub("_snd", "").to_sym]
128
+ if !tab.nil?
129
+ if tab.class == Bud::BudChannel
130
+ v.each_pair do |k2, v2|
131
+ v2.each do |row|
132
+ loc = row[tab.locspec_idx]
133
+ degree[k] ||= {}
134
+ degree[k][loc] = true
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ return degree
141
+ end
142
+
143
+ def write_index(inp, outp, priv, viz_name, cx)
88
144
  f = File.open("bud_doc/index.html", "w")
89
145
  f.puts "<html>"
90
146
  f.puts "<embed src=\"#{ENV['PWD']}/#{viz_name}_collapsed.svg\" width=\"100%\" height=\"60%\" type=\"image/svg+xml\" pluginspage=\"http://www.adobe.com/svg/viewer/install/\" />"
@@ -97,14 +153,24 @@ def write_index(inp, outp, priv, viz_name)
97
153
  f.puts "<h2>Output Interfaces</h2>"
98
154
  do_table(f, outp)
99
155
  f.puts "</td><td>"
100
- f.puts "<h2>Private State</h2>"
101
- do_table(f, priv)
102
- f.puts "</td>"
156
+ f.puts "<h2>Trace Analysis Results</h2>"
157
+ f.puts "<h3>Data Complexity</h3>"
158
+ do_cx(f, cx[:data])
159
+ f.puts "<h3>Coordination Complexity</h3>"
160
+ do_cx(f, cx[:coord])
103
161
  f.puts "</tr></table>"
104
162
  f.puts "</html>"
105
163
  f.close
106
164
  end
107
165
 
166
+ def do_cx(f, cx)
167
+ f.puts "<table border='1'>"
168
+ cx.each_pair do |k, v|
169
+ f.puts "<tr><td>#{k}</td><td>#{v.inspect}</td></tr>"
170
+ end
171
+ f.puts "</table>"
172
+ end
173
+
108
174
  def do_table(f, info)
109
175
  f.puts "<table border='1'>"
110
176
  info.sort{|a, b| a[0].to_s <=> b[0].to_s}.each do |tbl_name, tbl_impl|
@@ -117,11 +183,34 @@ def do_table(f, info)
117
183
  f.puts "</table>"
118
184
  end
119
185
 
186
+ def get_trace_data
187
+ data = nil
188
+
189
+ if @opts["t"]
190
+ data = {}
191
+ traces = @opts['t'].class == String ? [@opts['t']] : @opts['t']
192
+ traces.each do |t|
193
+ meta, da = get_meta2(t)
194
+ da.each do |d|
195
+ data[d[1]] ||= {}
196
+ data[d[1]][d[0]] ||= []
197
+ data[d[1]][d[0]] << d[2]
198
+ end
199
+ end
200
+ end
201
+ data
202
+ end
203
+
120
204
  if ARGV.length < 2
121
205
  puts "Usage: budplot LIST_OF_FILES LIST_OF_MODULES_OR_CLASSES"
122
206
  exit
123
207
  end
124
208
 
209
+ @opts = Getopt::Std.getopts("t:")
210
+
211
+
212
+
213
+ @data = get_trace_data
125
214
  `mkdir bud_doc`
126
215
 
127
216
  modules = []
data/bin/budvis CHANGED
@@ -28,10 +28,9 @@ usage unless ARGV[0]
28
28
  usage if ARGV[0] == '--help'
29
29
 
30
30
  meta, data = get_meta2(BUD_DBM_DIR)
31
- vh = VizHelper.new(meta[:tabinf], meta[:cycle], meta[:depends], meta[:rules], ARGV[0])
31
+ vh = VizHelper.new(meta[:tabinf], meta[:cycle], meta[:depends], meta[:rules], ARGV[0], meta[:provides])
32
32
  data.each do |d|
33
33
  vh.full_info << d
34
34
  end
35
-
36
35
  vh.tick
37
36
  vh.summarize(ARGV[0], meta[:schminf])
data/docs/cheat.md CHANGED
@@ -196,7 +196,7 @@ implicit map:
196
196
  end
197
197
 
198
198
  ## BudCollection-Specific Methods ##
199
- `bc.schema`: returns the schema of `bc` (Hash of key column names => non-key column names)<br>
199
+ `bc.schema`: returns the schema of `bc` (Hash of key column names => non-key column names). Note that for channels, this omits the location specifier (<tt>@</tt>).<br>
200
200
 
201
201
  `bc.cols`: returns the column names in `bc` as an Array<br>
202
202
 
data/lib/bud/aggs.rb CHANGED
@@ -181,7 +181,8 @@ module Bud
181
181
  end
182
182
 
183
183
  # aggregate method to be used in Bud::BudCollection.group.
184
- # accumulates all x inputs into an array
184
+ # accumulates all x inputs into an array. note that the order of the elements
185
+ # in the resulting array is undefined.
185
186
  def accum(x)
186
187
  [Accum.new, x]
187
188
  end
data/lib/bud/bud_meta.rb CHANGED
@@ -49,7 +49,7 @@ class BudMeta #:nodoc: all
49
49
  @bud_instance.sinks[s.first] = true
50
50
  end
51
51
 
52
- dump_rewrite(rewritten_strata) if @bud_instance.options[:dump_rewrite]
52
+ dump_rewrite(no_attr_rewrite_strata) if @bud_instance.options[:dump_rewrite]
53
53
 
54
54
  return rewritten_strata, no_attr_rewrite_strata
55
55
  end
@@ -97,13 +97,13 @@ class BudMeta #:nodoc: all
97
97
  unless rv.nil?
98
98
  if rv.class <= Sexp
99
99
  error_pt = rv
100
- error_msg = "Parse error"
100
+ error_msg = "parse error"
101
101
  else
102
102
  error_pt, error_msg = rv
103
103
  end
104
104
 
105
- # try to "generate" the source code associated with the problematic block,
106
- # so as to generate a more meaningful error message.
105
+ # try to dump the source code associated with the problematic block, so as
106
+ # to produce a more meaningful error message.
107
107
  begin
108
108
  code = Ruby2Ruby.new.process(Marshal.load(Marshal.dump(error_pt)))
109
109
  src_msg = "\nCode: #{code}"
@@ -139,7 +139,7 @@ class BudMeta #:nodoc: all
139
139
 
140
140
  # Check for a common case
141
141
  if n.sexp_type == :lasgn
142
- return [n, "Illegal operator: '='"]
142
+ return [n, "illegal operator: '='"]
143
143
  end
144
144
  return pt unless n.sexp_type == :call and n.length == 4
145
145
 
@@ -150,10 +150,10 @@ class BudMeta #:nodoc: all
150
150
  return n if lhs.nil? or lhs.sexp_type != :call
151
151
  lhs_name = lhs[2].to_sym
152
152
  unless @bud_instance.tables.has_key? lhs_name
153
- return [n, "Table does not exist: '#{lhs_name}'"]
153
+ return [n, "collection does not exist: '#{lhs_name}'"]
154
154
  end
155
155
 
156
- return [n, "Illegal operator: '#{op}'"] unless [:<, :<=].include? op
156
+ return [n, "illegal operator: '#{op}'"] unless [:<, :<=].include? op
157
157
 
158
158
  # Check superator invocation. A superator that begins with "<" is parsed
159
159
  # as a call to the binary :< operator. The right operand to :< is a :call
@@ -195,6 +195,7 @@ class BudMeta #:nodoc: all
195
195
  else
196
196
  top = 1
197
197
  end
198
+
198
199
  return top
199
200
  end
200
201
 
@@ -149,7 +149,7 @@ module Bud
149
149
  # project the collection to its key attributes
150
150
  public
151
151
  def keys
152
- self.map{|t| @key_colnums.map {|i| t[i]}}
152
+ self.map{|t| get_key_vals(t)}
153
153
  end
154
154
 
155
155
  # project the collection to its non-key attributes
@@ -255,6 +255,7 @@ module Bud
255
255
  def [](k)
256
256
  # assumes that key is in storage or delta, but not both
257
257
  # is this enforced in do_insert?
258
+ check_enumerable(k)
258
259
  t = @storage[k]
259
260
  return t.nil? ? @delta[k] : t
260
261
  end
@@ -264,7 +265,7 @@ module Bud
264
265
  def include?(item)
265
266
  return true if key_cols.nil? or (key_cols.empty? and length > 0)
266
267
  return false if item.nil? or item.empty?
267
- key = @key_colnums.map{|i| item[i]}
268
+ key = get_key_vals(item)
268
269
  return (item == self[key])
269
270
  end
270
271
 
@@ -282,8 +283,8 @@ module Bud
282
283
 
283
284
  private
284
285
  def raise_pk_error(new_guy, old)
285
- keycols = @key_colnums.map{|i| old[i]}
286
- raise KeyConstraintError, "key conflict inserting #{new_guy.inspect} into \"#{tabname}\": existing tuple #{old.inspect}, key_cols = #{keycols.inspect}"
286
+ key = get_key_vals(old)
287
+ raise KeyConstraintError, "key conflict inserting #{new_guy.inspect} into \"#{tabname}\": existing tuple #{old.inspect}, key = #{key.inspect}"
287
288
  end
288
289
 
289
290
  private
@@ -308,15 +309,22 @@ module Bud
308
309
  return o
309
310
  end
310
311
 
312
+ private
313
+ def get_key_vals(t)
314
+ @key_colnums.map do |i|
315
+ t[i]
316
+ end
317
+ end
318
+
311
319
  private
312
320
  def do_insert(o, store)
313
321
  return if o.nil? # silently ignore nils resulting from map predicates failing
314
322
  o = prep_tuple(o)
315
- keycols = @key_colnums.map{|i| o[i]}
323
+ key = get_key_vals(o)
316
324
 
317
- old = store[keycols]
325
+ old = store[key]
318
326
  if old.nil?
319
- store[keycols] = tuple_accessors(o)
327
+ store[key] = tuple_accessors(o)
320
328
  else
321
329
  raise_pk_error(o, old) unless old == o
322
330
  end
@@ -374,10 +382,10 @@ module Bud
374
382
  end
375
383
 
376
384
  private
377
- def include_any_buf?(t, key_vals)
385
+ def include_any_buf?(t, key)
378
386
  bufs = [self, @delta, @new_delta]
379
387
  bufs.each do |b|
380
- old = b[key_vals]
388
+ old = b[key]
381
389
  next if old.nil?
382
390
  if old != t
383
391
  raise_pk_error(t, old)
@@ -391,16 +399,15 @@ module Bud
391
399
  public
392
400
  def merge(o, buf=@new_delta) # :nodoc: all
393
401
  unless o.nil?
394
- o = o.uniq if o.respond_to?(:uniq)
395
402
  check_enumerable(o)
396
403
  establish_schema(o) if @cols.nil?
397
404
 
398
- # it's a pity that we are massaging the tuples that already exist in the head
405
+ # it's a pity that we are massaging tuples that may be dups
399
406
  o.each do |t|
400
407
  next if t.nil? or t == []
401
408
  t = prep_tuple(t)
402
- key_vals = @key_colnums.map{|k| t[k]}
403
- buf[key_vals] = tuple_accessors(t) unless include_any_buf?(t, key_vals)
409
+ key = get_key_vals(t)
410
+ buf[key] = tuple_accessors(t) unless include_any_buf?(t, key)
404
411
  end
405
412
  end
406
413
  return self
@@ -432,7 +439,7 @@ module Bud
432
439
  self <+ o
433
440
  self <- o.map do |t|
434
441
  unless t.nil?
435
- self[@key_colnums.map{|k| t[k]}]
442
+ self[get_key_vals(t)]
436
443
  end
437
444
  end
438
445
  end
@@ -461,9 +468,14 @@ module Bud
461
468
  @new_delta = {}
462
469
  end
463
470
 
464
- private
465
- def method_missing(sym, *args, &block)
466
- @storage.send sym, *args, &block
471
+ public
472
+ def length
473
+ @storage.length
474
+ end
475
+
476
+ public
477
+ def empty?
478
+ @storage.empty?
467
479
  end
468
480
 
469
481
  ######## aggs
@@ -480,7 +492,6 @@ module Bud
480
492
  end
481
493
  end
482
494
 
483
-
484
495
  # a generalization of argmin/argmax to arbitrary exemplary aggregates.
485
496
  # for each distinct value of the grouping key columns, return the items in that group
486
497
  # that have the value of the exemplary aggregate +aggname+
@@ -592,7 +603,7 @@ module Bud
592
603
  # Attributes can be referenced as symbols, or as +collection_name.attribute_name+
593
604
  public
594
605
  def group(key_cols, *aggpairs)
595
- key_cols = [] if key_cols.nil?
606
+ key_cols ||= []
596
607
  keynames = key_cols.map do |k|
597
608
  if k.class == Symbol
598
609
  k
@@ -892,15 +903,15 @@ module Bud
892
903
  public
893
904
  def tick #:nodoc: all
894
905
  @to_delete.each do |tuple|
895
- keycols = @key_colnums.map{|k| tuple[k]}
896
- if @storage[keycols] == tuple
897
- @storage.delete keycols
906
+ key = get_key_vals(tuple)
907
+ if @storage[key] == tuple
908
+ @storage.delete key
898
909
  end
899
910
  end
900
- @pending.each do |keycols, tuple|
901
- old = @storage[keycols]
911
+ @pending.each do |key, tuple|
912
+ old = @storage[key]
902
913
  if old.nil?
903
- @storage[keycols] = tuple
914
+ @storage[key] = tuple
904
915
  else
905
916
  raise_pk_error(tuple, old) unless tuple == old
906
917
  end
@@ -957,26 +968,3 @@ module Bud
957
968
  end
958
969
  end
959
970
  end
960
-
961
- module Enumerable
962
- public
963
- # monkeypatch to Enumerable to rename collections and their schemas
964
- def rename(new_tabname, new_schema=nil)
965
- budi = (respond_to?(:bud_instance)) ? bud_instance : nil
966
- if new_schema.nil? and respond_to?(:schema)
967
- new_schema = schema
968
- end
969
- scr = Bud::BudScratch.new(new_tabname.to_s, budi, new_schema)
970
- scr.uniquify_tabname
971
- scr.merge(self, scr.storage)
972
- scr
973
- end
974
-
975
- public
976
- # We rewrite "map" calls in Bloom blocks to invoke the "pro" method
977
- # instead. This is fine when applied to a BudCollection; when applied to a
978
- # normal Enumerable, just treat pro as an alias for map.
979
- def pro(&blk)
980
- map(&blk)
981
- end
982
- end
data/lib/bud/graphs.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  require 'rubygems'
2
- require 'digest/md5'
3
2
  require 'graphviz'
4
3
 
5
4
  class GraphGen #:nodoc: all
6
5
  attr_reader :nodes
7
6
 
8
- def initialize(tableinfo, builtin_tables, cycle, name, budtime, collapse=false, cardinalities={})
7
+ def initialize(tableinfo, builtin_tables, cycle, name, budtime, collapse=false, cardinalities={}, pathsto={}, begins={})
9
8
  @graph = GraphViz.new(:G, :type => :digraph, :label => "")
9
+ #@graph.dim = 2
10
10
  @graph.node[:fontname] = "Times-Roman"
11
11
  @graph.node[:fontsize] = 18
12
12
  @graph.edge[:fontname] = "Times-Roman"
@@ -16,6 +16,8 @@ class GraphGen #:nodoc: all
16
16
  @collapse = collapse
17
17
  @budtime = budtime
18
18
  @builtin_tables = builtin_tables
19
+ @pathsto = pathsto
20
+ @begins = begins
19
21
 
20
22
  # map: table name -> type
21
23
  @tabinf = {}
@@ -87,20 +89,46 @@ class GraphGen #:nodoc: all
87
89
  end
88
90
  end
89
91
 
92
+ def color_node(paths)
93
+ return "" if paths.nil?
94
+
95
+ case paths[0][:val]
96
+ when :A, :N
97
+ "yellow"
98
+ when :D, :G
99
+ "red"
100
+ else
101
+ puts "UNKNOWN tag #{paths[0][:val]} class #{paths[0][:val].class}"
102
+ "black"
103
+ end
104
+ end
105
+
90
106
  def addonce(node, negcluster, inhead=false)
91
107
  if !@nodes[node]
92
- @nodes[node] = @graph.add_node("n_#{node}")
108
+ @nodes[node] = @graph.add_nodes(node)
109
+ node_p = @nodes[node]
110
+ node_p.label = node
111
+ if @begins[:finish] and @begins[:finish][node]
112
+ # point of divergence.
113
+ node_p.penwidth = 4
114
+ end
115
+
93
116
  if @cards and @cards[node]
94
- @nodes[node].label = "#{node}\n (#{@cards[node].to_s})"
117
+ node_p.label = "#{node}\n (#{@cards[node].to_s})"
118
+ node_p.color = "green"
95
119
  else
96
- @nodes[node].label = node
120
+ p = @pathsto[node].nil? ? "" : "\n(#{@pathsto[node][0][:val]})"
121
+ node_p.label = node + p
122
+ node_p.color = color_node(@pathsto[node])
97
123
  end
124
+ else
125
+ node_p = @nodes[node]
98
126
  end
99
127
 
100
128
  if @budtime == -1
101
- @nodes[node].URL = "#{node}.html" if inhead
129
+ node_p.URL = "#{node}.html" if inhead
102
130
  else
103
- @nodes[node].URL = "javascript:openWin(\"#{node}\", #{@budtime})"
131
+ node_p.URL = "javascript:openWin(\"#{node}\", #{@budtime})"
104
132
  end
105
133
 
106
134
  if negcluster
@@ -116,13 +144,13 @@ class GraphGen #:nodoc: all
116
144
  res = res + ", " + p
117
145
  end
118
146
  end
119
- @nodes[node].label = res
120
- @nodes[node].color = "red"
121
- @nodes[node].shape = "octagon"
122
- @nodes[node].penwidth = 3
123
- @nodes[node].URL = "#{File.basename(@name).gsub(".staging", "").gsub("collapsed", "expanded")}.svg"
147
+ node_p.label = res
148
+ node_p.color = "red"
149
+ node_p.shape = "octagon"
150
+ node_p.penwidth = 3
151
+ node_p.URL = "#{File.basename(@name).gsub(".staging", "").gsub("collapsed", "expanded")}.svg"
124
152
  elsif @tabinf[node] and (@tabinf[node] == "Bud::BudTable")
125
- @nodes[node].shape = "rect"
153
+ node_p.shape = "rect"
126
154
  end
127
155
  end
128
156
 
@@ -134,9 +162,10 @@ class GraphGen #:nodoc: all
134
162
 
135
163
  ekey = body + head
136
164
  if !@edges[ekey]
137
- @edges[ekey] = @graph.add_edge(@nodes[body], @nodes[head], :penwidth => 5)
165
+ @edges[ekey] = @graph.add_edges(@nodes[body], @nodes[head], :penwidth => 5)
138
166
  @edges[ekey].arrowsize = 2
139
167
 
168
+ @edges[ekey].color = (@nodes[body]["color"].source || "")
140
169
  @edges[ekey].URL = "#{rule_id}.html" unless rule_id.nil?
141
170
  if head =~ /_msg\z/
142
171
  @edges[ekey].minlen = 2
@@ -212,7 +241,7 @@ class GraphGen #:nodoc: all
212
241
  if output.nil?
213
242
  @graph.output(:svg => @name)
214
243
  else
215
- @graph.output(output => @name)
244
+ @graph.output(output.to_sym => @name)
216
245
  end
217
246
  end
218
247
  end
@@ -234,9 +263,8 @@ class SpaceTime
234
263
  @head = {}
235
264
  last = nil
236
265
  processes.each_with_index do |p, i|
237
- #@head[p] = @hdr.add_node("process #{p}(#{i})")#, :color => "white", :label => "")
238
266
  @subs[p] = @g.subgraph("buster_#{i+1}")
239
- @head[p] = @hdr.add_node("process #{p}(#{i})", :group => p)#, :color => "white", :label => "")
267
+ @head[p] = @hdr.add_nodes("process #{p}(#{i})", :group => p)#, :color => "white", :label => "")
240
268
  end
241
269
  end
242
270
 
@@ -272,10 +300,9 @@ class SpaceTime
272
300
  if @links
273
301
  url = "DBM_#{k}/tm_#{item}.svg"
274
302
  end
275
- snd = @subs[k].add_node(label, {:label => item.to_s, :width => 0.1, :height => 0.1, :fontsize => 6, :pos => [1, i], :group => k, :URL => url})
276
-
303
+ snd = @subs[k].add_nodes(label, {:label => item.to_s, :width => 0.1, :height => 0.1, :fontsize => 6, :group => k, :URL => url})
277
304
  unless @head[k].id == snd.id
278
- @subs[k].add_edge(@head[k], snd, :weight => 2)
305
+ @subs[k].add_edges(@head[k], snd, :weight => 2)
279
306
  @head[k] = snd
280
307
  end
281
308
  end
@@ -295,7 +322,7 @@ class SpaceTime
295
322
  def finish(file, fmt=nil)
296
323
  @edges.each_pair do |k, v|
297
324
  lbl = v[3] > 1 ? "#{v[2]}(#{v[3]})" : v[2]
298
- @g.add_edge(v[0], v[1], :label => lbl, :color => "red", :weight => 1)
325
+ @g.add_edges(v[0], v[1], :label => lbl, :color => "red", :weight => 1)
299
326
  end
300
327
  if fmt.nil?
301
328
  @g.output(:svg => "#{file}.svg")
data/lib/bud/joins.rb CHANGED
@@ -291,7 +291,7 @@ module Bud
291
291
  otherpreds = allpreds - @localpreds
292
292
  unless otherpreds.empty?
293
293
  unless @rels[1].class <= Bud::BudJoin
294
- raise Bud::CompileError, "join predicates don't match tables being joined: #{otherpreds.inspect}"
294
+ raise Bud::CompileError, "join predicates don't match collections being joined: #{otherpreds.inspect}"
295
295
  end
296
296
  @rels[1].setup_preds(otherpreds)
297
297
  end