bud 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/LICENSE +9 -0
  2. data/README +30 -0
  3. data/bin/budplot +134 -0
  4. data/bin/budvis +201 -0
  5. data/bin/rebl +4 -0
  6. data/docs/README.md +13 -0
  7. data/docs/bfs.md +379 -0
  8. data/docs/bfs.raw +251 -0
  9. data/docs/bfs_arch.png +0 -0
  10. data/docs/bloom-loop.png +0 -0
  11. data/docs/bust.md +83 -0
  12. data/docs/cheat.md +291 -0
  13. data/docs/deploy.md +96 -0
  14. data/docs/diffs +181 -0
  15. data/docs/getstarted.md +296 -0
  16. data/docs/intro.md +36 -0
  17. data/docs/modules.md +112 -0
  18. data/docs/operational.md +96 -0
  19. data/docs/rebl.md +99 -0
  20. data/docs/ruby_hooks.md +19 -0
  21. data/docs/visualizations.md +75 -0
  22. data/examples/README +1 -0
  23. data/examples/basics/hello.rb +12 -0
  24. data/examples/basics/out +1103 -0
  25. data/examples/basics/out.new +856 -0
  26. data/examples/basics/paths.rb +51 -0
  27. data/examples/bust/README.md +9 -0
  28. data/examples/bust/bustclient-example.rb +23 -0
  29. data/examples/bust/bustinspector.html +135 -0
  30. data/examples/bust/bustserver-example.rb +18 -0
  31. data/examples/chat/README.md +9 -0
  32. data/examples/chat/chat.rb +45 -0
  33. data/examples/chat/chat_protocol.rb +8 -0
  34. data/examples/chat/chat_server.rb +29 -0
  35. data/examples/deploy/tokenring-ec2.rb +26 -0
  36. data/examples/deploy/tokenring-local.rb +17 -0
  37. data/examples/deploy/tokenring.rb +39 -0
  38. data/lib/bud/aggs.rb +126 -0
  39. data/lib/bud/bud_meta.rb +185 -0
  40. data/lib/bud/bust/bust.rb +126 -0
  41. data/lib/bud/bust/client/idempotence.rb +10 -0
  42. data/lib/bud/bust/client/restclient.rb +49 -0
  43. data/lib/bud/collections.rb +937 -0
  44. data/lib/bud/depanalysis.rb +44 -0
  45. data/lib/bud/deploy/countatomicdelivery.rb +50 -0
  46. data/lib/bud/deploy/deployer.rb +67 -0
  47. data/lib/bud/deploy/ec2deploy.rb +200 -0
  48. data/lib/bud/deploy/localdeploy.rb +41 -0
  49. data/lib/bud/errors.rb +15 -0
  50. data/lib/bud/graphs.rb +405 -0
  51. data/lib/bud/joins.rb +300 -0
  52. data/lib/bud/rebl.rb +314 -0
  53. data/lib/bud/rewrite.rb +523 -0
  54. data/lib/bud/rtrace.rb +27 -0
  55. data/lib/bud/server.rb +43 -0
  56. data/lib/bud/state.rb +108 -0
  57. data/lib/bud/storage/tokyocabinet.rb +170 -0
  58. data/lib/bud/storage/zookeeper.rb +178 -0
  59. data/lib/bud/stratify.rb +83 -0
  60. data/lib/bud/viz.rb +65 -0
  61. data/lib/bud.rb +797 -0
  62. metadata +330 -0
@@ -0,0 +1,937 @@
1
+ require 'msgpack'
2
+
3
+ module Bud
4
+ ########
5
+ #--
6
+ # the collection types
7
+ # each collection is partitioned into 4:
8
+ # - pending holds tuples deferred til the next tick
9
+ # - storage holds the "normal" tuples
10
+ # - delta holds the delta for rhs's of rules during semi-naive
11
+ # - new_delta will hold the lhs tuples currently being produced during s-n
12
+ #++
13
+
14
+ class BudCollection
15
+ include Enumerable
16
+
17
+ attr_accessor :bud_instance, :locspec_idx # :nodoc: all
18
+ attr_reader :schema, :tabname # :nodoc: all
19
+ attr_reader :storage, :delta, :new_delta # :nodoc: all
20
+
21
+ def initialize(name, bud_instance, given_schema=nil, defer_schema=false) # :nodoc: all
22
+ @tabname = name
23
+ @bud_instance = bud_instance
24
+ init_schema(given_schema) unless given_schema.nil? and defer_schema
25
+ init_buffers
26
+ end
27
+
28
+ private
29
+ def init_buffers
30
+ @sealed = false
31
+ init_storage
32
+ init_pending
33
+ init_deltas
34
+ end
35
+
36
+ private
37
+ def init_schema(given_schema)
38
+ given_schema ||= {[:key]=>[:val]}
39
+ @given_schema = given_schema
40
+ @schema, @key_cols = parse_schema(given_schema)
41
+ @key_colnums = key_cols.map {|k| schema.index(k)}
42
+ setup_accessors
43
+ end
44
+
45
+ # The user-specified schema might come in two forms: a hash of Array =>
46
+ # Array (key_cols => remaining columns), or simply an Array of columns (if no
47
+ # key_cols were specified). Return a pair: [list of columns in entire tuple,
48
+ # list of key columns]
49
+ private
50
+ def parse_schema(given_schema)
51
+ if given_schema.respond_to? :keys
52
+ raise BudError, "invalid schema for #{tabname}" if given_schema.length != 1
53
+ key_cols = given_schema.keys.first
54
+ val_cols = given_schema.values.first
55
+ else
56
+ key_cols = given_schema
57
+ val_cols = []
58
+ end
59
+
60
+ schema = key_cols + val_cols
61
+ schema.each do |s|
62
+ if s.class != Symbol
63
+ raise BudError, "Invalid schema element \"#{s}\", type \"#{s.class}\""
64
+ end
65
+ end
66
+ if schema.uniq.length < schema.length
67
+ raise BudError, "schema for #{tabname} contains duplicate names"
68
+ end
69
+
70
+ return [schema, key_cols]
71
+ end
72
+
73
+ public
74
+ def clone_empty #:nodoc: all
75
+ self.class.new(tabname, bud_instance, @given_schema)
76
+ end
77
+
78
+ # subset of the schema (i.e. an array of attribute names) that forms the key
79
+ public
80
+ def key_cols
81
+ @key_cols
82
+ end
83
+
84
+ # subset of the schema (i.e. an array of attribute names) that is not in the key
85
+ public
86
+ def val_cols # :nodoc: all
87
+ schema - key_cols
88
+ end
89
+
90
+ # define methods to turn 'table.col' into a [table,col] pair
91
+ # e.g. to support something like
92
+ # j = join link, path, {link.to => path.from}
93
+ private
94
+ def setup_accessors
95
+ s = @schema
96
+ s.each do |colname|
97
+ reserved = eval "defined?(#{colname})"
98
+ unless (reserved.nil? or
99
+ (reserved == "method" and method(colname).arity == -1 and (eval(colname))[0] == self.tabname))
100
+ raise BudError, "symbol :#{colname} reserved, cannot be used as column name for #{tabname}"
101
+ end
102
+ end
103
+
104
+ # set up schema accessors, which are class methods
105
+ m = Module.new do
106
+ s.each_with_index do |c, i|
107
+ define_method c do
108
+ [@tabname, i, c]
109
+ end
110
+ end
111
+ end
112
+ self.extend m
113
+
114
+ # now set up a Module for tuple accessors, which are instance methods
115
+ @tupaccess = Module.new do
116
+ s.each_with_index do |colname, offset|
117
+ define_method colname do
118
+ self[offset]
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ # define methods to access tuple attributes by column name
125
+ private
126
+ def tuple_accessors(tup)
127
+ tup.extend @tupaccess
128
+ end
129
+
130
+ # generate a tuple with the schema of this collection and nil values in each attribute
131
+ public
132
+ def null_tuple
133
+ tuple_accessors(Array.new(@schema.length))
134
+ end
135
+
136
+ # project the collection to its key attributes
137
+ public
138
+ def keys
139
+ self.map{|t| (0..self.key_cols.length-1).map{|i| t[i]}}
140
+ end
141
+
142
+ # project the collection to its non-key attributes
143
+ public
144
+ def values
145
+ self.map{|t| (self.key_cols.length..self.schema.length-1).map{|i| t[i]}}
146
+ end
147
+
148
+ # map each item in the collection into a string, suitable for placement in stdio
149
+ public
150
+ def inspected
151
+ self.map{|t| [t.inspect]}
152
+ end
153
+
154
+ # akin to map, but modified for efficiency in Bloom statements
155
+ public
156
+ def pro(&blk)
157
+ if @bud_instance.stratum_first_iter
158
+ return map(&blk)
159
+ else
160
+ if @delta.empty?
161
+ return []
162
+ else
163
+ retval = []
164
+ each_from([@delta]) do |t|
165
+ newitem = blk.call(t)
166
+ retval << newitem unless newitem.nil?
167
+ end
168
+ return retval
169
+ end
170
+ end
171
+ end
172
+
173
+ # By default, all tuples in any rhs are in storage or delta. Tuples in
174
+ # new_delta will get transitioned to delta in the next iteration of the
175
+ # evaluator (but within the current time tick).
176
+ public
177
+ def each(&block) # :nodoc: all
178
+ each_from([@storage, @delta], &block)
179
+ end
180
+
181
+ private
182
+ def each_from(bufs, &block) # :nodoc: all
183
+ bufs.each do |b|
184
+ b.each_value do |v|
185
+ yield v
186
+ end
187
+ end
188
+ end
189
+
190
+ public
191
+ def each_from_sym(buf_syms, &block) # :nodoc: all
192
+ bufs = buf_syms.map do |s|
193
+ case s
194
+ when :storage then @storage
195
+ when :delta then @delta
196
+ when :new_delta then @new_delta
197
+ else raise BudError, "bad symbol passed into each_from_sym"
198
+ end
199
+ end
200
+ each_from(bufs, &block)
201
+ end
202
+
203
+ private
204
+ def init_storage
205
+ @storage = {}
206
+ end
207
+
208
+ private
209
+ def init_pending
210
+ @pending = {}
211
+ end
212
+
213
+ private
214
+ def init_deltas
215
+ @delta = {}
216
+ @new_delta = {}
217
+ end
218
+
219
+ public
220
+ def close # :nodoc: all
221
+ end
222
+
223
+ # checks for key +k+ in the key columns
224
+ public
225
+ def has_key?(k)
226
+ return false if k.nil? or k.empty? or self[k].nil?
227
+ return true
228
+ end
229
+
230
+ # return item with key +k+
231
+ public
232
+ def [](k)
233
+ # assumes that key is in storage or delta, but not both
234
+ # is this enforced in do_insert?
235
+ return @storage[k].nil? ? @delta[k] : @storage[k]
236
+ end
237
+
238
+ # checks for +item+ in the collection
239
+ public
240
+ def include?(item)
241
+ return true if key_cols.nil? or (key_cols.empty? and length > 0)
242
+ return false if item.nil? or item.empty?
243
+ key = key_cols.map{|k| item[schema.index(k)]}
244
+ return (item == self[key])
245
+ end
246
+
247
+ # checks for an item for which +block+ produces a match
248
+ public
249
+ def exists?(&block)
250
+ if length == 0
251
+ return false
252
+ elsif not block_given?
253
+ return true
254
+ else
255
+ return ((detect{|t| yield t}).nil?) ? false : true
256
+ end
257
+ end
258
+
259
+ private
260
+ def raise_pk_error(new, old)
261
+ keycols = key_cols.map{|k| old[schema.index(k)]}
262
+ raise KeyConstraintError, "Key conflict inserting #{old.inspect} into \"#{tabname}\": existing tuple #{new.inspect}, key_cols = #{keycols.inspect}"
263
+ end
264
+
265
+ private
266
+ def prep_tuple(o)
267
+ unless o.respond_to?(:length) and o.respond_to?(:[])
268
+ raise BudTypeError, "non-indexable type inserted into BudCollection #{self.tabname}: #{o.inspect}"
269
+ end
270
+
271
+ if o.length < schema.length then
272
+ # if this tuple has too few fields, pad with nil's
273
+ old = o.clone
274
+ (o.length..schema.length-1).each{|i| o << nil}
275
+ # puts "in #{@tabname}, converted #{old.inspect} to #{o.inspect}"
276
+ elsif o.length > schema.length then
277
+ # if this tuple has more fields than usual, bundle up the
278
+ # extras into an array
279
+ o = (0..(schema.length - 1)).map{|c| o[c]} << (schema.length..(o.length - 1)).map{|c| o[c]}
280
+ end
281
+ return o
282
+ end
283
+
284
+ private
285
+ def do_insert(o, store)
286
+ return if o.nil? # silently ignore nils resulting from map predicates failing
287
+ o = prep_tuple(o)
288
+ keycols = @key_colnums.map{|i| o[i]}
289
+
290
+ old = store[keycols]
291
+ if old.nil?
292
+ store[keycols] = tuple_accessors(o)
293
+ else
294
+ raise_pk_error(o, old) unless old == o
295
+ end
296
+ end
297
+
298
+ public
299
+ def insert(o) # :nodoc: all
300
+ # puts "insert: #{o.inspect} into #{tabname}"
301
+ do_insert(o, @delta)
302
+ end
303
+
304
+ # instantaneously place an individual item from rhs into collection on lhs
305
+ def <<(item)
306
+ insert(item)
307
+ end
308
+
309
+ private
310
+ def check_enumerable(o)
311
+ unless (o.nil? or o.class < Enumerable) and o.respond_to? 'each'
312
+ raise BudTypeError, "Attempt to merge non-enumerable type into BudCollection"
313
+ end
314
+ end
315
+
316
+ # Assign self a schema, by hook or by crook. If +o+ is schemaless *and*
317
+ # empty, will leave @schema as is.
318
+ private
319
+ def establish_schema(o)
320
+ # use o's schema if available
321
+ deduce_schema(o) if @schema.nil?
322
+ # else use arity of first tuple of o
323
+ fit_schema(o.first.size) if @schema.nil? and not o.first.nil?
324
+ return @schema
325
+ end
326
+
327
+ # Copy over the schema from +o+ if available
328
+ private
329
+ def deduce_schema(o)
330
+ if @schema.nil? and o.class <= Bud::BudCollection and not o.schema.nil?
331
+ # must have been initialized with defer_schema==true. take schema from rhs
332
+ init_schema(o.schema)
333
+ end
334
+ # returns old state of @schema (nil) if nothing available
335
+ return @schema
336
+ end
337
+
338
+ # manufacture schema of the form [:c0, :c1, ...] with width = +arity+
339
+ private
340
+ def fit_schema(arity)
341
+ # rhs is schemaless. create schema from first tuple merged
342
+ init_schema((0..arity-1).map{|indx| ("c"+indx.to_s).to_sym})
343
+ return @schema
344
+ end
345
+
346
+ public
347
+ def merge(o, buf=@new_delta) # :nodoc: all
348
+ check_enumerable(o)
349
+ establish_schema(o) if @schema.nil?
350
+
351
+ delta = o.map do |i|
352
+ next if i.nil? or i == []
353
+ i = prep_tuple(i)
354
+ key_vals = @key_colnums.map{|k| i[k]}
355
+ if (old = self[key_vals])
356
+ raise_pk_error(i, old) if old != i
357
+ elsif (oldnew = self.new_delta[key_vals])
358
+ raise_pk_error(i, oldnew) if oldnew != i
359
+ else
360
+ buf[key_vals] = tuple_accessors(i)
361
+ end
362
+ end
363
+ return self
364
+ end
365
+
366
+ public
367
+ # instantaneously merge items from collection +o+ into +buf+
368
+ def <=(collection)
369
+ merge(collection)
370
+ end
371
+
372
+ # buffer items to be merged atomically at end of this timestep
373
+ public
374
+ def pending_merge(o) # :nodoc: all
375
+ check_enumerable(o)
376
+ deduce_schema(o)
377
+
378
+ o.each {|i| do_insert(i, @pending)}
379
+ return self
380
+ end
381
+
382
+ public
383
+ superator "<+" do |o|
384
+ pending_merge o
385
+ end
386
+
387
+ # Called at the end of each timestep: prepare the collection for the next
388
+ # timestep.
389
+ public
390
+ def tick # :nodoc: all
391
+ @storage = @pending
392
+ @pending = {}
393
+ raise BudError, "orphaned tuples in @delta for #{@tabname}" unless @delta.empty?
394
+ raise BudError, "orphaned tuples in @new_delta for #{@tabname}" unless @new_delta.empty?
395
+ end
396
+
397
+ # move deltas to storage, and new_deltas to deltas.
398
+ public
399
+ def tick_deltas # :nodoc: all
400
+ # assertion: intersect(@storage, @delta) == nil
401
+ @storage.merge!(@delta)
402
+ @delta = @new_delta
403
+ @new_delta = {}
404
+ end
405
+
406
+ private
407
+ def method_missing(sym, *args, &block)
408
+ @storage.send sym, *args, &block
409
+ end
410
+
411
+ ######## aggs
412
+
413
+ private
414
+ # we only do grouping during first iteration of stratum. group and argagg should
415
+ # never deal with deltas. This assumes that stratification is done right, and it will
416
+ # be sensitive to bugs in the stratification!
417
+ def agg_in
418
+ if not respond_to?(:bud_instance) or bud_instance.nil? or bud_instance.stratum_first_iter
419
+ return self
420
+ else
421
+ return []
422
+ end
423
+ end
424
+
425
+
426
+ # a generalization of argmin/argmax to arbitrary exemplary aggregates.
427
+ # for each distinct value in the grouping key columns, return the item in that group
428
+ # that has the value of the exemplary aggregate +aggname+
429
+ public
430
+ def argagg(aggname, gbkey_cols, collection)
431
+ agg = bud_instance.send(aggname, nil)[0]
432
+ raise BudError, "#{aggname} not declared exemplary" unless agg.class <= Bud::ArgExemplary
433
+ keynames = gbkey_cols.map do |k|
434
+ if k.class == Symbol
435
+ k.to_s
436
+ else
437
+ k[2]
438
+ end
439
+ end
440
+ if collection.class == Symbol
441
+ colnum = self.send(collection.to_s)[1]
442
+ else
443
+ colnum = collection[1]
444
+ end
445
+ tups = agg_in.inject({}) do |memo,p|
446
+ pkey_cols = keynames.map{|n| p.send(n.to_sym)}
447
+ if memo[pkey_cols].nil?
448
+ memo[pkey_cols] = {:agg=>agg.send(:init, p[colnum]), :tups => [p]}
449
+ else
450
+ newval = agg.send(:trans, memo[pkey_cols][:agg], p[colnum])
451
+ if memo[pkey_cols][:agg] == newval
452
+ if agg.send(:tie, memo[pkey_cols][:agg], p[colnum])
453
+ memo[pkey_cols][:tups] << p
454
+ end
455
+ else
456
+ memo[pkey_cols] = {:agg=>newval, :tups=>[p]}
457
+ end
458
+ end
459
+ memo
460
+ end
461
+
462
+ finals = []
463
+ outs = tups.each_value do |t|
464
+ ties = t[:tups].map do |tie|
465
+ finals << tie
466
+ end
467
+ end
468
+
469
+ # merge directly into retval.storage, so that the temp tuples get picked up
470
+ # by the lhs of the rule
471
+ retval = BudScratch.new('argagg_temp', bud_instance, @given_schema)
472
+ retval.uniquify_tabname
473
+ retval.merge(finals, retval.storage)
474
+ end
475
+
476
+ # for each distinct value in the grouping key columns, return the item in that group
477
+ # that has the minimum value of the attribute +col+
478
+ public
479
+ def argmin(gbkey_cols, col)
480
+ argagg(:min, gbkey_cols, col)
481
+ end
482
+
483
+ # for each distinct value in the grouping key columns, return the item in that group
484
+ # that has the maximum value of the attribute +col+
485
+ public
486
+ def argmax(gbkey_cols, col)
487
+ argagg(:max, gbkey_cols, col)
488
+ end
489
+
490
+ # form a collection containing all pairs of items in +self+ and items in
491
+ # +collection+
492
+ public
493
+ def *(collection)
494
+ bud_instance.join([self, collection])
495
+ end
496
+
497
+ # SQL-style grouping. first argument is an array of attributes to group by.
498
+ # Followed by a variable-length list of aggregates over attributes (e.g. +min(:x)+)
499
+ # Attributes can be referenced as symbols, or as +collection_name.attribute_name+
500
+ public
501
+ def group(key_cols, *aggpairs)
502
+ key_cols = [] if key_cols.nil?
503
+ keynames = key_cols.map do |k|
504
+ if k.class == Symbol
505
+ k
506
+ elsif k[2] and k[2].class == Symbol
507
+ k[2]
508
+ else
509
+ raise Bud::CompileError, "Invalid grouping key"
510
+ end
511
+ end
512
+ aggcolsdups = aggpairs.map{|ap| ap[0].class.name.split("::").last}
513
+ aggcols = []
514
+ aggcolsdups.each_with_index do |n, i|
515
+ aggcols << "#{n.downcase}_#{i}".to_sym
516
+ end
517
+ tups = agg_in.inject({}) do |memo, p|
518
+ pkey_cols = keynames.map{|n| p.send(n)}
519
+ memo[pkey_cols] = [] if memo[pkey_cols].nil?
520
+ aggpairs.each_with_index do |ap, i|
521
+ agg = ap[0]
522
+ if ap[1].class == Symbol
523
+ colnum = ap[1].nil? ? nil : self.send(ap[1].to_s)[1]
524
+ else
525
+ colnum = ap[1].nil? ? nil : ap[1][1]
526
+ end
527
+ colval = colnum.nil? ? nil : p[colnum]
528
+ if memo[pkey_cols][i].nil?
529
+ memo[pkey_cols][i] = agg.send(:init, colval)
530
+ else
531
+ memo[pkey_cols][i] = agg.send(:trans, memo[pkey_cols][i], colval)
532
+ end
533
+ end
534
+ memo
535
+ end
536
+
537
+ result = tups.inject([]) do |memo, t|
538
+ finals = []
539
+ aggpairs.each_with_index do |ap, i|
540
+ finals << ap[0].send(:final, t[1][i])
541
+ end
542
+ memo << t[0] + finals
543
+ end
544
+ if block_given?
545
+ result.map{|r| yield r}
546
+ else
547
+ # merge directly into retval.storage, so that the temp tuples get picked up
548
+ # by the lhs of the rule
549
+ if aggcols.empty?
550
+ schema = keynames
551
+ else
552
+ schema = { keynames => aggcols }
553
+ end
554
+ retval = BudScratch.new('temp_group', bud_instance, schema)
555
+ retval.uniquify_tabname
556
+ retval.merge(result, retval.storage)
557
+ end
558
+ end
559
+
560
+ alias reduce inject
561
+
562
+ # methods that work on nested collections (resulting from joins)
563
+
564
+
565
+ # given a * expression over n collections, form all combinations of items
566
+ # subject to an array of predicates, pred
567
+ # currently supports two options for equijoin predicates:
568
+ # general form: an array of arrays capturing a conjunction of equiv. classes
569
+ # [[table1.col1, table2.col2, table3.col3], [table1.col2, table2.col3]]
570
+ # common form: a hash capturing equality of a column on left with one on right.
571
+ # :col1 => :col2 (same as lefttable.col1 => righttable.col2)
572
+ public
573
+ def pairs(*preds, &blk)
574
+ setup_preds(preds) unless (preds.nil? or preds.empty?)
575
+ # given new preds, the state for the join will be different. set it up again.
576
+ setup_state if self.class <= Bud::BudJoin
577
+ blk.nil? ? self : map(&blk)
578
+ end
579
+
580
+ alias combos pairs
581
+
582
+ # the natural join: given a * expression over 2 collections, form all
583
+ # combinations of items that have the same values in matching fiels
584
+ public
585
+ def matches(&blk)
586
+ preds = BudJoin::natural_preds(@bud_instance, @rels)
587
+ pairs(*preds, &blk)
588
+ end
589
+
590
+ # given a * expression over 2 collections, form all
591
+ # combinations of items that satisfy the predicates +preds+,
592
+ # and project only onto the attributes of the first collection
593
+ public
594
+ def lefts(*preds)
595
+ @localpreds = disambiguate_preds(preds)
596
+ map{ |l,r| l }
597
+ end
598
+
599
+ # given a * expression over 2 collections, form all
600
+ # combinations of items that satisfy the predicates +preds+,
601
+ # and project only onto the attributes of the second item
602
+ public
603
+ def rights(*preds)
604
+ @localpreds = disambiguate_preds(preds)
605
+ map{ |l,r| r }
606
+ end
607
+
608
+ # extract predicates on rellist[0] and recurse to right side with remainder
609
+ protected
610
+ def setup_preds(preds) # :nodoc: all
611
+ allpreds = disambiguate_preds(preds)
612
+ allpreds = canonicalize_localpreds(@rels, allpreds)
613
+ @localpreds = allpreds.reject { |p| p[0][0] != @rels[0].tabname }
614
+ otherpreds = allpreds - @localpreds
615
+ otherpreds = nil if otherpreds.empty?
616
+ unless otherpreds.nil?
617
+ unless @rels[1].class <= Bud::BudJoin
618
+ raise BudError, "join predicates don't match tables being joined: #{otherpreds.inspect}"
619
+ end
620
+ @rels[1].setup_preds(otherpreds)
621
+ end
622
+ end
623
+
624
+ protected
625
+ def disambiguate_preds(preds) # :nodoc: all
626
+ if preds.size == 1 and preds[0].class <= Hash
627
+ predarray = preds[0].map do |k,v|
628
+ if k.class != v.class
629
+ raise Bud::CompileError, "inconsistent attribute ref style #{k.inspect} => #{v.inspect}"
630
+ elsif k.class <= Array
631
+ [k,v]
632
+ elsif k.class <= Symbol
633
+ if @origrels and @origrels.length == 2
634
+ [find_attr_match(k,@origrels[0]), find_attr_match(v,@origrels[1])]
635
+ else
636
+ [find_attr_match(k), find_attr_match(v)]
637
+ end
638
+ else
639
+ raise Bud::CompileError, "invalid attribute ref in #{k.inspect} => #{v.inspect}"
640
+ end
641
+ end
642
+ return decomp_preds(*predarray)
643
+ else
644
+ return decomp_preds(*preds)
645
+ end
646
+ end
647
+
648
+ # find element in @origrels that contains this aname method
649
+ # if 2nd arg is non-nil, only check that collection.
650
+ # after found, return the result of invoking aname from chosen collection
651
+ protected
652
+ def find_attr_match(aname, rel=nil) # :nodoc: all
653
+ dorels = (rel.nil? ? @origrels : [rel])
654
+ match = nil
655
+ dorels.each do |r|
656
+ match ||= r if r.respond_to?(aname)
657
+ if r.respond_to?(aname) and match != r
658
+ raise Bud::CompileError, "ambiguous attribute :#{aname} in both #{match.tabname} and #{r.tabname}"
659
+ end
660
+ end
661
+ if match.nil?
662
+ raise Bud::CompileError, "attribute :#{aname} not found in any of #{dorels.map{|t| t.tabname}.inspect}"
663
+ end
664
+ match.send(aname)
665
+ end
666
+
667
+ protected
668
+ def decomp_preds(*preds) # :nodoc:all
669
+ # decompose each pred into a binary pred
670
+ return nil if preds.nil? or preds.empty? or preds == [nil]
671
+ newpreds = []
672
+ preds.each do |p|
673
+ p.each_with_index do |c, i|
674
+ newpreds << [p[i], p[i+1]] unless p[i+1].nil?
675
+ end
676
+ end
677
+ newpreds
678
+ end
679
+
680
+ protected
681
+ def canonicalize_localpreds(rel_list, preds) # :nodoc:all
682
+ return if preds.nil?
683
+ retval = preds.map do |p|
684
+ p[1][0] == rel_list[0].tabname ? p.reverse : p
685
+ end
686
+ end
687
+
688
+ public
689
+ def uniquify_tabname # :nodoc: all
690
+ # just append current number of microseconds
691
+ @tabname = (@tabname.to_s + Time.new.tv_usec.to_s).to_sym
692
+ end
693
+ end
694
+
695
+ class BudScratch < BudCollection # :nodoc: all
696
+ end
697
+
698
+ class BudTemp < BudCollection # :nodoc: all
699
+ end
700
+
701
+ class BudChannel < BudCollection
702
+ attr_reader :locspec_idx # :nodoc: all
703
+
704
+ def initialize(name, bud_instance, given_schema=nil) # :nodoc: all
705
+ given_schema ||= [:@address, :val]
706
+ the_schema, the_key_cols = parse_schema(given_schema)
707
+ the_val_cols = the_schema - the_key_cols
708
+ @locspec_idx = remove_at_sign!(the_key_cols)
709
+ @locspec_idx = remove_at_sign!(the_schema) if @locspec_idx.nil?
710
+ # If @locspec_idx is still nil, this is a loopback channel
711
+
712
+ # We mutate the hash key above, so we need to recreate the hash
713
+ # XXX: ugh, hacky
714
+ if given_schema.respond_to? :keys
715
+ given_schema = {the_key_cols => the_val_cols}
716
+ end
717
+
718
+ super(name, bud_instance, given_schema)
719
+ end
720
+
721
+ private
722
+ def remove_at_sign!(cols)
723
+ i = cols.find_index {|c| c.to_s[0].chr == '@'}
724
+ unless i.nil?
725
+ cols[i] = cols[i].to_s.delete('@').to_sym
726
+ end
727
+ return i
728
+ end
729
+
730
+ private
731
+ def split_locspec(l)
732
+ lsplit = l.split(':')
733
+ lsplit[1] = lsplit[1].to_i
734
+ return lsplit
735
+ end
736
+
737
+ public
738
+ def clone_empty
739
+ retval = super
740
+ retval.locspec_idx = @locspec_idx
741
+ retval
742
+ end
743
+
744
+ public
745
+ def tick # :nodoc: all
746
+ @sealed = false
747
+ @storage = {}
748
+ # Note that we do not clear @pending here: if the user inserted into the
749
+ # channel manually (e.g., via <~ from inside a sync_do block), we send the
750
+ # message at the end of the current tick.
751
+ end
752
+
753
+ public
754
+ def flush # :nodoc: all
755
+ ip = @bud_instance.ip
756
+ port = @bud_instance.port
757
+ each_from([@pending]) do |t|
758
+ if @locspec_idx.nil?
759
+ the_locspec = [ip, port]
760
+ else
761
+ the_locspec = split_locspec(t[@locspec_idx])
762
+ raise BudError, "'#{t[@locspec_idx]}', channel '#{@tabname}'" if the_locspec[0].nil? or the_locspec[1].nil? or the_locspec[0] == '' or the_locspec[1] == ''
763
+ end
764
+ @bud_instance.dsock.send_datagram([@tabname, t].to_msgpack, the_locspec[0], the_locspec[1])
765
+ end
766
+ @pending.clear
767
+ end
768
+
769
+ public
770
+ # project to the non-address fields
771
+ def payloads
772
+ if schema.size > 2
773
+ # bundle up each tuple's non-locspec fields into an array
774
+ retval = case @locspec_idx
775
+ when 0 then self.pro{|t| t[1..(t.size-1)]}
776
+ when (schema.size - 1) then self.pro{|t| t[0..(t.size-2)]}
777
+ else self.pro{|t| t[0..(@locspec_idx-1)] + t[@locspec_idx+1..(t.size-1)]}
778
+ end
779
+ else
780
+ # just return each tuple's non-locspec field value
781
+ retval = self.pro{|t| t[(@locspec_idx == 0) ? 1 : 0]}
782
+ end
783
+ return retval
784
+ end
785
+
786
+ superator "<~" do |o|
787
+ pending_merge o
788
+ end
789
+
790
+ superator "<+" do |o|
791
+ raise BudError, "Illegal use of <+ with channel '#{@tabname}' on left"
792
+ end
793
+
794
+ undef merge
795
+
796
+ def <=(o)
797
+ raise BudError, "Illegal use of <= with channel '#{@tabname}' on left"
798
+ end
799
+ end
800
+
801
+ class BudTerminal < BudCollection # :nodoc: all
802
+ def initialize(name, given_schema, bud_instance, prompt=false) # :nodoc: all
803
+ super(name, bud_instance, given_schema)
804
+ @prompt = prompt
805
+ end
806
+
807
+ public
808
+ def start_stdin_reader # :nodoc: all
809
+ # XXX: Ugly hack. Rather than sending terminal data to EM via UDP,
810
+ # we should add the terminal file descriptor to the EM event loop.
811
+ @reader = Thread.new do
812
+ begin
813
+ while true
814
+ $stdout.print("#{tabname} > ") if @prompt
815
+ s = $stdin.gets
816
+ break if s.nil? # Hit EOF
817
+ s = s.chomp if s
818
+ tup = [s]
819
+
820
+ ip = @bud_instance.ip
821
+ port = @bud_instance.port
822
+ EventMachine::schedule do
823
+ socket = EventMachine::open_datagram_socket("127.0.0.1", 0)
824
+ socket.send_datagram([tabname, tup].to_msgpack, ip, port)
825
+ end
826
+ end
827
+ rescue
828
+ puts "terminal reader thread failed: #{$!}"
829
+ print $!.backtrace.join("\n")
830
+ exit
831
+ end
832
+ end
833
+ end
834
+
835
+ public
836
+ def flush #:nodoc: all
837
+ @pending.each do |p|
838
+ $stdout.puts p[0]
839
+ $stdout.flush
840
+ end
841
+ @pending = {}
842
+ end
843
+
844
+ public
845
+ def tick #:nodoc: all
846
+ @storage = {}
847
+ raise BudError unless @pending.empty?
848
+ end
849
+
850
+ undef merge
851
+
852
+ public
853
+ def <=(o) #:nodoc: all
854
+ raise BudError, "Illegal use of <= with terminal '#{@tabname}' on left"
855
+ end
856
+
857
+ superator "<~" do |o|
858
+ pending_merge(o)
859
+ end
860
+ end
861
+
862
+ class BudPeriodic < BudCollection # :nodoc: all
863
+ end
864
+
865
+ class BudTable < BudCollection # :nodoc: all
866
+ def initialize(name, bud_instance, given_schema) # :nodoc: all
867
+ super(name, bud_instance, given_schema)
868
+ @to_delete = []
869
+ end
870
+
871
+ public
872
+ def tick #:nodoc: all
873
+ @to_delete.each do |tuple|
874
+ keycols = @key_colnums.map{|k| tuple[k]}
875
+ if @storage[keycols] == tuple
876
+ @storage.delete keycols
877
+ end
878
+ end
879
+ @storage.merge! @pending
880
+ @to_delete = []
881
+ @pending = {}
882
+ end
883
+
884
+ superator "<-" do |o|
885
+ o.each do |tuple|
886
+ next if tuple.nil?
887
+ tuple = prep_tuple(tuple)
888
+ @to_delete << tuple
889
+ end
890
+ end
891
+ end
892
+
893
+ class BudReadOnly < BudScratch # :nodoc: all
894
+ superator "<+" do |o|
895
+ raise CompileError, "Illegal use of <+ with read-only collection '#{@tabname}' on left"
896
+ end
897
+ public
898
+ def merge(o) #:nodoc: all
899
+ raise CompileError, "Illegal use of <= with read-only collection '#{@tabname}' on left"
900
+ end
901
+ end
902
+
903
+ class BudFileReader < BudReadOnly # :nodoc: all
904
+ def initialize(name, filename, delimiter, bud_instance) # :nodoc: all
905
+ super(name, bud_instance, {[:lineno] => [:text]})
906
+ @filename = filename
907
+ @storage = {}
908
+ # NEEDS A TRY/RESCUE BLOCK
909
+ @fd = File.open(@filename, "r")
910
+ @linenum = 0
911
+ end
912
+
913
+ public
914
+ def each(&block) # :nodoc: all
915
+ while (l = @fd.gets)
916
+ t = tuple_accessors([@linenum, l.strip])
917
+ @linenum += 1
918
+ yield t
919
+ end
920
+ end
921
+ end
922
+ end
923
+
924
+ module Enumerable
925
+ public
926
+ # monkeypatch to Enumerable to rename collections and their schemas
927
+ def rename(new_tabname, new_schema=nil)
928
+ budi = (respond_to?(:bud_instance)) ? bud_instance : nil
929
+ if new_schema.nil? and respond_to?(:schema)
930
+ new_schema = schema
931
+ end
932
+ scr = Bud::BudScratch.new(new_tabname.to_s, budi, new_schema)
933
+ scr.uniquify_tabname
934
+ scr.merge(self, scr.storage)
935
+ scr
936
+ end
937
+ end