piglet 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/README.rdoc +24 -4
  2. data/lib/piglet/field/binary_conditional.rb +15 -0
  3. data/lib/piglet/field/call_expression.rb +21 -0
  4. data/lib/piglet/field/infix_expression.rb +19 -0
  5. data/lib/piglet/field/literal.rb +20 -0
  6. data/lib/piglet/field/operators.rb +80 -0
  7. data/lib/piglet/field/prefix_expression.rb +23 -0
  8. data/lib/piglet/field/reference.rb +41 -0
  9. data/lib/piglet/field/rename.rb +13 -0
  10. data/lib/piglet/field/suffix_expression.rb +19 -0
  11. data/lib/piglet/inout/describe.rb +7 -0
  12. data/lib/piglet/inout/dump.rb +7 -0
  13. data/lib/piglet/inout/explain.rb +15 -0
  14. data/lib/piglet/inout/illustrate.rb +7 -0
  15. data/lib/piglet/inout/load.rb +31 -0
  16. data/lib/piglet/inout/output.rb +15 -0
  17. data/lib/piglet/inout/storage_types.rb +18 -0
  18. data/lib/piglet/inout/store.rb +19 -0
  19. data/lib/piglet/interpreter.rb +39 -7
  20. data/lib/piglet/relation/cogroup.rb +33 -0
  21. data/lib/piglet/relation/cross.rb +24 -0
  22. data/lib/piglet/relation/distinct.rb +18 -0
  23. data/lib/piglet/relation/filter.rb +15 -0
  24. data/lib/piglet/relation/foreach.rb +21 -0
  25. data/lib/piglet/relation/group.rb +23 -0
  26. data/lib/piglet/relation/join.rb +22 -0
  27. data/lib/piglet/relation/limit.rb +15 -0
  28. data/lib/piglet/relation/order.rb +31 -0
  29. data/lib/piglet/relation/relation.rb +179 -0
  30. data/lib/piglet/relation/sample.rb +15 -0
  31. data/lib/piglet/relation/split.rb +45 -0
  32. data/lib/piglet/relation/stream.rb +7 -0
  33. data/lib/piglet/relation/union.rb +21 -0
  34. data/lib/piglet.rb +40 -38
  35. data/spec/piglet/{field_spec.rb → field/reference_spec.rb} +22 -6
  36. data/spec/piglet/interpreter_spec.rb +51 -5
  37. data/spec/piglet/{relation_spec.rb → relation/relation_spec.rb} +6 -6
  38. data/spec/piglet/{split_spec.rb → relation/split_spec.rb} +8 -8
  39. data/spec/spec_helper.rb +0 -2
  40. metadata +39 -40
  41. data/examples/spike1.rb +0 -43
  42. data/examples/spike2.rb +0 -40
  43. data/lib/piglet/assignment.rb +0 -13
  44. data/lib/piglet/cogroup.rb +0 -31
  45. data/lib/piglet/cross.rb +0 -22
  46. data/lib/piglet/describe.rb +0 -5
  47. data/lib/piglet/distinct.rb +0 -16
  48. data/lib/piglet/dump.rb +0 -5
  49. data/lib/piglet/explain.rb +0 -13
  50. data/lib/piglet/field.rb +0 -40
  51. data/lib/piglet/field_expression_functions.rb +0 -62
  52. data/lib/piglet/field_function_expression.rb +0 -19
  53. data/lib/piglet/field_infix_expression.rb +0 -17
  54. data/lib/piglet/field_prefix_expression.rb +0 -21
  55. data/lib/piglet/field_rename.rb +0 -11
  56. data/lib/piglet/field_suffix_expression.rb +0 -17
  57. data/lib/piglet/filter.rb +0 -13
  58. data/lib/piglet/foreach.rb +0 -19
  59. data/lib/piglet/group.rb +0 -21
  60. data/lib/piglet/illustrate.rb +0 -5
  61. data/lib/piglet/join.rb +0 -20
  62. data/lib/piglet/limit.rb +0 -13
  63. data/lib/piglet/load.rb +0 -31
  64. data/lib/piglet/load_and_store.rb +0 -16
  65. data/lib/piglet/order.rb +0 -29
  66. data/lib/piglet/relation.rb +0 -177
  67. data/lib/piglet/sample.rb +0 -13
  68. data/lib/piglet/split.rb +0 -41
  69. data/lib/piglet/store.rb +0 -17
  70. data/lib/piglet/storing.rb +0 -13
  71. data/lib/piglet/stream.rb +0 -5
  72. data/lib/piglet/union.rb +0 -19
data/README.rdoc CHANGED
@@ -256,14 +256,34 @@ All the aggregate functions are supported:
256
256
  * +SUM+
257
257
  * +TOKENIZE+
258
258
 
259
- Piglet only supports a limited set of arithmetic, comparison and boolean operators, so in practice the support for +FILTER+ and <code>FOREACH GENERATE</code> is limited. This will be remedied soon, although the <code>!=</code> operator may never be supported in Ruby 1.8, since the <code>!</code> operator cannot be overridden (so there will be some less good looking solution like <code>.not</code>).
259
+ Piglet only supports most arithmetic and logic operators (see below) on fields -- but check the output and make sure that it's doing what you expect because some it's tricky to see where Piglet hijacks the operators and when it's Ruby that is running the show. I'm doing the best I can, but there are many things that can't be done, at least not in Ruby 1.8.
260
+
261
+ Piglet does support these field operators:
262
+
263
+ * <code>==</code> (equality)
264
+ * <code>&gt;</code> (greater than)
265
+ * <code>&lt;</code> (less than)
266
+ * <code>&gt=</code> (greater or equal to)
267
+ * <code>&lt;=</code> (less than or equal to)
268
+ * <code>%</code> (modulo)
269
+ * <code>+</code> (addition)
270
+ * <code>-</code> (subtraction)
271
+ * <code>*</code> (multiplication)
272
+ * <code>/</code> (division)
273
+
274
+ It also has these operators, see below for explanations:
275
+
276
+ * <code>#not</code> (logical negation)
277
+ * <code>#neg</code> (numerical negation)
278
+ * <code>#ne</code> (not equals)
279
+ * <code>#test</code> (binary conditionals)
260
280
 
261
281
  Piglet does not support:
262
282
 
263
- * <code>!=</code> (not equals, you have to use <code>==</code> and a <code>NOT</code>, e.g. <code>(a == b).not</code>, which will be translated as <code>NOT (a == b)</code> or you can use <code>#ne</code>, which will translate to !=, e.g. <code>a.ne(b)</code> will become <code>a != b</code>)
283
+ * <code>!=</code> (not equals, you have to use <code>==</code> and a <code>NOT</code>, e.g. <code>(a == b).not</code>, which will be translated as <code>NOT (a == b)</code> or you can use <code>#ne</code>, which will translate to !=, e.g. <code>a.ne(b)</code> will become <code>a != b</code>. May be supported in the future, but only in Ruby 1.9)
264
284
  * <code>? :</code> (the ternary operator)
265
- * <code>-</code> (negation, but you can use <code>#neg</code> on a field expression to get the same result, e.g. <code>a.neg</code> will be translated as <code>-a</code>)
266
- * <code>key#value</code> (map dereferencing)
285
+ * <code>-</code> (negation, but you can use <code>#neg</code> on a field expression to get the same result, e.g. <code>a.neg</code> will be translated as <code>-a</code>. May be supported in the future, but only in Ruby 1.9)
286
+ * <code>key#value</code> (map dereferencing, may be supported in the future)
267
287
 
268
288
  === Why aren't the aliases in the Pig Latin the same as the variable names in the Piglet script?
269
289
 
@@ -0,0 +1,15 @@
1
+ module Piglet
2
+ module Field
3
+ include Operators
4
+
5
+ class BinaryConditional
6
+ def initialize(test, if_true, if_false)
7
+ @test, @if_true, @if_false = test, if_true, if_false
8
+ end
9
+
10
+ def to_s
11
+ "(#{@test} ? #{@if_true} : #{@if_false})"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ module Piglet
2
+ module Field
3
+ class CallExpression # :nodoc:
4
+ include Operators
5
+
6
+ def initialize(name, inner_expression, options=nil)
7
+ options ||= {}
8
+ @name, @inner_expression = name, inner_expression
9
+ @new_name = options[:as]
10
+ end
11
+
12
+ def simple?
13
+ false
14
+ end
15
+
16
+ def to_s
17
+ "#{@name}(#{@inner_expression})"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module Piglet
2
+ module Field
3
+ class InfixExpression # :nodoc:
4
+ include Operators
5
+
6
+ def initialize(operator, left_expression, right_expression)
7
+ @operator, @left_expression, @right_expression = operator, left_expression, right_expression
8
+ end
9
+
10
+ def simple?
11
+ false
12
+ end
13
+
14
+ def to_s
15
+ "#{parenthesise(@left_expression)} #{@operator} #{parenthesise(@right_expression)}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ module Piglet
2
+ module Field
3
+ class Literal
4
+ include Operators
5
+
6
+ def initialize(obj)
7
+ @obj = obj
8
+ end
9
+
10
+ def to_s
11
+ case @obj
12
+ when Numeric
13
+ @obj.to_s
14
+ else
15
+ "'#{escape(@obj.to_s)}'"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,80 @@
1
+ module Piglet
2
+ module Field
3
+ module Operators # :nodoc:
4
+ SYMBOLIC_OPERATORS = [:==, :>, :<, :>=, :<=, :%, :+, :-, :*, :/]
5
+ FUNCTIONS = [:avg, :count, :diff, :max, :min, :size, :sum, :tokenize]
6
+
7
+ FUNCTIONS.each do |fun|
8
+ define_method(fun) do
9
+ CallExpression.new(fun.to_s.upcase, self)
10
+ end
11
+ end
12
+
13
+ def empty?
14
+ CallExpression.new('IsEmpty', self)
15
+ end
16
+
17
+ def as(new_name)
18
+ Rename.new(new_name, self)
19
+ end
20
+
21
+ def not
22
+ PrefixExpression.new('NOT', self)
23
+ end
24
+
25
+ def null?
26
+ SuffixExpression.new('is null', self)
27
+ end
28
+
29
+ def not_null?
30
+ SuffixExpression.new('is not null', self)
31
+ end
32
+
33
+ def cast(type)
34
+ PrefixExpression.new("(#{type.to_s})", self)
35
+ end
36
+
37
+ def matches(pattern)
38
+ regex_options_pattern = /^\(\?.+?:(.*)\)$/
39
+ pattern = pattern.to_s.sub(regex_options_pattern, '\1') if pattern.is_a?(Regexp) && pattern.to_s =~ regex_options_pattern
40
+ InfixExpression.new('matches', self, "'#{pattern.to_s}'")
41
+ end
42
+
43
+ def neg
44
+ PrefixExpression.new('-', self, false)
45
+ end
46
+
47
+ def ne(other)
48
+ InfixExpression.new('!=', self, other)
49
+ end
50
+
51
+ def and(other)
52
+ InfixExpression.new('AND', self, other)
53
+ end
54
+
55
+ def or(other)
56
+ InfixExpression.new('OR', self, other)
57
+ end
58
+
59
+ SYMBOLIC_OPERATORS.each do |op|
60
+ define_method(op) do |other|
61
+ InfixExpression.new(op.to_s, self, other)
62
+ end
63
+ end
64
+
65
+ protected
66
+
67
+ def parenthesise(expr)
68
+ if expr.respond_to?(:simple?) && ! expr.simple?
69
+ "(#{expr})"
70
+ else
71
+ expr.to_s
72
+ end
73
+ end
74
+
75
+ def escape(str)
76
+ str.gsub(/("|'|\\)/) { |m| "\\#{$1}" }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,23 @@
1
+ module Piglet
2
+ module Field
3
+ class PrefixExpression # :nodoc:
4
+ include Operators
5
+
6
+ def initialize(operator, expression, space_between=true)
7
+ @operator, @expression, @space_between = operator, expression, space_between
8
+ end
9
+
10
+ def simple?
11
+ true
12
+ end
13
+
14
+ def to_s
15
+ if @space_between
16
+ "#{@operator} #{parenthesise(@expression)}"
17
+ else
18
+ "#{@operator}#{parenthesise(@expression)}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,41 @@
1
+ module Piglet
2
+ module Field
3
+ class Reference # :nodoc:
4
+ include Operators
5
+
6
+ def initialize(name, relation=nil, options=nil)
7
+ options ||= {}
8
+ @name, @parent = name, relation
9
+ @explicit_ancestry = options[:explicit_ancestry] || false
10
+ end
11
+
12
+ def simple?
13
+ true
14
+ end
15
+
16
+ def method_missing(name, *args)
17
+ if name.to_s =~ /^\w+$/ && args.empty?
18
+ Reference.new(name, self, :explicit_ancestry => true)
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ def [](n)
25
+ Reference.new("\$#{n}", self, :explicit_ancestry => true)
26
+ end
27
+
28
+ def to_s
29
+ if @explicit_ancestry
30
+ if @parent.respond_to?(:alias)
31
+ "#{@parent.alias}.#{@name.to_s}"
32
+ else
33
+ "#{@parent}.#{@name.to_s}"
34
+ end
35
+ else
36
+ @name.to_s
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ module Piglet
2
+ module Field
3
+ class Rename # :nodoc:
4
+ def initialize(new_name, field_expression)
5
+ @new_name, @field_expression = new_name, field_expression
6
+ end
7
+
8
+ def to_s
9
+ "#{@field_expression} AS #{@new_name}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Piglet
2
+ module Field
3
+ class SuffixExpression # :nodoc:
4
+ include Operators
5
+
6
+ def initialize(operator, expression)
7
+ @operator, @expression = operator, expression
8
+ end
9
+
10
+ def simple?
11
+ false
12
+ end
13
+
14
+ def to_s
15
+ "#{parenthesise(@expression)} #{@operator}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ module Piglet
2
+ module Inout
3
+ class Describe # :nodoc:
4
+ include Output
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Piglet
2
+ module Inout
3
+ class Dump # :nodoc:
4
+ include Output
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module Piglet
2
+ module Inout
3
+ class Explain # :nodoc:
4
+ include Output
5
+
6
+ def to_s
7
+ if relation.nil?
8
+ "EXPLAIN"
9
+ else
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ module Piglet
2
+ module Inout
3
+ class Illustrate # :nodoc:
4
+ include Output
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ module Piglet
2
+ module Inout
3
+ class Load # :nodoc:
4
+ include StorageTypes
5
+
6
+ def initialize(path, options={})
7
+ options ||= {}
8
+ @path, @using, @schema = path, options[:using], options[:schema]
9
+ end
10
+
11
+ def to_s
12
+ str = "LOAD '#{@path}'"
13
+ str << " USING #{resolve_load_store_function(@using)}" if @using
14
+ str << " AS (#{schema_string})" if @schema
15
+ str
16
+ end
17
+
18
+ private
19
+
20
+ def schema_string
21
+ @schema.map do |field|
22
+ if field.is_a?(Enumerable)
23
+ field.map { |f| f.to_s }.join(':')
24
+ else
25
+ field.to_s
26
+ end
27
+ end.join(', ')
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ module Piglet
2
+ module Inout
3
+ module Output # :nodoc:
4
+ attr_reader :relation
5
+
6
+ def initialize(relation)
7
+ @relation = relation
8
+ end
9
+
10
+ def to_s
11
+ "#{self.class.name.split(/::/).last.upcase} #{@relation.alias}"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ module Piglet
2
+ module Inout
3
+ module StorageTypes # :nodoc:
4
+ LOAD_STORE_FUNCTIONS = {
5
+ :binary_serializer => 'BinarySerializer',
6
+ :binary_deserializer => 'BinaryDeserializer',
7
+ :bin_storage => 'BinStorage',
8
+ :pig_storage => 'PigStorage',
9
+ :pig_dump => 'PigDump',
10
+ :text_loader => 'TextLoader'
11
+ }
12
+
13
+ def resolve_load_store_function(name)
14
+ LOAD_STORE_FUNCTIONS[name] || name.to_s
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module Piglet
2
+ module Inout
3
+ class Store # :nodoc:
4
+ include StorageTypes
5
+ include Output
6
+
7
+ def initialize(relation, path, options={})
8
+ @relation, @path, @using = relation, path, options[:using]
9
+ end
10
+
11
+ def to_s
12
+ str = super
13
+ str << " INTO '#{@path}'"
14
+ str << " USING #{resolve_load_store_function(@using)}" if @using
15
+ str
16
+ end
17
+ end
18
+ end
19
+ end
@@ -52,7 +52,9 @@ module Piglet
52
52
  # NOTE: the syntax load('path', :schema => {:a => :chararray, :b => :int})
53
53
  # would be nice, but the order of the keys can't be guaranteed in Ruby 1.8.
54
54
  def load(path, options={})
55
- Load.new(path, options)
55
+ load = Inout::Load.new(path, options)
56
+ load.extend Piglet::Relation::Relation
57
+ load
56
58
  end
57
59
 
58
60
  # STORE
@@ -61,28 +63,28 @@ module Piglet
61
63
  # store(x, 'some/path', :using => 'Xyz') # => STORE x INTO 'some/path' USING Xyz
62
64
  # store(x, 'some/path', :using => :pig_storage) # => STORE x INTO 'some/path' USING PigStorage
63
65
  def store(relation, path, options={})
64
- @stores << Store.new(relation, path, options)
66
+ @stores << Inout::Store.new(relation, path, options)
65
67
  end
66
68
 
67
69
  # DUMP
68
70
  #
69
71
  # dump(x) # => DUMP x
70
72
  def dump(relation)
71
- @stores << Dump.new(relation)
73
+ @stores << Inout::Dump.new(relation)
72
74
  end
73
75
 
74
76
  # ILLUSTRATE
75
77
  #
76
78
  # illustrate(x) # => ILLUSTRATE x
77
79
  def illustrate(relation)
78
- @stores << Illustrate.new(relation)
80
+ @stores << Inout::Illustrate.new(relation)
79
81
  end
80
82
 
81
83
  # DESCRIBE
82
84
  #
83
85
  # describe(x) # => DESCRIBE x
84
86
  def describe(relation)
85
- @stores << Describe.new(relation)
87
+ @stores << Inout::Describe.new(relation)
86
88
  end
87
89
 
88
90
  # EXPLAIN
@@ -90,7 +92,23 @@ module Piglet
90
92
  # explain # => EXPLAIN
91
93
  # explain(x) # => EXPLAIN(x)
92
94
  def explain(relation=nil)
93
- @stores << Explain.new(relation)
95
+ @stores << Inout::Explain.new(relation)
96
+ end
97
+
98
+ # Support for binary conditions, a.k.a. the ternary operator.
99
+ #
100
+ # x.test(x.a > x.b, x.a, x.b) # => (a > b ? a : b)
101
+ #
102
+ # Should only be used in the block given to #filter and #foreach
103
+ def test(test, if_true, if_false)
104
+ Field::BinaryConditional.new(test, if_true, if_false)
105
+ end
106
+
107
+ # Support for literals in FOREACH … GENERATE blocks.
108
+ #
109
+ # x.foreach { |r| [literal("hello").as(:hello)] } # => FOREACH x GENERATE 'hello' AS hello
110
+ def literal(obj)
111
+ Field::Literal.new(obj)
94
112
  end
95
113
 
96
114
  private
@@ -104,5 +122,19 @@ module Piglet
104
122
  [assignment]
105
123
  end
106
124
  end
107
- end
125
+ end
126
+
127
+ private
128
+
129
+ class Assignment # :nodoc:
130
+ attr_reader :target
131
+
132
+ def initialize(relation)
133
+ @target = relation
134
+ end
135
+
136
+ def to_s
137
+ "#{@target.alias} = #{@target.to_s}"
138
+ end
139
+ end
108
140
  end
@@ -0,0 +1,33 @@
1
+ module Piglet
2
+ module Relation
3
+ class Cogroup # :nodoc:
4
+ include Relation
5
+
6
+ def initialize(relation, description)
7
+ @join_fields = description.reject { |k, v| ! (k.is_a?(Relation)) }
8
+ @sources = @join_fields.keys
9
+ @parallel = description[:parallel]
10
+ end
11
+
12
+ def to_s
13
+ joins = @sources.map do |s|
14
+ fields = @join_fields[s]
15
+ if fields.is_a?(Enumerable) && fields.size > 1 && (fields.last == :inner || fields.last == :outer)
16
+ inout = fields.last.to_s.upcase
17
+ fields = fields[0..-2]
18
+ end
19
+ if fields.is_a?(Enumerable) && fields.size > 1
20
+ str = "#{s.alias} BY (#{fields.join(', ')})"
21
+ else
22
+ str = "#{s.alias} BY #{fields}"
23
+ end
24
+ str << " #{inout}" if inout
25
+ str
26
+ end
27
+ str = "COGROUP #{joins.join(', ')}"
28
+ str << " PARALLEL #{@parallel}" if @parallel
29
+ str
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ module Piglet
2
+ module Relation
3
+ class Cross # :nodoc:
4
+ include Relation
5
+
6
+ def initialize(relations, options={})
7
+ options ||= {}
8
+ @sources, @parallel = relations, options[:parallel]
9
+ end
10
+
11
+ def to_s
12
+ str = "CROSS #{source_aliases.join(', ')}"
13
+ str << " PARALLEL #{@parallel}" if @parallel
14
+ str
15
+ end
16
+
17
+ private
18
+
19
+ def source_aliases
20
+ @sources.map { |s| s.alias }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ module Piglet
2
+ module Relation
3
+ class Distinct # :nodoc:
4
+ include Relation
5
+
6
+ def initialize(relation, options={})
7
+ options ||= {}
8
+ @sources, @parallel = [relation], options[:parallel]
9
+ end
10
+
11
+ def to_s
12
+ str = "DISTINCT #{@sources.first.alias}"
13
+ str << " PARALLEL #{@parallel}" if @parallel
14
+ str
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module Piglet
2
+ module Relation
3
+ class Filter # :nodoc:
4
+ include Relation
5
+
6
+ def initialize(relation, expression)
7
+ @sources, @expression = [relation], expression
8
+ end
9
+
10
+ def to_s
11
+ "FILTER #{@sources.first.alias} BY #{@expression}"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ module Piglet
2
+ module Relation
3
+ class Foreach # :nodoc:
4
+ include Relation
5
+
6
+ def initialize(relation, field_expressions)
7
+ @sources, @field_expressions = [relation], [field_expressions].flatten
8
+ end
9
+
10
+ def to_s
11
+ "FOREACH #{@sources.first.alias} GENERATE #{field_expressions_string}"
12
+ end
13
+
14
+ private
15
+
16
+ def field_expressions_string
17
+ @field_expressions.map { |fe| fe.to_s }.join(', ')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Piglet
2
+ module Relation
3
+ class Group # :nodoc:
4
+ include Relation
5
+
6
+ def initialize(relation, grouping, options={})
7
+ options ||= {}
8
+ @sources, @grouping, @parallel = [relation], grouping, options[:parallel]
9
+ end
10
+
11
+ def to_s
12
+ str = "GROUP #{@sources.first.alias} BY "
13
+ if @grouping.size > 1
14
+ str << "(#{@grouping.join(', ')})"
15
+ else
16
+ str << @grouping.first.to_s
17
+ end
18
+ str << " PARALLEL #{@parallel}" if @parallel
19
+ str
20
+ end
21
+ end
22
+ end
23
+ end