piglet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +293 -0
  5. data/Rakefile +50 -0
  6. data/bin/piglet +9 -0
  7. data/examples/analysis.rb +311 -0
  8. data/examples/scratch.rb +11 -0
  9. data/examples/spike1.rb +43 -0
  10. data/examples/spike2.rb +40 -0
  11. data/examples/test1.rb +3 -0
  12. data/examples/test2.rb +5 -0
  13. data/examples/test3.rb +4 -0
  14. data/lib/piglet/assignment.rb +13 -0
  15. data/lib/piglet/cogroup.rb +31 -0
  16. data/lib/piglet/cross.rb +22 -0
  17. data/lib/piglet/describe.rb +5 -0
  18. data/lib/piglet/distinct.rb +16 -0
  19. data/lib/piglet/dump.rb +5 -0
  20. data/lib/piglet/explain.rb +13 -0
  21. data/lib/piglet/field.rb +40 -0
  22. data/lib/piglet/field_expression_functions.rb +62 -0
  23. data/lib/piglet/field_function_expression.rb +19 -0
  24. data/lib/piglet/field_infix_expression.rb +17 -0
  25. data/lib/piglet/field_prefix_expression.rb +21 -0
  26. data/lib/piglet/field_rename.rb +11 -0
  27. data/lib/piglet/field_suffix_expression.rb +17 -0
  28. data/lib/piglet/filter.rb +13 -0
  29. data/lib/piglet/foreach.rb +19 -0
  30. data/lib/piglet/group.rb +21 -0
  31. data/lib/piglet/illustrate.rb +5 -0
  32. data/lib/piglet/interpreter.rb +108 -0
  33. data/lib/piglet/join.rb +20 -0
  34. data/lib/piglet/limit.rb +13 -0
  35. data/lib/piglet/load.rb +31 -0
  36. data/lib/piglet/load_and_store.rb +16 -0
  37. data/lib/piglet/order.rb +29 -0
  38. data/lib/piglet/relation.rb +177 -0
  39. data/lib/piglet/sample.rb +13 -0
  40. data/lib/piglet/split.rb +41 -0
  41. data/lib/piglet/store.rb +17 -0
  42. data/lib/piglet/storing.rb +13 -0
  43. data/lib/piglet/stream.rb +5 -0
  44. data/lib/piglet/union.rb +19 -0
  45. data/lib/piglet.rb +45 -0
  46. data/spec/piglet/field_spec.rb +130 -0
  47. data/spec/piglet/interpreter_spec.rb +413 -0
  48. data/spec/piglet/relation_spec.rb +79 -0
  49. data/spec/piglet/split_spec.rb +34 -0
  50. data/spec/piglet_spec.rb +7 -0
  51. data/spec/spec.opts +3 -0
  52. data/spec/spec_helper.rb +14 -0
  53. metadata +123 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ doc
20
+ pkg
21
+
22
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Theo Hultberg
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,293 @@
1
+ = Piglet
2
+
3
+ Piglet is a DSL for writing Pig Latin scripts in Ruby:
4
+
5
+ a = load 'input'
6
+ b = a.group :c
7
+ store b, 'output'
8
+
9
+ The code above will be translated to the following Pig Latin:
10
+
11
+ relation_2 = LOAD 'input';
12
+ relation_1 = GROUP relation_2 BY c;
13
+ STORE relation_1 INTO 'output';
14
+
15
+ Piglet aims to look like Pig Latin while allowing for things like loops and control of flow that are missing from Pig. I started working on Piglet out of frustration that my Pig scripts started to be very repetitive. Pig lacks control of flow and mechanisms to apply the same set of operations on multiple relations. Piglet is my way of adding those features.
16
+
17
+ == Usage
18
+
19
+ It can be used either as a command line tool for translating a file of Piglet code into Pig Latin, or you can use it inline in a Ruby script:
20
+
21
+ === Command line usage
22
+
23
+ If <code>piggy.rb</code> contains
24
+
25
+ store(load('input')), 'output')
26
+
27
+ then running
28
+
29
+ piglet piggy.rb
30
+
31
+ will output
32
+
33
+ relation_1 = LOAD 'input';
34
+ STORE relation_1 INTO 'output';
35
+
36
+ === Programmatic interface
37
+
38
+ require 'piglet'
39
+
40
+ @piglet = Piglet::Interpreter.new
41
+ @piglet.interpret do
42
+ store(load('input'), 'output')
43
+ end
44
+ puts @piglet.to_pig_latin
45
+
46
+ or
47
+
48
+ Piglet::Interpreter.new { store(load('input'), 'output') }.to_pig_latin
49
+
50
+ will print
51
+
52
+ relation_1 = LOAD 'input';
53
+ STORE relation_1 INTO 'output';
54
+
55
+ to standard out.
56
+
57
+ == Examples of what it can do
58
+
59
+ a = load 'input', :schema => [:a, :b, :c]
60
+ b = a.group :c
61
+ c = b.foreach { |r| [r[0], r[1].a.max, r[1].b.max] }
62
+ store c, 'output'
63
+
64
+ will result in the following Pig Latin:
65
+
66
+ relation_3 = LOAD 'input' AS (a, b, c);
67
+ relation_2 = GROUP relation_3 BY c;
68
+ relation_1 = FOREACH relation_2 GENERATE $0, MAX($1.a), MAX($1.b);
69
+ STORE relation_1 INTO 'output';
70
+
71
+ == Syntax
72
+
73
+ There are two kinds of operators in Piglet: load & store operators, and relation operators. Load & store are called as functions with no receiver, like this:
74
+
75
+ load('input')
76
+ store(a, 'out')
77
+ describe(b)
78
+ illustrate(c)
79
+ dump(d)
80
+ explain(e)
81
+
82
+ and those are also all the load & store operators. They mirror the Pig Latin operators +LOAD+, +STORE+, +DESCRIBE+, +ILLUSTRATE+, +DUMP+ and +EXPLAIN+.
83
+
84
+ Relation operators are called as methods on relations. Relations are created by the +load+ operator, and can be stored in regular variables:
85
+
86
+ a = load('input', :schema => [:x, :y, :z])
87
+ b = a.group(:x)
88
+ store(b, 'out')
89
+
90
+ Unlinke Pig Latin, operators can be chained:
91
+
92
+ a = load('input', :schema => [:x, :y, :z])
93
+ b = a.sample(3).group(:x)
94
+ store(b, 'out')
95
+
96
+ In fact, a whole script can be written without using variables at all:
97
+
98
+ store(load('input', :schema => [:x, :y, :z]).sample(3).group(:x))
99
+
100
+ The relation operators are meant to be close to the Pig Latin syntax, but there are obvious limitations and tradeoffs, see the documentation for the Piglet::Relation mixin for syntax examples.
101
+
102
+ === <code>load</code>
103
+
104
+ When loading a relation you can specify the schema by passing the <code>:schema</code> option to +load+. The syntax of the schema specification is not perfect at this time: if you don't care about types you can pass an array of symbols or strings, like this:
105
+
106
+ load('input', :schema => %w(a b c d))
107
+ load('input', :schema => [:a, :b, :c, :d])
108
+
109
+ But if you want types, then you need to pass an array of arrays, where the inner arrays contain the field name and the field type:
110
+
111
+ load('input', :schema => [[:a, :chararray], [:b, :long]])
112
+
113
+ This is a bit inconvenient. I would like to use a hash, like this: <code>{:a => :chararray, :b => :long}</code>, but since the order of the keys isn't guaranteed in Ruby 1.8, it's not possible. I'm working on something better.
114
+
115
+ You can also specify a load function by passing the <code>:using</code> option:
116
+
117
+ load('input', :using => :pig_storage)
118
+ load('input', :using => 'MyOwnFunction')
119
+
120
+ Piglet knows to translate <code>:pig_storage</code> to <code>PigStorage</code>, as well as the other pre-defined load and store functions: <code>:binary_serializer</code>, <code>:binary_deserializer</code>, <code>:bin_storage</code>, <code>:pig_dump</code> and <code>:text_loader</code>.
121
+
122
+ == <code>store</code>, <code>dump</code>, <code>describe</code>, etc.
123
+
124
+ +store+ works similarily to +load+, but it takes a relation as its first argument, and the path to the output as second. It too takes the option <code>:using</code>, with the same values as +load+.
125
+
126
+ +dump+, +describe+, +illustrate+ and +explain+ all take a relation as sole argument. +explain+ can be called without argument (see the Pig Latin manual for what +EXPLAIN+ without argument does).
127
+
128
+ === Putting it all together
129
+
130
+ Let's look at a more complex example:
131
+
132
+ students = load('students.txt', :schema => [%w(student chararray), %w(age int), %w(grade int)])
133
+ top_acheivers = students.filter { |r| r.grade == 5 }
134
+ name_and_age = top_acheivers.foreach { |r| [r.student.as(:name), r.age] }
135
+ name_by_age = name_and_age.group(:age)
136
+ count_by_age = name_by_age.foreach { |r| [r[0].as(:age), r[1].name.count.as(:count)]}
137
+ store(count_by_age, 'student_counts_by_age.txt', :using => :pig_storage)
138
+
139
+ We load the file <code>students.txt</code> as a relation with three fields: <code>student</code>, a string, <code>age</code> an integer and <code>grade</code> another integer. Next we filter out the top acheivers with +filter+. +filter+ takes a block and that block gets a referece to the relation (the one +filter+ was called on), the result of the block will be the filter expression, in this case it's <code>grade == 5</code>.
140
+
141
+ When we have the top acheivers we want to make a projection to remove the grades field, since we will not use it in the next set of calculations. In Pig Latin this is done with <code>FOREACH … GENERATE</code>, which is just +foreach+ in Piglet. Like +filter+, +foreach+ takes a block that gets a reference to the relation. The result of the block should be an array of expressions, and in this case it's <code>[r.student.as(:name), r.age]</code>, which means the student field from the relation, renamed to "name" and the age field. The resulting relation will have two fields: "name" and "age".
142
+
143
+ On the next line we group the relation by the age field, and following that we do another projection, this time on the grouped relation. Remember that when doing a grouping in Pig you get a relation that in this case looks like this: <code>(group:int, name_by_age:{name:chararray, age:int})</code>. In the block passed to +foreach+ we use <code>r[0]</code> and <code>r[1]</code> to reference the first and second fields of <code>name_by_age</code>, equivalent to <code>$0</code> and <code>$1</code> in Pig Latin. In Pig Latin you could also have used the names <code>group</code> and <code>name_by_age</code>, but for a number of reasons you can't do that in Piglet (<code>r.group</code> unfortunately refers to the <code>group</code> method, and the relation isn't actually called <code>name_by_age</code> after Piglet has translated the code into Pig Latin).
144
+
145
+ The expression <code>r[1].name.count.as(:count)</code> means take the "name" field from the relation in the second field of the relation (<code>$1.name</code>), run the <code>COUNT</code> operator on it, and rename it <code>count</code>, i.e. <code>COUNT($1.name) AS count</code>.
146
+
147
+ Finally we store the result in a file called <code>student_counts_by_age.txt</code>, using PigStorage (which isn't strictly necessary to specify since it's the default. If you have a custom method you can pass its name as a string instead of <code>:pig_storage</code>).
148
+
149
+ Piglet will translate this into the following Pig Latin:
150
+
151
+ relation_5 = LOAD 'students.txt' AS (student:chararray, age:int, grade:int);
152
+ relation_4 = FILTER relation_5 BY grade == 5;
153
+ relation_3 = FOREACH relation_4 GENERATE student AS name, age;
154
+ relation_2 = GROUP relation_3 BY age;
155
+ relation_1 = FOREACH relation_2 GENERATE $0 AS age, COUNT($1.name) AS count;
156
+ STORE relation_1 INTO 'student_counts_by_age.txt' USING PigStorage;
157
+
158
+ === Going beyond Pig Latin
159
+
160
+ My goal with Piglet was to add control of flow and reuse mechanisms to Pig, so I'd better show some of the things you can do:
161
+
162
+ input = load('input', :schema => %w(country browser site visit_duration))
163
+ %w(country browser site).each do |dimension|
164
+ grouped = input.group(dimension).foreach do |r|
165
+ [r[0], r[1].visit_duration.sum]
166
+ end
167
+ store(grouped, "output-#{dimension}")
168
+ end
169
+
170
+ We load a file that contains an ID field, three dimensions (country, browser and site) and a metric: the duration of a visit. This could be data from a the logs of a set of websites, or an ad server. What we want to do is to sum the the <code>visit_duration</code> field for each of the three dimensions. This would be a big tedious in Pig Latin:
171
+
172
+ input = LOAD 'input' AS (country browser site visit_duration);
173
+ by_country = GROUP input BY country;
174
+ by_browser = GROUP input BY browser;
175
+ by_site = GROUP input BY site;
176
+ sum_by_country = FOREACH by_country GENERATE $0, SUM($1.visit_duration);
177
+ sum_by_browser = FOREACH by_browser GENERATE $0, SUM($1.visit_duration);
178
+ sum_by_site = FOREACH by_site GENERATE $0, SUM($1.visit_duration);
179
+ STORE sum_by_country INTO 'output-country;
180
+ STORE sum_by_browser INTO 'output-browser;
181
+ STORE sum_by_site INTO 'output-site;
182
+
183
+ But in Piglet it's as simple as looping over the names of the dimensions. You could even define a method that encapsulates the grouping, summing and storing (although in this case it would be a bit overkill):
184
+
185
+ def sum_dimension(relation, dimension)
186
+ grouped = relation.group(dimension).foreach do |r|
187
+ [r[0], r[1].visit_duration.sum]
188
+ end
189
+ store(grouped, "output-#{dimension}")
190
+ end
191
+
192
+ input = load('input', :schema => %w(country browser site visit_duration))
193
+ %w(country browser site).each do |dimension|
194
+ sum_dimension(input, dimension)
195
+ end
196
+
197
+ You can even define your own relation operations if you want, just add them to Piglet::Relation:
198
+
199
+ module Piglet::Relation
200
+ # Returns a list of sampled relations for each given sample size
201
+ def samples(*sizes)
202
+ sizes.map { |s| sample(s) }
203
+ end
204
+ end
205
+
206
+ and then use them just as any other operator:
207
+
208
+ small, medium, large = input.samples(0.01, 0.1, 0.5)
209
+
210
+ nifty, huh?
211
+
212
+ == Limitations
213
+
214
+ The aim is to support most of Pig Latin, but currently there are some limitations.
215
+
216
+ The following Pig operators are supported:
217
+
218
+ * +COGROUP+
219
+ * +CROSS+
220
+ * +DESCRIBE+
221
+ * +DISTINCT+
222
+ * +DUMP+
223
+ * +EXPLAIN+
224
+ * +FILTER+
225
+ * <code>FOREACH … GENERATE</code>
226
+ * +GROUP+
227
+ * +ILLUSTRATE+
228
+ * +JOIN+
229
+ * +LIMIT+
230
+ * +LOAD+
231
+ * +ORDER+
232
+ * +SAMPLE+
233
+ * +SPLIT+
234
+ * +STORE+
235
+ * +UNION+
236
+
237
+ The following are currently not supported (but will be soon):
238
+
239
+ * +STREAM+
240
+ * +DEFINE+
241
+ * +DECLARE+
242
+ * +REGISTER+
243
+
244
+ The file commands (+cd+, +cat+, etc.) will probably not be supported for the forseeable future.
245
+
246
+ All the aggregate functions are supported:
247
+
248
+ * +AVG+
249
+ * +CONCAT+
250
+ * +COUNT+
251
+ * +DIFF+
252
+ * +IsEmpty+
253
+ * +MAX+
254
+ * +MIN+
255
+ * +SIZE+
256
+ * +SUM+
257
+ * +TOKENIZE+
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>).
260
+
261
+ Piglet does not support:
262
+
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>)
264
+ * <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)
267
+
268
+ === Why aren't the aliases in the Pig Latin the same as the variable names in the Piglet script?
269
+
270
+ When you run +piglet+ on a Piglet script the aliases in the output will be <tt>relation_1</tt>, <tt>relation_2</tt>, <tt>relation_3</tt>, and so on, instead of the names of the variables of the Piglet script -- like in the example at the top of this document.
271
+
272
+ The names +a+ and +b+ are lost in translation, this is unfortunate but hard to avoid. Firstly there is no way to discover the names of variables, and secondly there is no correspondence between a statement in a Piglet script and a statement in Pig Latin, <code>a.union(b, c).sample(3).group(:x)</code> is at least three statements in Pig Latin. It simply wouldn't be worth the extra complexity of trying to infer some variable names and reuse them as aliases in the Pig Latin output.
273
+
274
+ In the future I may add a way of manually suggesting relation aliases, so that the Pig Latin output is more readable.
275
+
276
+ You may also wonder why the relation aliases aren't in consecutive order. The reason is that they get their names in the order they are evaluated, and the interpreter walks the relation ancestry upwards from a +store+ (and it only evaluates a relation once).
277
+
278
+ === Why aren’t all operations included in the output?
279
+
280
+ If you try this Piglet code:
281
+
282
+ a = load 'input'
283
+ b = a.group :c
284
+
285
+ You might be surprised that Piglet will not output anything. In fact, Piglet only creates Pig Latin operations on relations that will somehow be outputed. Unless there is a +store+, +dump+, +describe+, +illustrate+ or +explain+ that outputs a relation, the operations applied to that relation and its ancestors will not be included.
286
+
287
+ When you call +group+, +filter+ or any of the other methods that can be applied to a relation a datastructure that encodes these operations is created. When a relation is passed to +store+ or one of the other output operators the Piglet interpreter traverses the datastructure backwards, building the Pig Latin operations needed to arrive at the relation that should be passed to the output operator. This is similar to how Pig itself interprets a Pig Latin script.
288
+
289
+ As a side effect of using +store+ and the other output operators as the trigger for creating the needed relational operations any relations that are not ancestors of relations that are outputed will not be included in the Pig Latin output. On the other hand, they would be no-ops when run by Pig anyway.
290
+
291
+ == Copyright
292
+
293
+ © 2009-2010 Theo Hultberg / Iconara. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'lib/piglet'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "piglet"
9
+ gem.summary = %Q{Piglet is a DSL for Pig scripts}
10
+ gem.description = %Q{Piglet aims to look like Pig Latin while allowing for things like loops and control of flow that are missing from Pig.}
11
+ gem.email = "theo@iconara.net"
12
+ gem.homepage = "http://github.com/iconara/piglet"
13
+ gem.authors = ["Theo Hultberg"]
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ gem.version = Piglet::VERSION
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+
25
+ Spec::Rake::SpecTask.new(:spec) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.spec_files = FileList['spec/**/*_spec.rb']
28
+ end
29
+
30
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
31
+ spec.libs << 'lib' << 'spec'
32
+ spec.pattern = 'spec/**/*_spec.rb'
33
+ spec.rcov = true
34
+ end
35
+
36
+ task :spec => :check_dependencies
37
+
38
+ task :default => :spec
39
+
40
+ require 'rake/rdoctask'
41
+
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "piglet #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ rdoc.options << '--charset' << 'utf-8'
50
+ end
data/bin/piglet ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+
5
+
6
+ require 'piglet'
7
+
8
+
9
+ puts Piglet::Interpreter.new { ARGV.each { |path| eval(open(path).read) } }.to_pig_latin