gooddata 0.6.0.pre9 → 0.6.0.pre10
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.rspec +3 -0
- data/.yardopts +19 -0
- data/Gemfile +5 -0
- data/README.md +191 -0
- data/Rakefile +18 -1
- data/bin/gooddata +74 -63
- data/doc/.gitignore +1 -0
- data/doc/css/.gitkeepme +1 -0
- data/doc/images/.gitkeepme +1 -0
- data/doc/images/background.png +0 -0
- data/doc/images/bg-callout-button.png +0 -0
- data/doc/images/header-logo.png +0 -0
- data/doc/images/logo-image.png +0 -0
- data/doc/js/.gitkeepme +1 -0
- data/doc/pages/GET_STARTED.md +309 -0
- data/doc/pages/HOMEPAGE.md +75 -0
- data/doc/pages/TUTORIALS.md +52 -0
- data/doc/pages/tutorial/BRICKS.md +257 -0
- data/doc/pages/tutorial/CREATING_A_MODEL.md +79 -0
- data/doc/pages/tutorial/CRUNCHING_NUMBERS.md +233 -0
- data/doc/pages/tutorial/TEST_DRIVEN_DEVELOPMENT.md +118 -0
- data/doc/pages/tutorial/YOUR_FIRST_PROJECT.md +52 -0
- data/doc/templates/default/class/dot/setup.rb +6 -0
- data/doc/templates/default/class/dot/superklass.erb +3 -0
- data/doc/templates/default/class/setup.rb +36 -0
- data/doc/templates/default/class/text/setup.rb +11 -0
- data/doc/templates/default/class/text/subclasses.erb +5 -0
- data/doc/templates/default/constant/text/header.erb +11 -0
- data/doc/templates/default/constant/text/setup.rb +3 -0
- data/doc/templates/default/docstring/setup.rb +51 -0
- data/doc/templates/default/docstring/text/abstract.erb +2 -0
- data/doc/templates/default/docstring/text/deprecated.erb +2 -0
- data/doc/templates/default/docstring/text/index.erb +2 -0
- data/doc/templates/default/docstring/text/note.erb +4 -0
- data/doc/templates/default/docstring/text/private.erb +2 -0
- data/doc/templates/default/docstring/text/returns_void.erb +1 -0
- data/doc/templates/default/docstring/text/text.erb +1 -0
- data/doc/templates/default/docstring/text/todo.erb +4 -0
- data/doc/templates/default/layout/dot/header.erb +6 -0
- data/doc/templates/default/layout/dot/setup.rb +14 -0
- data/doc/templates/default/method/setup.rb +3 -0
- data/doc/templates/default/method/text/header.erb +1 -0
- data/doc/templates/default/method_details/setup.rb +10 -0
- data/doc/templates/default/method_details/text/header.erb +10 -0
- data/doc/templates/default/method_details/text/method_signature.erb +12 -0
- data/doc/templates/default/method_details/text/setup.rb +10 -0
- data/doc/templates/default/module/dot/child.erb +1 -0
- data/doc/templates/default/module/dot/dependencies.erb +3 -0
- data/doc/templates/default/module/dot/header.erb +6 -0
- data/doc/templates/default/module/dot/info.erb +14 -0
- data/doc/templates/default/module/dot/setup.rb +14 -0
- data/doc/templates/default/module/setup.rb +164 -0
- data/doc/templates/default/module/text/children.erb +10 -0
- data/doc/templates/default/module/text/class_meths_list.erb +8 -0
- data/doc/templates/default/module/text/extends.erb +8 -0
- data/doc/templates/default/module/text/header.erb +7 -0
- data/doc/templates/default/module/text/includes.erb +8 -0
- data/doc/templates/default/module/text/instance_meths_list.erb +8 -0
- data/doc/templates/default/module/text/setup.rb +12 -0
- data/doc/templates/default/root/dot/child.erb +3 -0
- data/doc/templates/default/root/dot/setup.rb +5 -0
- data/doc/templates/default/tags/setup.rb +55 -0
- data/doc/templates/default/tags/text/example.erb +12 -0
- data/doc/templates/default/tags/text/index.erb +1 -0
- data/doc/templates/default/tags/text/option.erb +20 -0
- data/doc/templates/default/tags/text/overload.erb +19 -0
- data/doc/templates/default/tags/text/see.erb +11 -0
- data/doc/templates/default/tags/text/tag.erb +13 -0
- data/examples.rb +2 -2
- data/gooddata.gemspec +31 -26
- data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +1 -1
- data/lib/gooddata/client.rb +65 -53
- data/lib/gooddata/commands/commands.rb +9 -0
- data/lib/gooddata/commands/process.rb +9 -8
- data/lib/gooddata/commands/projects.rb +29 -0
- data/lib/gooddata/commands/runners.rb +1 -1
- data/lib/gooddata/connection.rb +6 -4
- data/lib/gooddata/exceptions.rb +2 -1
- data/lib/gooddata/helpers.rb +1 -1
- data/lib/gooddata/model.rb +360 -189
- data/lib/gooddata/models/metadata.rb +1 -1
- data/lib/gooddata/models/metric.rb +2 -1
- data/lib/gooddata/models/project.rb +1 -1
- data/lib/gooddata/models/report.rb +0 -18
- data/lib/gooddata/version.rb +1 -1
- data/spec/blueprint_spec.rb +83 -43
- data/spec/data/additional_dataset_module.json +18 -0
- data/spec/data/blueprint_invalid.json +36 -0
- data/spec/data/blueprint_valid.json +37 -0
- data/spec/data/model_module.json +18 -0
- data/spec/{test_project_model_spec.json → data/test_project_model_spec.json} +4 -0
- data/spec/full_project_spec.rb +4 -3
- data/spec/helpers/blueprint_helper.rb +17 -0
- data/spec/merging_blueprints_spec.rb +23 -48
- data/spec/model_dsl_spec.rb +2 -2
- data/spec/model_spec.rb +44 -0
- data/spec/project_build_and_update_spec.rb +28 -0
- data/spec/spec_helper.rb +6 -0
- data/yard-server.sh +3 -0
- metadata +251 -74
- data/README.rdoc +0 -176
@@ -0,0 +1,257 @@
|
|
1
|
+
You can run Ruby on GoodData platform. Let's have a look at the platform and walk step by step through doing the simplest possible deployment and then move to more advanced tasks.
|
2
|
+
|
3
|
+
## Not reinventing the wheel
|
4
|
+
|
5
|
+
The main idea is that only minority of people should be forced to write code. The others should be happy running them without understanding the details. Soon we will introduce better UI to do just that. Until then it is more programatic but if you are not scared read on.
|
6
|
+
|
7
|
+
## Setting up the stage
|
8
|
+
|
9
|
+
GoodData Ruby SDK stack is built so you can easily develop things locally and deploy them when you have tested them and are happy with how they work. You need to set up an environment first.
|
10
|
+
|
11
|
+
### Prerequisites
|
12
|
+
* Git
|
13
|
+
* Ruby (JRuby recommended)
|
14
|
+
* Ruby Gems
|
15
|
+
|
16
|
+
Go ahead and run
|
17
|
+
|
18
|
+
git clone https://github.com/gooddata/app_store.git
|
19
|
+
|
20
|
+
We just cloned the remote repository which contains information about the used libraries and also contains some stuff that others created. We will investigate that later. Let's continue with setting things up.
|
21
|
+
|
22
|
+
Run
|
23
|
+
|
24
|
+
gem install bundler
|
25
|
+
|
26
|
+
This will install bundler which is a useful package installation tool. Let's use it
|
27
|
+
|
28
|
+
cd local_repo
|
29
|
+
bundle install --binstubs
|
30
|
+
|
31
|
+
This will ensure that you have installed exactly what we have on the production machines. This should mitigate bugs caused by slightly different versions of libraries and incompatible APIs.
|
32
|
+
|
33
|
+
You are ready to go.
|
34
|
+
|
35
|
+
## Running your first brick
|
36
|
+
|
37
|
+
The small pieces of ruby that are run on the platform are called bricks. Nobody knows where and why this name emerged but there are rumors that it is supposed to reference the fact that out of brick just laid together you can create a solid wall.
|
38
|
+
|
39
|
+
If you open the repository you will see there some directories. Find directory called misc/hello_world and open it. It contains only one file. Do not open it yet.
|
40
|
+
|
41
|
+
Run this in console.
|
42
|
+
|
43
|
+
bundle exec gooddata -l -Uname@gooddata.com -Pmy_pass run_ruby -p project_pid -d hello_world_brick --name "some_deploy" --remote
|
44
|
+
|
45
|
+
It will take some time but after a while you should see green DONE on your console and a link for the log. Open it in your browser and you should see there something like this. On one of the lines there should be hello world. Great you just ran your first ruby brick.
|
46
|
+
|
47
|
+
###Looking inside Hello World
|
48
|
+
|
49
|
+
Let's see what is happening inside. Open the main.rb file in your favorite editor. You should see something like this.
|
50
|
+
|
51
|
+
require 'gooddata'
|
52
|
+
require 'logger'
|
53
|
+
|
54
|
+
module GoodData::Bricks
|
55
|
+
class HelloWorldBrick
|
56
|
+
|
57
|
+
def call(params)
|
58
|
+
logger = Logger.new(params[:GDC_LOGGER_FILE])
|
59
|
+
logger.info "Hello world"
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
GoodData::Bricks::HelloWorldBrick.new
|
66
|
+
|
67
|
+
This is all. Let's dissect it. The interface is very simple. You have to provide an instance of an object that responds to a message :call (in other words does implement method call). This method takes one parameter and that is a hash map of parameters.
|
68
|
+
|
69
|
+
You can see that we have implemented such a class and returning an instance of that class. The method accepts params and you can see that we immediately make use of them when grabbing logger and the writing to it. Platform does the heavy lifting on the back and you have already seen the result.
|
70
|
+
|
71
|
+
## Digging deeper
|
72
|
+
|
73
|
+
It might be surprising if I tell you that this is not exactly how majority of the real bricks are implemented. What we showed you is fine and this is how we started but after we implemented some bricks we found out that we are repeating ourselves a lot. So we tried to come up with something better.
|
74
|
+
|
75
|
+
remark: If you know how Rack or any similar framework works for abstracting web applications you would be right at home since that is where most of the inspiration came from.
|
76
|
+
|
77
|
+
We introduced three concepts.
|
78
|
+
|
79
|
+
* _Application_ - This part is responsible for doing the core of the task you are interested in. Structure of an app is pretty much what you have already seen.
|
80
|
+
* _Middleware_ - very similar to app. The main difference is that you can chain them together. The main similarity is that it has the same interface as an app
|
81
|
+
* _Pipelines_ - If you chain multiple middleweres and applications it creates a pipeline.
|
82
|
+
|
83
|
+
Let's have a look how it works visually.
|
84
|
+
|
85
|
+
![Example pipeline consisting of 3 middlewares](https://dl.dropboxusercontent.com/s/g5rymdmmx97hc61/middlewares.png?token_hash=AAE7qAjkOxA6tQGDk8UY17ltRu0ZG5UqwSJ_8ZtAl7ZNaA)
|
86
|
+
|
87
|
+
This is a simple pipeline with 2 middlewares and one app. The arrows depict how the execution order would flow.
|
88
|
+
|
89
|
+
### Executing a pipeline
|
90
|
+
Your pipeline is executed. First middleware is called then the second and third. Then your app is called it does what it needs to and then the call goes back through the middlewares (so they can actually act twice).
|
91
|
+
|
92
|
+
This probably does not seem that much useful so let's have a look at couple of examples where you might use it.
|
93
|
+
|
94
|
+
Plumbing - just the plumbing. Did you notice how we had to set up the logger in our Hello World example? It is not a lot of code but imagine that you need to do 10 things like this. It can bog you down. There are couple of middlewares that try to help you with similar stuff. It is similar to what AOP style of programming tries to do.
|
95
|
+
|
96
|
+
Examples
|
97
|
+
|
98
|
+
* log in to various systems and prepeare for action
|
99
|
+
* Set up loggers
|
100
|
+
|
101
|
+
Decorators - Imagine that you have done something great. For example computing hierarchy of people from some information. It is so much useful that you would like to let other people use it. But everybody has slightly different use case. Somebody wants to output it to web dav or s3 storage. Somebody want's to tweet about that it finished somebody might want to store this file into vertica. Implementing serialization in a separate middlewere means that you do not need to touch the actually code that.
|
102
|
+
|
103
|
+
* measuring time
|
104
|
+
* serializing stuff to various places
|
105
|
+
* letting other people know
|
106
|
+
|
107
|
+
### First pipeline
|
108
|
+
|
109
|
+
Ok let's create our first pipeline. Let's open misc/hello_world_pipeline_brick in your browser
|
110
|
+
|
111
|
+
You will see two files. Let's check the hello_world.rb first.
|
112
|
+
|
113
|
+
module MyFirstBrick
|
114
|
+
|
115
|
+
class HelloWorldBrick
|
116
|
+
def call(params)
|
117
|
+
logger = params[:gdc_logger]
|
118
|
+
logger.info "Hello world"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
It looks exactly the same as in previosus case except for the logger setting up. Let's look at the other file main.rb
|
125
|
+
|
126
|
+
You see that at the top we are requiring the hello_world.rb and then we are setting up the pipeline. You can see that even this basic example uses quite a bit of middleare but hopefully their names are fairly self describing. Logging will hook up a logger. GoodData logs you in to GD and hooks the library to the logger if you want to. Timinng will simply measure the execution of the app itself.
|
127
|
+
|
128
|
+
Notice several things. Pipeline has exactly one app. It also has to be the last one in the pipeline. It has zero or more middlewares. When constructong a pipeline you can specify a stage either via a class or an instance. If it is a class we instatntiate it for you (without parameters). This is useful if you want to parametrize the middleware somehow.
|
129
|
+
|
130
|
+
## Middlewares
|
131
|
+
Let's have a look at some middlewares
|
132
|
+
App was not very challenging from programming perspective (which is the goal) and you will see that middleware is not any more complicated. There are generally two types of middleares. First is a middleware that wants to act only once. It is like fire and forget. For example setting up logger. You just create it and do not care. There is a second type and that is a middleware that wants to act twice. Once before the app itself was called. Second after an app was called. Typical example might be time measuring. You want to start your clock before the app itself is called but then you have to stop them sometimes. The difference is absolutely minimal let's walk through both of them.
|
133
|
+
|
134
|
+
### Logger middleware
|
135
|
+
require 'logger'
|
136
|
+
|
137
|
+
module GoodData::Bricks
|
138
|
+
class LoggerMiddleware < GoodData::Bricks::Middleware
|
139
|
+
|
140
|
+
def call(params)
|
141
|
+
logger = params[:gdc_logger] = params[:GDC_LOGGER_FILE].nil? ? Logger.new(STDOUT) : Logger.new(params[:GDC_LOGGER_FILE])
|
142
|
+
logger.info("Pipeline starts")
|
143
|
+
|
144
|
+
returning(@app.call(params)) do |result|
|
145
|
+
logger.info("Pipeline ending")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
### Benchmarking middleware
|
153
|
+
|
154
|
+
require 'benchmark'
|
155
|
+
|
156
|
+
module GoodData::Bricks
|
157
|
+
|
158
|
+
class BenchMiddleware < GoodData::Bricks::Middleware
|
159
|
+
|
160
|
+
def call(params)
|
161
|
+
puts "Starting timer"
|
162
|
+
result = nil
|
163
|
+
report = Benchmark.measure { result = @app.call(params) }
|
164
|
+
puts "Stopping timer"
|
165
|
+
pp report
|
166
|
+
result
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
You see there are really only minimal changes. Let's walk through the couple of important points more carefully. As we stated before difference between app and a middleware is mainly in the fact that you can chain middlewares. Thus middleware has to know who is the next on in the chain and at some point it is going to call him. That is the `@app.call(params)`. Notice how we are still using the same interface. This way it is no difference if the next guy is app or middleware. The second important piece might be the returning function but that is just a way how to be more dry. Returning will take one param evaluate it store it you can do some stuff on it and then its value is returned. These things are equivalent.
|
173
|
+
|
174
|
+
returning(Person.new) do |o|
|
175
|
+
p.name = "Tomas"
|
176
|
+
end
|
177
|
+
|
178
|
+
p = Person.new
|
179
|
+
p.name = "Tomas"
|
180
|
+
p
|
181
|
+
|
182
|
+
### Passing values
|
183
|
+
|
184
|
+
We haven't talk much explicitly about passing values. The recommended way how to pass parameters is through the `:call(params)` methods parameters. You can see example of that for example in the logger middleware. It creates a logger and puts it into the param object. All following middlewares that are called can benefit from it. You can again see the usage in the HelloWorld.
|
185
|
+
|
186
|
+
### Providing initial sets of parameters
|
187
|
+
|
188
|
+
When deploying you will have to either provide parameters during execution or to the scheduler. Since we are developing locally it would be great to have similar functionality during local execution. This is exactly what `--params` parameter does.
|
189
|
+
|
190
|
+
bundle exec gooddata -v run_ruby --remote asdas/asdadas --params path_to_params_file.json
|
191
|
+
|
192
|
+
The file is simple JSON file. This file is used in both local or remote execution.
|
193
|
+
|
194
|
+
{
|
195
|
+
"param1" : "value1",
|
196
|
+
"param2" : "value2"
|
197
|
+
}
|
198
|
+
|
199
|
+
### Return parameters
|
200
|
+
|
201
|
+
## Local harness
|
202
|
+
Let's talk a little about the harness that makes it possible to test things locally. It tries to run your bricks in similar environment as it would run on the server. This mainly means that you will get the same parameters etc. The goal is to provide you environment to debug and test before you deploy.
|
203
|
+
|
204
|
+
|
205
|
+
### Run locally
|
206
|
+
|
207
|
+
The most complete command
|
208
|
+
|
209
|
+
bundle exec gooddata -l -s https://secure.gooddata.com -Uname@gooddata.com -Pmy_pass -w https://secure-di.getgooddata.com run_ruby -p rjzbt1shubkj9c8es6f75t2avke748mj -d brick_test --name "some_deploy"
|
210
|
+
|
211
|
+
looks intimidating but let's break it down. Many of these can be omitted and are there in case you can override everything.
|
212
|
+
|
213
|
+
bundle exec
|
214
|
+
|
215
|
+
means you are executing it with exact the same libraries as you would on the server (we are expecting that your local repo is up to date).
|
216
|
+
|
217
|
+
-l
|
218
|
+
|
219
|
+
Means that HTTP communication will be logged to STDOUT.
|
220
|
+
|
221
|
+
-s https://secure.gooddata.com
|
222
|
+
|
223
|
+
Means which datacenter you are connecting to. If you are not going against something special you should be able to leave this out
|
224
|
+
|
225
|
+
-w https://secure-di.getgooddata.com
|
226
|
+
|
227
|
+
File staging area used for uploading the files for execution. Similar case as for he server. By default you should not be forced to use it.
|
228
|
+
|
229
|
+
-p PID
|
230
|
+
|
231
|
+
Currently only execution context we have is project context. You have to specify a project PID to be abel to run your brick
|
232
|
+
|
233
|
+
-d path_to_brick
|
234
|
+
|
235
|
+
This tells the tool where brick lives. It expect the directory where the main.rb lives. Do not point this directly to a file.
|
236
|
+
|
237
|
+
--name some_name
|
238
|
+
|
239
|
+
Used when deployed to name the process so later you can identify it
|
240
|
+
|
241
|
+
### Run remotely
|
242
|
+
|
243
|
+
The only change to do when you are running things remotely is to add `--remote` to the command. Everything else should remain the same.
|
244
|
+
|
245
|
+
<div class="section-nav">
|
246
|
+
<div class="left align-right">
|
247
|
+
<a href="/docs/file/doc/pages/tutorial/TEST_DRIVEN_DEVELOPMENT.md" class="prev">
|
248
|
+
Back
|
249
|
+
</a>
|
250
|
+
</div>
|
251
|
+
<div class="right align-left">
|
252
|
+
|
253
|
+
<span class="next disabled">Next</span>
|
254
|
+
|
255
|
+
</div>
|
256
|
+
<div class="clear"></div>
|
257
|
+
</div>
|
@@ -0,0 +1,79 @@
|
|
1
|
+
There are several ways how to express a model and create it in GoodData. The most prominent way to do it is the visual modeler that is part of the CloudConnect package. There are clear advantages like being visual but there are also drawbacks. It is not repeatable, it is not programmable and it is not text based. Let's have a look how to create a simple model using Ruby SDK.
|
2
|
+
|
3
|
+
## The model
|
4
|
+
The model we will be creating is this
|
5
|
+
|
6
|
+
## The code
|
7
|
+
Create a file called model.rb and put this inside.
|
8
|
+
|
9
|
+
GoodData::Model::ProjectBuilder.create("gooddata-ruby test #{Time.now.to_i}") do |p|
|
10
|
+
p.add_date_dimension("closed_date")
|
11
|
+
|
12
|
+
p.add_dataset("users") do |d|
|
13
|
+
d.add_anchor("id")
|
14
|
+
d.add_label("name", :reference => 'id')
|
15
|
+
end
|
16
|
+
|
17
|
+
p.add_dataset("regions") do |d|
|
18
|
+
d.add_anchor("id")
|
19
|
+
d.add_attribute("name")
|
20
|
+
end
|
21
|
+
|
22
|
+
p.add_dataset("opportunities") do |d|
|
23
|
+
d.add_fact("amount")
|
24
|
+
d.add_date("closed_date", :dataset => "closed_date")
|
25
|
+
d.add_reference("user_id", :dataset => 'users', :reference => 'id')
|
26
|
+
d.add_reference("region_id", :dataset => 'regions', :reference => 'id')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Hopefully the model is self descriptive and if you are not strong on terminology like label, anchor etc please refer to "Building a model with GD".
|
31
|
+
|
32
|
+
## Some rules
|
33
|
+
|
34
|
+
Please note several things
|
35
|
+
|
36
|
+
* we are trying to apply several conventions if you follow them it will be less typing for you but you can always override them.
|
37
|
+
* in all the cases where you type a name it is a string that will be used to create a technical name in gooddata also called identifier on the API. The user visible name which we call title will be inferred if not provided. The inferring process is simple. We expect you to provide name in the snake case (as is typical in ruby, this means things like close_date, opportunity_dimension etc). These will be translated int human readable strings (Close date, Opportunity dimension). If you do not like the title you can specify it directly via :title => "My own title"`
|
38
|
+
* the names are also used as reference names in the model. Notice how the date is using name of the close_date dimension and also the user_id reference is using reference users
|
39
|
+
|
40
|
+
## Executing the model
|
41
|
+
|
42
|
+
gooddata --username joe@example.com --password my_secret_pass --token my_token project build model.rb
|
43
|
+
|
44
|
+
## Loading data
|
45
|
+
As part of the process we allow you to load data since sometimes some initial datasets should be part of the model and not ETL. The typical usecase is for the sake of defining reports which are filtered on certain values these values have to be present.
|
46
|
+
|
47
|
+
### Loading data given inline
|
48
|
+
|
49
|
+
p.upload([["id", "name"],
|
50
|
+
["1", "Tomas"],
|
51
|
+
["2", "Petr"]], :dataset => 'users')
|
52
|
+
|
53
|
+
|
54
|
+
### Loading data given by filename
|
55
|
+
|
56
|
+
p.upload("/some/local_file.csv", :dataset => "users")
|
57
|
+
|
58
|
+
### Loading data given by web file
|
59
|
+
|
60
|
+
p.upload("http://www.example.com/some/remote_file.csv", :dataset => "users")
|
61
|
+
|
62
|
+
In all cases the file has to have headers that has the same name as the name of the particular columns (not necessarily in the same order).
|
63
|
+
|
64
|
+
<div class="section-nav">
|
65
|
+
<div class="left align-right">
|
66
|
+
<a href="/docs/file/doc/pages/tutorial/YOUR_FIRST_PROJECT.md" class="prev">
|
67
|
+
Back
|
68
|
+
</a>
|
69
|
+
</div>
|
70
|
+
|
71
|
+
<div class="right align-left">
|
72
|
+
|
73
|
+
<a href="/docs/file/doc/pages/tutorial/CRUNCHING_NUMBERS.md" class="next">
|
74
|
+
Next
|
75
|
+
</a>
|
76
|
+
|
77
|
+
</div>
|
78
|
+
<div class="clear"></div>
|
79
|
+
</div>
|
@@ -0,0 +1,233 @@
|
|
1
|
+
<div markdown="1">
|
2
|
+
This is *true* markdown text.
|
3
|
+
</div>
|
4
|
+
|
5
|
+
MAQL is a language that is fairly similar to SQL but it is aimed towards getting the data from OLAP system. You are never forced to talk about columns and specify joins explicitly. This is great but there are some drawbacks. Same as SQL MAQL is aimed towards users more than machines which does not help for automation but there is one more caveat that make it hard to use even for humans. Probably to your surprise
|
6
|
+
|
7
|
+
SELECT SUM(Amount)
|
8
|
+
|
9
|
+
Is not a MAQL statement (even though you probably have seen this on UI). The more correct (and what goes back and forth over the wire) is
|
10
|
+
|
11
|
+
SELECT SUM([/gdc/md/132131231/obj/1])
|
12
|
+
|
13
|
+
GoodData UI does a great job at hiding this complexity from you but this significantly hinders the use of MAQL over and API by regular Joes. Ruby SDK tries to alleviate the situation with some tricks. It also gives you many tools to programmatically define and deal with reports and lays the foundations for test driven BI.
|
14
|
+
|
15
|
+
### Jack in
|
16
|
+
If you do not have a project best would be to create one by following our tutorial [Your first project](http://sdk.gooddata.com/gooddata-ruby/recipe/2014/01/19/your-first-project.html) so you can get predictable results.
|
17
|
+
|
18
|
+
First let's look around. There are no metrics
|
19
|
+
|
20
|
+
GoodData::Metric[:all]
|
21
|
+
> []
|
22
|
+
|
23
|
+
there is one fact
|
24
|
+
|
25
|
+
GoodData::Fact[:all]
|
26
|
+
> [{"link"=>"/gdc/md/ptbedvc1841r4obgptywd2mzhbwjsfyr/obj/223",
|
27
|
+
"author"=>"/gdc/account/profile/4e1e8cacc4989228e0ae531b30853248",
|
28
|
+
"tags"=>"",
|
29
|
+
"created"=>"2014-02-18 07:44:26",
|
30
|
+
"deprecated"=>"0",
|
31
|
+
"summary"=>"",
|
32
|
+
"title"=>"Lines changed",
|
33
|
+
"category"=>"fact",
|
34
|
+
"updated"=>"2014-02-18 07:44:26",
|
35
|
+
"contributor"=>"/gdc/account/profile/4e1e8cacc4989228e0ae531b30853248"}]
|
36
|
+
|
37
|
+
### First metric
|
38
|
+
Let's create our first metric. There are couple of ways so I will show them one by one. Regardless of how you create the metric the result is the same so pick the one that suits your style or situation.
|
39
|
+
|
40
|
+
TBD(add identifier based metric)
|
41
|
+
|
42
|
+
m = GoodData::Metric.create("SELECT SUM([/gdc/md/ptbedvc1841r4obgptywd2mzhbwjsfyr/obj/223])")
|
43
|
+
|
44
|
+
You can do it like this but obviously this is the ugly verbose way.
|
45
|
+
|
46
|
+
m = GoodData::Metric.xcreate('SELECT SUM(#"Lines changed")')
|
47
|
+
|
48
|
+
Here you are using the name of the fact. Let's notice couple of things. First we are not using create any more. Method xcreate stands for eXtended notation and tries to turn it into valid MAQL. When you are specifying the fact you are doing it using #"NAME".
|
49
|
+
|
50
|
+
Regardless of which way you used you have a metric definition. Metric is only locally on your computer we haven't saved it yet. Let's do it.
|
51
|
+
|
52
|
+
m.save
|
53
|
+
> RuntimeError: Meric needs to have title
|
54
|
+
|
55
|
+
Uh ok. You have two options
|
56
|
+
|
57
|
+
m.title = "My shiny metric"
|
58
|
+
|
59
|
+
or
|
60
|
+
|
61
|
+
m = GoodData::Metric.xcreate(:title => "My shiny metric", :expression => 'SELECT SUM(#"Lines changed")')
|
62
|
+
|
63
|
+
Go ahead and try saving it again.
|
64
|
+
|
65
|
+
m.save
|
66
|
+
> #<GoodData::Metric:0x007f95b609b548 ....
|
67
|
+
|
68
|
+
Great, looks good. Let's see if it worked
|
69
|
+
|
70
|
+
m.saved?
|
71
|
+
> true
|
72
|
+
|
73
|
+
m.uri
|
74
|
+
> "/gdc/md/ptbedvc1841r4obgptywd2mzhbwjsfyr/obj/292"
|
75
|
+
|
76
|
+
Let's get some numbers. You can execute the metric.
|
77
|
+
|
78
|
+
m.execute
|
79
|
+
> 9.0
|
80
|
+
|
81
|
+
Fantastic. You just created your first report over API.
|
82
|
+
|
83
|
+
### More on metric execution
|
84
|
+
|
85
|
+
Maybe you are wondering if you cannot just execute stuff to poke around. Well you kinda can. The API does not allow to execute a metric without it is saved (but we hope this will change soon). SDK tries to hide this from you but sometimes you can see the wiring. Let's explore. This is our well known metric
|
86
|
+
|
87
|
+
m = GoodData::Metric.xcreate('SELECT SUM(#"Lines changed")')
|
88
|
+
|
89
|
+
Let's try executing it
|
90
|
+
|
91
|
+
m.execute
|
92
|
+
> 9
|
93
|
+
|
94
|
+
It works. What happens behind the scenes is that SDK saves the metric and then deletes it again. It should mostly work
|
95
|
+
|
96
|
+
m.is_saved?
|
97
|
+
> false
|
98
|
+
|
99
|
+
m.uri
|
100
|
+
> nil
|
101
|
+
|
102
|
+
But sometimes you can see some inconsistencies.
|
103
|
+
|
104
|
+
m.title
|
105
|
+
> "Untitled metric"
|
106
|
+
|
107
|
+
This should not stop you most of the time. Just keep this in mind. Hopefully it will go completely away soon.
|
108
|
+
|
109
|
+
### Defining a ReportDefinition
|
110
|
+
|
111
|
+
Ok we have our metric. Now it would be interesting to see a report. The metric broken down. If you are familiar with the model you know the metric is a summation of lines changed in all commits in all products made by all developers. Let's see, how developers contributed.
|
112
|
+
|
113
|
+
GoodData::ReportDefinition.execute(:top => ["attr.devs.id"], :left => [m])
|
114
|
+
> [ 1 | 2 | 3 ]
|
115
|
+
[1.0 | 3.0 | 5.0]
|
116
|
+
|
117
|
+
Again, there are a lots of ways how to achieve the same result so let's have a look at what is available right now. You can already see that you can see a metric by reference and attribute can be referenced just by a string containing an identifier. Let's pass the attribute as an object as well.
|
118
|
+
|
119
|
+
a = GoodData::Attribute.get_by_id("attr.devs.id")
|
120
|
+
GoodData::ReportDefinition.execute(:top => [a], :left => [m])
|
121
|
+
|
122
|
+
If you studied UI well you know that Report is defined using Display Forms (or labels) not by attribute. If you are specifying an attribute SDK will take the first one automatically. This works well most of the time since attributes have typically just one label. But sometimes they have many so you need to me more specific. Coincidently our attribute has 2 labels.
|
123
|
+
|
124
|
+
a.display_forms.count
|
125
|
+
> 2
|
126
|
+
|
127
|
+
The identifiers are "label.devs.email" and "label.devs.id". Let's try using those
|
128
|
+
|
129
|
+
GoodData::ReportDefinition.execute(:top => ["label.devs.id"], :left => [m])
|
130
|
+
GoodData::ReportDefinition.execute(:top => ["label.devs.email"], :left => [m])
|
131
|
+
|
132
|
+
You can even do something that you cannot do on UI and that is using both of the labels at the same time (sic).
|
133
|
+
|
134
|
+
GoodData::ReportDefinition.execute(:top => ["label.devs.id", "label.devs.email"], :left => [m])
|
135
|
+
|
136
|
+
In almost all above cases we had only one thing in left or top section. You can save your fingers and not use the array literal if there is only one item in the section. SDK will wrap them for you.
|
137
|
+
|
138
|
+
GoodData::ReportDefinition.execute(:top => "label.devs.id", :left => m)
|
139
|
+
|
140
|
+
Sometimes it might be useful to refer to the objects in a different way. You can do it by title
|
141
|
+
|
142
|
+
GoodData::ReportDefinition.execute(
|
143
|
+
:top => [{:type => :attribute, :title => "Month/Year (committed_on)"}],
|
144
|
+
:left => m)
|
145
|
+
|
146
|
+
Since underneath it uses MdObject.find_first_by_title it also accepts RegExp literal
|
147
|
+
|
148
|
+
GoodData::ReportDefinition.execute(
|
149
|
+
:top => [{:type => :attribute, :title => /Month\/Year/}],
|
150
|
+
:left => m)
|
151
|
+
|
152
|
+
GoodData::ReportDefinition.execute(
|
153
|
+
:top => [{:type => :attribute, :title => /month\/year/i}],
|
154
|
+
:left => m)
|
155
|
+
|
156
|
+
In our model we have two attributes of name Id. Since the title does not have to be unique this is ok. Currently it will pick the first for you but this behavior will likely change in the favor of throwing an ambiguous error much like your SQL client probably does.
|
157
|
+
|
158
|
+
GoodData::ReportDefinition.execute(:top => [{:type => :attribute, :title => "Id"}], :left => m)
|
159
|
+
|
160
|
+
Of course you can combine all things we learned together.
|
161
|
+
|
162
|
+
a = GoodData::Attribute.find_first_by_title(/month\/year/i)
|
163
|
+
GoodData::ReportDefinition.execute(:top => [{:type => :attribute, :title => "Id"}], :left => [m, a])
|
164
|
+
|
165
|
+
### Reports
|
166
|
+
Up until now we have been computing the reports just because. Maybe you wonder how you can actually create a report that would be saved. It is simple
|
167
|
+
|
168
|
+
GoodData::ReportDefinition.execute(
|
169
|
+
:top => [{:type => :attribute, :title => /Month\/Year/}],
|
170
|
+
:left => m)
|
171
|
+
|
172
|
+
report = GoodData::Report.create(
|
173
|
+
:title => "Fantastic report",
|
174
|
+
:top => [{:type => :attribute, :title => /Month\/Year/}],
|
175
|
+
:left => m)
|
176
|
+
|
177
|
+
report.save
|
178
|
+
|
179
|
+
### Results
|
180
|
+
|
181
|
+
Ok now we know how to create a report. Now let's see what we can do with the result. The point of this framework is not only you being able to create reports programmatically and save them for consumption over UI. Yes this is incredibly useful but when we have that why stop there. With Ruby SDK you can actually consume the result programmatically as well so you can use GD as a basis for your application. Or as we show in this section we build a foundation for Test Driven BI development.
|
182
|
+
|
183
|
+
Let's execute one of the previously defined reports and this time let's store the result
|
184
|
+
|
185
|
+
result = GoodData::ReportDefinition.execute(
|
186
|
+
:top => [{:type => :attribute, :title => /month\/year/i}],
|
187
|
+
:left => m)
|
188
|
+
>
|
189
|
+
[Jan 2014 | Feb 2014]
|
190
|
+
[1.0 | 8.0 ]
|
191
|
+
|
192
|
+
The class is ReportDataResult. As you will see further it tries to conform to API of an array in key aspoects
|
193
|
+
|
194
|
+
result.class
|
195
|
+
> GoodData::ReportDataResult
|
196
|
+
|
197
|
+
result[0][0]
|
198
|
+
> "Jan 2014"
|
199
|
+
|
200
|
+
result[1][0]
|
201
|
+
> 1.0
|
202
|
+
|
203
|
+
All the numbers are of BigDecimal class so you should be able to perform additional computations without losing precision
|
204
|
+
|
205
|
+
result[1][0].class
|
206
|
+
> BigDecimal
|
207
|
+
|
208
|
+
Let's look on couple of methods that are useful for validating the results of reports
|
209
|
+
|
210
|
+
result.include_row? [1, 8]
|
211
|
+
> true
|
212
|
+
|
213
|
+
result.include_column? ["Feb 2014", 8]
|
214
|
+
> true
|
215
|
+
|
216
|
+
Result is coming from server in a special format but after some processing it is just an 2 D array so it is no wonder that you can test on equality of a whole report.
|
217
|
+
|
218
|
+
result == [["Jan 2014", "Feb 2014"], [1, 8]]
|
219
|
+
|
220
|
+
<div class="section-nav">
|
221
|
+
<div class="left align-right">
|
222
|
+
<a href="/docs/file/doc/pages/tutorial/CREATING_A_MODEL.md" class="prev">
|
223
|
+
Back
|
224
|
+
</a>
|
225
|
+
</div>
|
226
|
+
|
227
|
+
<div class="right align-left">
|
228
|
+
<a href="/docs/file/doc/pages/tutorial/TEST_DRIVEN_DEVELOPMENT.md" class="next">
|
229
|
+
Next
|
230
|
+
</a>
|
231
|
+
</div>
|
232
|
+
<div class="clear"></div
|
233
|
+
</div>
|