gooddata 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +13 -5
- data/.rubocop.yml +23 -0
- data/.travis.yml +9 -4
- data/CLI.md +439 -0
- data/Gemfile +0 -1
- data/README.md +2 -2
- data/Rakefile +60 -8
- data/doc/templates/default/module/setup.rb +1 -1
- data/examples.rb +2 -0
- data/gooddata +2 -0
- data/gooddata.gemspec +12 -8
- data/lib/gooddata.rb +0 -2
- data/lib/gooddata/bricks/base_downloader.rb +52 -47
- data/lib/gooddata/bricks/brick.rb +20 -31
- data/lib/gooddata/bricks/bricks.rb +1 -1
- data/lib/gooddata/bricks/middleware/base_middleware.rb +9 -7
- data/lib/gooddata/bricks/middleware/bench_middleware.rb +12 -10
- data/lib/gooddata/bricks/middleware/bulk_salesforce_middleware.rb +28 -28
- data/lib/gooddata/bricks/middleware/fs_upload_middleware.rb +20 -16
- data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +21 -19
- data/lib/gooddata/bricks/middleware/logger_middleware.rb +10 -8
- data/lib/gooddata/bricks/middleware/restforce_middleware.rb +36 -34
- data/lib/gooddata/bricks/middleware/stdout_middleware.rb +11 -9
- data/lib/gooddata/bricks/middleware/twitter_middleware.rb +14 -12
- data/lib/gooddata/bricks/pipeline.rb +28 -0
- data/lib/gooddata/bricks/utils.rb +10 -8
- data/lib/gooddata/cli/cli.rb +1 -6
- data/lib/gooddata/cli/commands/auth_cmd.rb +1 -1
- data/lib/gooddata/cli/commands/console_cmd.rb +7 -5
- data/lib/gooddata/cli/commands/domain_cmd.rb +45 -0
- data/lib/gooddata/cli/commands/process_cmd.rb +42 -5
- data/lib/gooddata/cli/commands/project_cmd.rb +96 -36
- data/lib/gooddata/cli/commands/projects_cmd.rb +21 -0
- data/lib/gooddata/cli/commands/role_cmd.rb +28 -0
- data/lib/gooddata/cli/commands/run_ruby_cmd.rb +5 -5
- data/lib/gooddata/cli/commands/scaffold_cmd.rb +1 -1
- data/lib/gooddata/cli/commands/{profile_cmd.rb → user_cmd.rb} +7 -9
- data/lib/gooddata/cli/shared.rb +3 -2
- data/lib/gooddata/client.rb +16 -304
- data/lib/gooddata/commands/api.rb +13 -5
- data/lib/gooddata/commands/auth.rb +47 -40
- data/lib/gooddata/commands/base.rb +4 -2
- data/lib/gooddata/commands/commands.rb +1 -1
- data/lib/gooddata/commands/datasets.rb +20 -7
- data/lib/gooddata/commands/domain.rb +23 -0
- data/lib/gooddata/commands/process.rb +23 -117
- data/lib/gooddata/commands/project.rb +147 -0
- data/lib/gooddata/commands/projects.rb +8 -102
- data/lib/gooddata/commands/role.rb +26 -0
- data/lib/gooddata/commands/runners.rb +41 -38
- data/lib/gooddata/commands/scaffold.rb +46 -43
- data/lib/gooddata/commands/user.rb +33 -0
- data/lib/gooddata/connection.rb +43 -353
- data/lib/gooddata/core/connection.rb +389 -0
- data/lib/gooddata/core/core.rb +5 -4
- data/lib/gooddata/core/logging.rb +48 -0
- data/lib/gooddata/core/nil_logger.rb +13 -0
- data/lib/gooddata/core/project.rb +70 -0
- data/lib/gooddata/core/rest.rb +120 -0
- data/lib/gooddata/core/threaded.rb +14 -0
- data/lib/gooddata/core/user.rb +19 -0
- data/lib/gooddata/data/data.rb +2 -1
- data/lib/gooddata/data/guesser.rb +16 -12
- data/lib/gooddata/exceptions/command_failed.rb +1 -1
- data/lib/gooddata/exceptions/exceptions.rb +2 -1
- data/lib/gooddata/exceptions/no_project_error.rb +11 -0
- data/lib/gooddata/exceptions/project_not_found.rb +1 -1
- data/lib/gooddata/extensions/big_decimal.rb +6 -2
- data/lib/gooddata/extract.rb +10 -8
- data/lib/gooddata/goodzilla/goodzilla.rb +61 -59
- data/lib/gooddata/helpers.rb +15 -9
- data/lib/gooddata/models/account_settings.rb +124 -0
- data/lib/gooddata/models/attributes/anchor.rb +37 -0
- data/lib/gooddata/models/attributes/attributes.rb +8 -0
- data/lib/gooddata/models/attributes/date_attribute.rb +25 -0
- data/lib/gooddata/models/attributes/time_attribute.rb +24 -0
- data/lib/gooddata/models/columns/attribute.rb +71 -0
- data/lib/gooddata/models/columns/columns.rb +8 -0
- data/lib/gooddata/models/columns/date_column.rb +63 -0
- data/lib/gooddata/models/columns/fact_model.rb +54 -0
- data/lib/gooddata/models/columns/label.rb +55 -0
- data/lib/gooddata/models/columns/reference.rb +57 -0
- data/lib/gooddata/models/dashboard_builder.rb +26 -0
- data/lib/gooddata/models/data_result.rb +10 -9
- data/lib/gooddata/models/domain.rb +131 -0
- data/lib/gooddata/models/empty_result.rb +5 -8
- data/lib/gooddata/models/facts/facts.rb +8 -0
- data/lib/gooddata/models/facts/time_fact.rb +20 -0
- data/lib/gooddata/models/folders/attribute_folder.rb +20 -0
- data/lib/gooddata/models/folders/fact_folder.rb +20 -0
- data/lib/gooddata/models/folders/folders.rb +8 -0
- data/lib/gooddata/models/invitation.rb +78 -0
- data/lib/gooddata/models/links.rb +6 -6
- data/lib/gooddata/models/md_object.rb +25 -0
- data/lib/gooddata/models/metadata.rb +160 -62
- data/lib/gooddata/models/metadata/attribute.rb +81 -0
- data/lib/gooddata/models/metadata/column.rb +61 -0
- data/lib/gooddata/models/{dashboard.rb → metadata/dashboard.rb} +12 -7
- data/lib/gooddata/models/{data_set.rb → metadata/data_set.rb} +5 -4
- data/lib/gooddata/models/metadata/date_dimension.rb +26 -0
- data/lib/gooddata/models/metadata/display_form.rb +61 -0
- data/lib/gooddata/models/metadata/fact.rb +36 -0
- data/lib/gooddata/models/metadata/folder.rb +24 -0
- data/lib/gooddata/models/metadata/metadata.rb +8 -0
- data/lib/gooddata/models/metadata/metric.rb +197 -0
- data/lib/gooddata/models/metadata/report.rb +115 -0
- data/lib/gooddata/models/{report_definition.rb → metadata/report_definition.rb} +16 -10
- data/lib/gooddata/models/metadata/schema.rb +227 -0
- data/lib/gooddata/models/model.rb +38 -1339
- data/lib/gooddata/models/models.rb +5 -2
- data/lib/gooddata/models/module_constants.rb +29 -0
- data/lib/gooddata/models/process.rb +142 -13
- data/lib/gooddata/models/profile.rb +4 -6
- data/lib/gooddata/models/project.rb +406 -136
- data/lib/gooddata/models/project_blueprint.rb +221 -0
- data/lib/gooddata/models/project_builder.rb +136 -0
- data/lib/gooddata/models/project_creator.rb +138 -0
- data/lib/gooddata/models/project_metadata.rb +11 -10
- data/lib/gooddata/models/project_role.rb +92 -0
- data/lib/gooddata/models/references/date_reference.rb +44 -0
- data/lib/gooddata/models/references/references.rb +8 -0
- data/lib/gooddata/models/references/time_reference.rb +13 -0
- data/lib/gooddata/models/report_data_result.rb +11 -11
- data/lib/gooddata/models/schedule.rb +284 -0
- data/lib/gooddata/models/schema_blueprint.rb +158 -0
- data/lib/gooddata/models/schema_builder.rb +81 -0
- data/lib/gooddata/models/tab_builder.rb +23 -0
- data/lib/gooddata/models/user.rb +165 -0
- data/lib/gooddata/version.rb +1 -1
- data/lib/templates/project/data/devs.csv +1 -1
- data/lib/templates/project/data/repos.csv +1 -1
- data/lib/templates/project/model/model.rb.erb +7 -11
- data/spec/bricks/bricks_spec.rb +2 -0
- data/spec/data/test-ci-data.csv +2 -0
- data/spec/data/test_project_model_spec.json +7 -27
- data/spec/helpers/blueprint_helper.rb +2 -0
- data/spec/helpers/cli_helper.rb +2 -0
- data/spec/helpers/connection_helper.rb +14 -1
- data/spec/helpers/project_helper.rb +16 -0
- data/spec/helpers/schema_helper.rb +16 -0
- data/spec/integration/command_projects_spec.rb +7 -7
- data/spec/integration/create_from_template_spec.rb +2 -2
- data/spec/integration/full_project_spec.rb +160 -7
- data/spec/integration/partial_md_export_import_spec.rb +3 -3
- data/spec/logging_in_logging_out_spec.rb +2 -1
- data/spec/spec_helper.rb +26 -4
- data/spec/unit/bricks/bricks_spec.rb +15 -7
- data/spec/unit/bricks/middleware/bench_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/bulk_salesforce_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/gooddata_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/logger_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/restforce_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/stdout_middleware_spec.rb +2 -0
- data/spec/unit/bricks/middleware/twitter_middleware_spec.rb +2 -0
- data/spec/unit/cli/cli_spec.rb +2 -0
- data/spec/unit/cli/commands/cmd_api_spec.rb +23 -15
- data/spec/unit/cli/commands/cmd_auth_spec.rb +8 -4
- data/spec/unit/cli/commands/cmd_domain_spec.rb +82 -0
- data/spec/unit/cli/commands/cmd_process_spec.rb +29 -13
- data/spec/unit/cli/commands/cmd_project_spec.rb +51 -30
- data/spec/unit/cli/commands/cmd_role_spec.rb +44 -0
- data/spec/unit/cli/commands/cmd_run_ruby_spec.rb +8 -4
- data/spec/unit/cli/commands/cmd_scaffold_spec.rb +48 -11
- data/spec/unit/cli/commands/cmd_user_spec.rb +29 -0
- data/spec/unit/commands/command_api_spec.rb +1 -1
- data/spec/unit/commands/command_auth_spec.rb +100 -18
- data/spec/unit/commands/command_dataset_spec.rb +4 -0
- data/spec/unit/commands/command_process_spec.rb +9 -4
- data/spec/unit/commands/command_projects_spec.rb +10 -6
- data/spec/unit/commands/command_scaffold_spec.rb +5 -1
- data/spec/unit/commands/command_user_spec.rb +22 -0
- data/spec/unit/core/connection_spec.rb +35 -6
- data/spec/unit/core/logging_spec.rb +65 -0
- data/spec/unit/core/nil_logger_spec.rb +9 -0
- data/spec/unit/core/project_spec.rb +51 -0
- data/spec/unit/core/rest_spec.rb +33 -0
- data/spec/unit/data/guesser_spec.rb +5 -0
- data/spec/unit/godzilla/goodzilla_spec.rb +2 -0
- data/spec/unit/models/account_settings_spec.rb +28 -0
- data/spec/unit/models/anchor_spec.rb +32 -0
- data/spec/unit/models/attribute_column_spec.rb +7 -0
- data/spec/unit/models/domain_spec.rb +45 -0
- data/spec/unit/models/invitation_spec.rb +13 -0
- data/spec/unit/models/md_object_spec.rb +47 -0
- data/spec/unit/models/metric.rb +92 -0
- data/spec/unit/{model → models}/model_spec.rb +9 -7
- data/spec/unit/models/project_blueprint_spec.rb +202 -0
- data/spec/unit/models/project_creator.rb +73 -0
- data/spec/unit/models/project_role_spec.rb +90 -0
- data/spec/unit/models/project_spec.rb +143 -0
- data/spec/unit/models/schedule_spec.rb +491 -0
- data/spec/unit/{model → models}/schema_builder_spec.rb +2 -0
- data/spec/unit/{model → models}/tools_spec.rb +13 -7
- data/spec/unit/models/user_spec.rb +16 -0
- data/test/test_upload.rb +2 -0
- metadata +189 -86
- data/lib/gooddata/commands/profile.rb +0 -11
- data/lib/gooddata/models/attribute.rb +0 -29
- data/lib/gooddata/models/display_form.rb +0 -9
- data/lib/gooddata/models/fact.rb +0 -19
- data/lib/gooddata/models/metric.rb +0 -99
- data/lib/gooddata/models/report.rb +0 -89
- data/spec/data/blueprint_valid.json +0 -37
- data/spec/unit/cli/commands/cmd_profile_spec.rb +0 -16
- data/spec/unit/commands/command_profile_spec.rb +0 -18
- data/spec/unit/core/core_spec.rb +0 -7
- data/spec/unit/model/blueprint_spec.rb +0 -132
- data/spec/unit/model/project_blueprint_spec.rb +0 -44
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
require_relative '../metadata'
|
|
4
|
+
require_relative 'metadata'
|
|
5
|
+
|
|
6
|
+
module GoodData
|
|
7
|
+
class Report < GoodData::MdObject
|
|
8
|
+
root_key :report
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def [](id, options = {})
|
|
12
|
+
if id == :all
|
|
13
|
+
fail 'You have to specify a project ID' if GoodData.project.nil?
|
|
14
|
+
uri = GoodData.project.md['query'] + '/reports/'
|
|
15
|
+
GoodData.get(uri)['query']['entries']
|
|
16
|
+
else
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create(options = {})
|
|
22
|
+
title = options[:title]
|
|
23
|
+
summary = options[:summary] || ''
|
|
24
|
+
rd = options[:rd] || ReportDefinition.create(:top => options[:top], :left => options[:left])
|
|
25
|
+
rd.save
|
|
26
|
+
|
|
27
|
+
report = {
|
|
28
|
+
'report' => {
|
|
29
|
+
'content' => {
|
|
30
|
+
'domains' => [],
|
|
31
|
+
'definitions' => [rd.uri]
|
|
32
|
+
},
|
|
33
|
+
'meta' => {
|
|
34
|
+
'tags' => '',
|
|
35
|
+
'deprecated' => '0',
|
|
36
|
+
'summary' => summary,
|
|
37
|
+
'title' => title
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
# TODO: write test for report definitions with explicit identifiers
|
|
42
|
+
report['report']['meta']['identifier'] = options[:identifier] if options[:identifier]
|
|
43
|
+
Report.new report
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def results
|
|
48
|
+
content['results']
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def definitions
|
|
52
|
+
content['definitions']
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def latest_report_definition_uri
|
|
56
|
+
definitions.last
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def latest_report_definition
|
|
60
|
+
GoodData::MdObject[latest_report_definition_uri]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def remove_definition(definition)
|
|
64
|
+
def_uri = is_a?(GoodData::ReportDefinition) ? definition.uri : definition
|
|
65
|
+
content['definitions'] = definitions.reject { |x| x == def_uri }
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# TODO: Cover with test. You would probably need something that will be able to create a report easily from a definition
|
|
70
|
+
def remove_definition_but_latest
|
|
71
|
+
to_remove = definitions - [latest_report_definition_uri]
|
|
72
|
+
to_remove.each do |uri|
|
|
73
|
+
remove_definition(uri)
|
|
74
|
+
end
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def purge_report_of_unused_definitions!
|
|
79
|
+
full_list = definitions
|
|
80
|
+
remove_definition_but_latest
|
|
81
|
+
purged_list = definitions
|
|
82
|
+
to_remove = full_list - purged_list
|
|
83
|
+
save
|
|
84
|
+
to_remove.each { |uri| GoodData.delete(uri) }
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def execute
|
|
89
|
+
fail 'You have to save the report before executing. If you do not want to do that please use GoodData::ReportDefinition' unless saved?
|
|
90
|
+
result = GoodData.post '/gdc/xtab2/executor3', 'report_req' => { 'report' => uri }
|
|
91
|
+
data_result_uri = result['execResult']['dataResult']
|
|
92
|
+
result = GoodData.get data_result_uri
|
|
93
|
+
while result['taskState'] && result['taskState']['status'] == 'WAIT'
|
|
94
|
+
sleep 10
|
|
95
|
+
result = GoodData.get data_result_uri
|
|
96
|
+
end
|
|
97
|
+
ReportDataResult.new(GoodData.get data_result_uri)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def exportable?
|
|
101
|
+
true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def export(format)
|
|
105
|
+
result = GoodData.post('/gdc/xtab2/executor3', 'report_req' => { 'report' => uri })
|
|
106
|
+
result1 = GoodData.post('/gdc/exporter/executor', :result_req => { :format => format, :result => result })
|
|
107
|
+
png = GoodData.get(result1['uri'], :process => false)
|
|
108
|
+
while png.code == 202
|
|
109
|
+
sleep(1)
|
|
110
|
+
png = GoodData.get(result1['uri'], :process => false)
|
|
111
|
+
end
|
|
112
|
+
png
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
|
|
3
|
+
require_relative '../metadata'
|
|
3
4
|
require_relative 'metadata'
|
|
4
5
|
|
|
5
6
|
# GoodData Module
|
|
@@ -10,7 +11,7 @@ module GoodData
|
|
|
10
11
|
root_key :reportDefinition
|
|
11
12
|
|
|
12
13
|
class << self
|
|
13
|
-
def [](id)
|
|
14
|
+
def [](id, options = {})
|
|
14
15
|
if id == :all
|
|
15
16
|
uri = GoodData.project.md['query'] + '/reportdefinition/'
|
|
16
17
|
result = GoodData.get(uri)
|
|
@@ -22,7 +23,7 @@ module GoodData
|
|
|
22
23
|
|
|
23
24
|
def create_metrics_part(left, top)
|
|
24
25
|
stuff = Array(left) + Array(top)
|
|
25
|
-
stuff.select { |item| item.respond_to?(:
|
|
26
|
+
stuff.select { |item| item.respond_to?(:metric?) && item.metric? }.map do |metric|
|
|
26
27
|
create_metric_part(metric)
|
|
27
28
|
end
|
|
28
29
|
end
|
|
@@ -47,14 +48,14 @@ module GoodData
|
|
|
47
48
|
def create_part(stuff)
|
|
48
49
|
stuff = Array(stuff)
|
|
49
50
|
parts = stuff.reduce([]) do |memo, item|
|
|
50
|
-
if item.respond_to?(:
|
|
51
|
+
if item.respond_to?(:metric?) && item.metric?
|
|
51
52
|
memo
|
|
52
53
|
else
|
|
53
54
|
memo << create_attribute_part(item)
|
|
54
55
|
end
|
|
55
56
|
memo
|
|
56
57
|
end
|
|
57
|
-
if stuff.any? { |item| item.respond_to?(:
|
|
58
|
+
if stuff.any? { |item| item.respond_to?(:metric?) && item.metric? }
|
|
58
59
|
parts << 'metricGroup'
|
|
59
60
|
end
|
|
60
61
|
parts
|
|
@@ -62,7 +63,7 @@ module GoodData
|
|
|
62
63
|
|
|
63
64
|
def find(stuff)
|
|
64
65
|
stuff.map do |item|
|
|
65
|
-
if item.respond_to?(:
|
|
66
|
+
if item.respond_to?(:attribute?) && item.attribute?
|
|
66
67
|
item.display_forms.first
|
|
67
68
|
elsif item.is_a?(String)
|
|
68
69
|
x = GoodData::MdObject.get_by_id(item)
|
|
@@ -112,7 +113,7 @@ module GoodData
|
|
|
112
113
|
left = Array(options[:left])
|
|
113
114
|
top = Array(options[:top])
|
|
114
115
|
|
|
115
|
-
metrics = (left + top).select { |item| item.respond_to?(:
|
|
116
|
+
metrics = (left + top).select { |item| item.respond_to?(:metric?) && item.metric? }
|
|
116
117
|
|
|
117
118
|
unsaved_metrics = metrics.reject { |i| i.saved? }
|
|
118
119
|
unsaved_metrics.each { |m| m.title = 'Untitled metric' unless m.title }
|
|
@@ -120,7 +121,7 @@ module GoodData
|
|
|
120
121
|
begin
|
|
121
122
|
unsaved_metrics.each { |m| m.save }
|
|
122
123
|
rd = GoodData::ReportDefinition.create(options)
|
|
123
|
-
|
|
124
|
+
data_result(execute_inline(rd))
|
|
124
125
|
ensure
|
|
125
126
|
unsaved_metrics.each { |m| m.delete if m && m.saved? }
|
|
126
127
|
end
|
|
@@ -140,7 +141,11 @@ module GoodData
|
|
|
140
141
|
GoodData.post(uri, data)
|
|
141
142
|
end
|
|
142
143
|
|
|
143
|
-
|
|
144
|
+
# TODO: refactor the method. It should be instance method
|
|
145
|
+
# Method used for getting a data_result from a wire representation of
|
|
146
|
+
# @param result [Hash, Object] Wire data from JSON
|
|
147
|
+
# @return [GoodData::ReportDataResult]
|
|
148
|
+
def data_result(result)
|
|
144
149
|
data_result_uri = result['execResult']['dataResult']
|
|
145
150
|
result = GoodData.get data_result_uri
|
|
146
151
|
|
|
@@ -186,6 +191,8 @@ module GoodData
|
|
|
186
191
|
}
|
|
187
192
|
}
|
|
188
193
|
}
|
|
194
|
+
# TODO: write test for report definitions with explicit identifiers
|
|
195
|
+
pars['reportDefinition']['meta']['identifier'] = options[:identifier] if options[:identifier]
|
|
189
196
|
|
|
190
197
|
ReportDefinition.new(pars)
|
|
191
198
|
end
|
|
@@ -204,8 +211,7 @@ module GoodData
|
|
|
204
211
|
else
|
|
205
212
|
ReportDefinition.execute_inline(self)
|
|
206
213
|
end
|
|
207
|
-
|
|
208
|
-
get_data_result(result)
|
|
214
|
+
ReportDefinition.data_result(result)
|
|
209
215
|
end
|
|
210
216
|
end
|
|
211
217
|
end
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
require_relative '../../helpers'
|
|
4
|
+
|
|
5
|
+
require_relative '../attributes/anchor'
|
|
6
|
+
require_relative '../columns/columns'
|
|
7
|
+
require_relative '../md_object'
|
|
8
|
+
|
|
9
|
+
module GoodData
|
|
10
|
+
module Model
|
|
11
|
+
##
|
|
12
|
+
# Server-side representation of a local data set; includes connection point,
|
|
13
|
+
# attributes and labels, facts, folders and corresponding pieces of physical
|
|
14
|
+
# model abstractions.
|
|
15
|
+
#
|
|
16
|
+
class Schema < MdObject
|
|
17
|
+
attr_reader :fields, :attributes, :facts, :folders, :references, :labels, :name, :title, :anchor
|
|
18
|
+
|
|
19
|
+
def self.load(file)
|
|
20
|
+
Schema.new JSON.load(open(file))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(a_config, a_name = 'Default Name', a_title = 'Default Title')
|
|
24
|
+
super()
|
|
25
|
+
@fields = []
|
|
26
|
+
@attributes = []
|
|
27
|
+
@facts = []
|
|
28
|
+
@folders = {
|
|
29
|
+
:facts => {},
|
|
30
|
+
:attributes => {}
|
|
31
|
+
}
|
|
32
|
+
@references = []
|
|
33
|
+
@labels = []
|
|
34
|
+
|
|
35
|
+
a_config[:name] = a_name unless a_config[:name]
|
|
36
|
+
a_config[:title] = a_config[:name] unless a_config[:title]
|
|
37
|
+
a_config[:title] = a_title unless a_config[:title]
|
|
38
|
+
a_config[:title] = a_config[:title].humanize
|
|
39
|
+
|
|
40
|
+
fail 'Schema name not specified' unless a_config[:name]
|
|
41
|
+
@name = a_config[:name]
|
|
42
|
+
@title = a_config[:title]
|
|
43
|
+
self.config = (a_config)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def config=(config)
|
|
47
|
+
config[:columns].each do |c|
|
|
48
|
+
case c[:type].to_s
|
|
49
|
+
when 'attribute'
|
|
50
|
+
add_attribute c
|
|
51
|
+
when 'fact'
|
|
52
|
+
add_fact c
|
|
53
|
+
when 'date'
|
|
54
|
+
add_date c
|
|
55
|
+
when 'anchor'
|
|
56
|
+
set_anchor c
|
|
57
|
+
when 'label'
|
|
58
|
+
add_label c
|
|
59
|
+
when 'reference'
|
|
60
|
+
add_reference c
|
|
61
|
+
else
|
|
62
|
+
fail "Unexpected type #{c[:type]} in #{c.inspect}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
@anchor = Anchor.new(nil, self) unless @anchor
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def type_prefix
|
|
69
|
+
'dataset'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# Underlying fact table name
|
|
74
|
+
#
|
|
75
|
+
def table
|
|
76
|
+
@table ||= FACT_COLUMN_PREFIX + name
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Generates MAQL DDL script to drop this data set and included pieces
|
|
81
|
+
#
|
|
82
|
+
def to_maql_drop
|
|
83
|
+
maql = ''
|
|
84
|
+
[attributes, facts].each do |obj|
|
|
85
|
+
maql += obj.to_maql_drop
|
|
86
|
+
end
|
|
87
|
+
maql += "DROP {#{identifier}};\n"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Generates MAQL DDL script to create this data set and included pieces
|
|
92
|
+
#
|
|
93
|
+
def to_maql_create
|
|
94
|
+
# TODO: Use template (.erb)
|
|
95
|
+
maql = "# Create the '#{title}' data set\n"
|
|
96
|
+
maql += "CREATE DATASET {#{identifier}} VISUAL (TITLE \"#{title}\");\n\n"
|
|
97
|
+
[attributes, facts, { 1 => @anchor }].each do |objects|
|
|
98
|
+
objects.values.each do |obj|
|
|
99
|
+
maql += "# Create '#{obj.title}' and add it to the '#{title}' data set.\n"
|
|
100
|
+
maql += obj.to_maql_create
|
|
101
|
+
maql += "ALTER DATASET {#{identifier}} ADD {#{obj.identifier}};\n\n"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
labels.each do |label|
|
|
106
|
+
maql += "# Creating Labels\n"
|
|
107
|
+
maql += label.to_maql_create
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
references.values.each do |ref|
|
|
111
|
+
maql += "# Creating references\n"
|
|
112
|
+
maql += ref.to_maql_create
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
folders_maql = "# Create folders\n"
|
|
116
|
+
(folders[:attributes].values + folders[:facts].values).each { |folder| folders_maql += folder.to_maql_create }
|
|
117
|
+
folders_maql + "\n" + maql + "SYNCHRONIZE {#{identifier}};\n"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def upload(path, project = nil, mode = 'FULL')
|
|
121
|
+
if path =~ URI.regexp
|
|
122
|
+
Tempfile.open('remote_file') do |temp|
|
|
123
|
+
temp << open(path).read
|
|
124
|
+
temp.flush
|
|
125
|
+
upload_data(temp, mode)
|
|
126
|
+
end
|
|
127
|
+
else
|
|
128
|
+
upload_data(path, mode)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def upload_data(path, mode)
|
|
133
|
+
GoodData::Model.upload_data(path, to_manifest(mode))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Generates the SLI manifest describing the data loading
|
|
137
|
+
#
|
|
138
|
+
def to_manifest(mode = 'FULL')
|
|
139
|
+
{
|
|
140
|
+
'dataSetSLIManifest' => {
|
|
141
|
+
'parts' => fields.reduce([]) do |memo, f|
|
|
142
|
+
val = f.to_manifest_part(mode)
|
|
143
|
+
memo << val unless val.nil?
|
|
144
|
+
memo
|
|
145
|
+
end,
|
|
146
|
+
'dataSet' => identifier,
|
|
147
|
+
'file' => 'data.csv', # should be configurable
|
|
148
|
+
'csvParams' => {
|
|
149
|
+
'quoteChar' => '"',
|
|
150
|
+
'escapeChar' => '"',
|
|
151
|
+
'separatorChar' => ',',
|
|
152
|
+
'endOfLine' => "\n"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def to_wire_model
|
|
159
|
+
{
|
|
160
|
+
'dataset' => {
|
|
161
|
+
'identifier' => identifier,
|
|
162
|
+
'title' => title,
|
|
163
|
+
'anchor' => @anchor.to_wire_model,
|
|
164
|
+
'facts' => facts.map { |f| f.to_wire_model },
|
|
165
|
+
'attributes' => attributes.map { |a| a.to_wire_model },
|
|
166
|
+
'references' => references.map { |r| r.is_a?(DateReference) ? r.schema_ref : type_prefix + '.' + r.schema_ref }
|
|
167
|
+
} }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
def add_attribute(column)
|
|
173
|
+
attribute = Attribute.new column, self
|
|
174
|
+
fields << attribute
|
|
175
|
+
attributes << attribute
|
|
176
|
+
add_attribute_folder(attribute.folder)
|
|
177
|
+
# folders[AttributeFolder.new(attribute.folder)] = 1 if attribute.folder
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def add_attribute_folder(name)
|
|
181
|
+
return if name.nil?
|
|
182
|
+
return if folders[:attributes].key?(name)
|
|
183
|
+
folders[:attributes][name] = AttributeFolder.new(name)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def add_fact(column)
|
|
187
|
+
fact = Fact.new column, self
|
|
188
|
+
fields << fact
|
|
189
|
+
facts << fact
|
|
190
|
+
add_fact_folder(fact.folder)
|
|
191
|
+
# folders[FactFolder.new(fact.folder)] = 1 if fact.folder
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def add_fact_folder(name)
|
|
195
|
+
return if name.nil?
|
|
196
|
+
return if folders[:facts].key?(name)
|
|
197
|
+
folders[:facts][name] = FactFolder.new(name)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def add_label(column)
|
|
201
|
+
label = Label.new(column, nil, self)
|
|
202
|
+
labels << label
|
|
203
|
+
fields << label
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def add_reference(column)
|
|
207
|
+
reference = Reference.new(column, self)
|
|
208
|
+
fields << reference
|
|
209
|
+
references << reference
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def add_date(column)
|
|
213
|
+
date = DateColumn.new column, self
|
|
214
|
+
@fields << date
|
|
215
|
+
date.parts.values.each { |p| @fields << p }
|
|
216
|
+
date.facts.each { |f| facts << f }
|
|
217
|
+
date.attributes.each { |a| attributes << a }
|
|
218
|
+
date.references.each { |r| references << r }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def set_anchor(column) # rubocop:disable AccessorMethodName
|
|
222
|
+
@anchor = Anchor.new column, self
|
|
223
|
+
@fields << @anchor
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
|
|
3
|
-
require_relative '../
|
|
4
|
-
|
|
3
|
+
require_relative '../core/connection'
|
|
4
|
+
require_relative '../core/rest'
|
|
5
|
+
|
|
6
|
+
require_relative 'attributes/attributes'
|
|
7
|
+
require_relative 'columns/columns'
|
|
8
|
+
require_relative 'facts/facts'
|
|
9
|
+
require_relative 'folders/folders'
|
|
10
|
+
require_relative 'metadata/metadata'
|
|
11
|
+
require_relative 'references/references'
|
|
12
|
+
|
|
13
|
+
require_relative 'links'
|
|
14
|
+
require_relative 'module_constants'
|
|
15
|
+
require_relative 'metadata/schema'
|
|
16
|
+
|
|
17
|
+
require 'fileutils'
|
|
18
|
+
require 'multi_json'
|
|
5
19
|
require 'open-uri'
|
|
6
|
-
require '
|
|
20
|
+
require 'zip'
|
|
7
21
|
|
|
8
22
|
##
|
|
9
23
|
# Module containing classes that counter-part GoodData server-side meta-data
|
|
@@ -11,49 +25,25 @@ require 'active_support/all'
|
|
|
11
25
|
#
|
|
12
26
|
module GoodData
|
|
13
27
|
module Model
|
|
14
|
-
# GoodData REST API categories
|
|
15
|
-
LDM_CTG = 'ldm'
|
|
16
|
-
LDM_MANAGE_CTG = 'ldm-manage'
|
|
17
|
-
|
|
18
|
-
# Model naming conventions
|
|
19
|
-
FIELD_PK = 'id'
|
|
20
|
-
FK_SUFFIX = '_id'
|
|
21
|
-
FACT_COLUMN_PREFIX = 'f_'
|
|
22
|
-
DATE_COLUMN_PREFIX = 'dt_'
|
|
23
|
-
TIME_COLUMN_PREFIX = 'tm_'
|
|
24
|
-
LABEL_COLUMN_PREFIX = 'nm_'
|
|
25
|
-
ATTRIBUTE_FOLDER_PREFIX = 'dim'
|
|
26
|
-
ATTRIBUTE_PREFIX = 'attr'
|
|
27
|
-
LABEL_PREFIX = 'label'
|
|
28
|
-
FACT_PREFIX = 'fact'
|
|
29
|
-
DATE_FACT_PREFIX = 'dt'
|
|
30
|
-
DATE_ATTRIBUTE = 'date'
|
|
31
|
-
DATE_ATTRIBUTE_DEFAULT_DISPLAY_FORM = 'mdyy'
|
|
32
|
-
TIME_FACT_PREFIX = 'tm.dt'
|
|
33
|
-
TIME_ATTRIBUTE_PREFIX = 'attr.time'
|
|
34
|
-
FACT_FOLDER_PREFIX = 'ffld'
|
|
35
|
-
|
|
36
|
-
SKIP_FIELD = false
|
|
37
|
-
|
|
38
28
|
class << self
|
|
39
|
-
def add_dataset(name, columns, project = nil)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
end
|
|
29
|
+
# def add_dataset(name, columns, project = nil)
|
|
30
|
+
# Schema.new('columns' => columns, 'name' => name)
|
|
31
|
+
# add_schema(schema, project)
|
|
32
|
+
# end
|
|
43
33
|
|
|
44
|
-
def add_schema(schema, project = nil)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
end
|
|
34
|
+
# def add_schema(schema, project = nil)
|
|
35
|
+
# unless schema.respond_to?(:to_maql_create) || schema.is_a?(String)
|
|
36
|
+
# fail(ArgumentError, "Schema object or schema file path expected, got '#{schema}'")
|
|
37
|
+
# end
|
|
38
|
+
# schema = Schema.load(schema) unless schema.respond_to?(:to_maql_create)
|
|
39
|
+
# project = GoodData.project unless project
|
|
40
|
+
# ldm_links = GoodData.get project.md[LDM_CTG]
|
|
41
|
+
# ldm_uri = Links.new(ldm_links)[LDM_MANAGE_CTG]
|
|
42
|
+
# GoodData.post ldm_uri, 'manage' => { 'maql' => schema.to_maql_create }
|
|
43
|
+
# end
|
|
54
44
|
|
|
55
45
|
# Load given file into a data set described by the given schema
|
|
56
|
-
def upload_data(path, manifest, options={})
|
|
46
|
+
def upload_data(path, manifest, options = {})
|
|
57
47
|
project = options[:project] || GoodData.project
|
|
58
48
|
# mode = options[:mode] || "FULL"
|
|
59
49
|
path = path.path if path.respond_to? :path
|
|
@@ -63,7 +53,7 @@ module GoodData
|
|
|
63
53
|
dir = Dir.mktmpdir
|
|
64
54
|
begin
|
|
65
55
|
Zip::File.open("#{dir}/upload.zip", Zip::File::CREATE) do |zip|
|
|
66
|
-
# TODO make sure schema columns match CSV column names
|
|
56
|
+
# TODO: make sure schema columns match CSV column names
|
|
67
57
|
zip.get_output_stream('upload_info.json') { |f| f.puts JSON.pretty_generate(manifest) }
|
|
68
58
|
if inline_data
|
|
69
59
|
zip.get_output_stream('data.csv') do |f|
|
|
@@ -83,10 +73,11 @@ module GoodData
|
|
|
83
73
|
end
|
|
84
74
|
|
|
85
75
|
# kick the load
|
|
86
|
-
pull = {'pullIntegration' => File.basename(dir)}
|
|
76
|
+
pull = { 'pullIntegration' => File.basename(dir) }
|
|
87
77
|
link = project.md.links('etl')['pull']
|
|
88
78
|
task = GoodData.post link, pull
|
|
89
|
-
|
|
79
|
+
# TODO: Refactor the task status out
|
|
80
|
+
while GoodData.get(task['pullTask']['uri'])['taskStatus'] == 'RUNNING' || GoodData.get(task['pullTask']['uri'])['taskStatus'] == 'PREPARED'
|
|
90
81
|
sleep 30
|
|
91
82
|
end
|
|
92
83
|
if GoodData.get(task['pullTask']['uri'])['taskStatus'] == 'ERROR'
|
|
@@ -103,1302 +94,10 @@ module GoodData
|
|
|
103
94
|
d = Marshal.load(Marshal.dump(a_schema_blueprint))
|
|
104
95
|
d[:columns] = d[:columns] + b_schema_blueprint[:columns]
|
|
105
96
|
d[:columns].uniq!
|
|
106
|
-
columns_that_failed_to_merge = d[:columns].group_by { |x| x[:name] }.map { |k, v| [k, v.count] }.
|
|
97
|
+
columns_that_failed_to_merge = d[:columns].group_by { |x| x[:name] }.map { |k, v| [k, v.count] }.select { |x| x[1] > 1 }
|
|
107
98
|
fail "Columns #{columns_that_failed_to_merge} failed to merge. When merging columns with the same name they have to be identical." unless columns_that_failed_to_merge.empty?
|
|
108
99
|
d
|
|
109
100
|
end
|
|
110
101
|
end
|
|
111
|
-
|
|
112
|
-
class ProjectBlueprint
|
|
113
|
-
attr_accessor :data
|
|
114
|
-
|
|
115
|
-
def self.from_json(spec)
|
|
116
|
-
if spec.is_a?(String)
|
|
117
|
-
ProjectBlueprint.new(MultiJson.load(File.read(spec), :symbolize_keys => true))
|
|
118
|
-
else
|
|
119
|
-
ProjectBlueprint.new(spec)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def change(&block)
|
|
124
|
-
builder = ProjectBuilder.create_from_data(self)
|
|
125
|
-
block.call(builder)
|
|
126
|
-
builder
|
|
127
|
-
@data = builder.to_hash
|
|
128
|
-
self
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def datasets
|
|
132
|
-
data[:datasets].map { |d| SchemaBlueprint.new(d) }
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def add_dataset(a_dataset, index=nil)
|
|
136
|
-
if index.nil? || index > datasets.length
|
|
137
|
-
data[:datasets] << a_dataset.to_hash
|
|
138
|
-
else
|
|
139
|
-
data[:datasets].insert(index, a_dataset.to_hash)
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def remove_dataset(dataset_name)
|
|
144
|
-
x = data[:datasets].find { |d| d[:name] == dataset_name }
|
|
145
|
-
index = data[:datasets].index(x)
|
|
146
|
-
data[:datasets].delete_at(index)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def date_dimensions
|
|
150
|
-
data[:date_dimensions]
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def get_dataset(name)
|
|
154
|
-
ds = data[:datasets].find { |d| d[:name] == name }
|
|
155
|
-
SchemaBlueprint.new(ds) unless ds.nil?
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def initialize(init_data)
|
|
159
|
-
@data = init_data
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def model_validate
|
|
163
|
-
if datasets.count == 1
|
|
164
|
-
[]
|
|
165
|
-
else
|
|
166
|
-
x = datasets.reduce([]) { |memo, schema| schema.has_anchor? ? memo << [schema.name, schema.anchor[:name]] : memo }
|
|
167
|
-
refs = datasets.reduce([]) do |memo, dataset|
|
|
168
|
-
memo.concat(dataset.references)
|
|
169
|
-
end
|
|
170
|
-
refs.reduce([]) do |memo, ref|
|
|
171
|
-
x.include?([ref[:dataset], ref[:reference]]) ? memo : memo.concat([ref])
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def model_valid?
|
|
177
|
-
errors = model_validate
|
|
178
|
-
errors.empty? ? true : false
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def merge!(a_blueprint)
|
|
182
|
-
temp_blueprint = dup
|
|
183
|
-
a_blueprint.datasets.each do |dataset|
|
|
184
|
-
local_dataset = temp_blueprint.get_dataset(dataset.name)
|
|
185
|
-
if local_dataset.nil?
|
|
186
|
-
temp_blueprint.add_dataset(dataset.dup)
|
|
187
|
-
else
|
|
188
|
-
index = temp_blueprint.datasets.index(local_dataset)
|
|
189
|
-
local_dataset.merge!(dataset)
|
|
190
|
-
temp_blueprint.remove_dataset(local_dataset.name)
|
|
191
|
-
temp_blueprint.add_dataset(local_dataset, index)
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
@data = temp_blueprint.data
|
|
195
|
-
self
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def dup
|
|
199
|
-
deep_copy = Marshal.load(Marshal.dump(data))
|
|
200
|
-
ProjectBlueprint.new(deep_copy)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def title
|
|
204
|
-
data[:title]
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def to_wire_model
|
|
208
|
-
{
|
|
209
|
-
'diffRequest' => {
|
|
210
|
-
'targetModel' => {
|
|
211
|
-
'projectModel' => {
|
|
212
|
-
'datasets' => datasets.map { |d| d.to_wire_model },
|
|
213
|
-
'dateDimensions' => date_dimensions.map { |d|
|
|
214
|
-
{
|
|
215
|
-
'dateDimension' => {
|
|
216
|
-
'name' => d[:name],
|
|
217
|
-
'title' => d[:title] || d[:name].humanize
|
|
218
|
-
}
|
|
219
|
-
} }
|
|
220
|
-
}}}}
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def to_hash
|
|
224
|
-
@data
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
class SchemaBlueprint
|
|
229
|
-
attr_accessor :data
|
|
230
|
-
|
|
231
|
-
def change(&block)
|
|
232
|
-
builder = SchemaBuilder.create_from_data(self)
|
|
233
|
-
block.call(builder)
|
|
234
|
-
builder
|
|
235
|
-
@data = builder.to_hash
|
|
236
|
-
self
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def initialize(init_data)
|
|
240
|
-
@data = init_data
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def upload(source, options={})
|
|
244
|
-
project = options[:project] || GoodData.project
|
|
245
|
-
fail 'You have to specify a project into which you want to load.' if project.nil?
|
|
246
|
-
mode = options[:load] || 'FULL'
|
|
247
|
-
project.upload(source, to_schema, mode)
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def merge!(a_blueprint)
|
|
251
|
-
new_blueprint = GoodData::Model.merge_dataset_columns(self, a_blueprint)
|
|
252
|
-
@data = new_blueprint
|
|
253
|
-
self
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def name
|
|
257
|
-
data[:name]
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def title
|
|
261
|
-
data[:title]
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def to_hash
|
|
265
|
-
data
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def columns
|
|
269
|
-
data[:columns]
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def has_anchor?
|
|
273
|
-
columns.any? { |c| c[:type].to_s == 'anchor' }
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def anchor
|
|
277
|
-
find_column_by_type(:anchor, :first)
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def references
|
|
281
|
-
find_column_by_type(:reference)
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def attributes
|
|
285
|
-
find_column_by_type(:attribute)
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
def facts
|
|
289
|
-
find_column_by_type(:fact)
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def find_column_by_type(type, all=:all)
|
|
293
|
-
type = type.to_s
|
|
294
|
-
if all == :all
|
|
295
|
-
columns.find_all { |c| c[:type].to_s == type }
|
|
296
|
-
else
|
|
297
|
-
columns.find { |c| c[:type].to_s == type }
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
def find_column_by_name(type, all=:all)
|
|
302
|
-
type = type.to_s
|
|
303
|
-
if all == :all
|
|
304
|
-
columns.find_all { |c| c[:name].to_s == type }
|
|
305
|
-
else
|
|
306
|
-
columns.find { |c| c[:name].to_s == type }
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
def to_schema
|
|
311
|
-
Schema.new(to_hash)
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def to_manifest
|
|
315
|
-
to_schema.to_manifest
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
def pretty_print(printer)
|
|
319
|
-
printer.text "Schema <#{object_id}>:\n"
|
|
320
|
-
printer.text " Name: #{name}\n"
|
|
321
|
-
printer.text " Columns: \n"
|
|
322
|
-
printer.text columns.map { |c| " #{c[:name]}: #{c[:type]}" }.join("\n")
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
def dup
|
|
326
|
-
deep_copy = Marshal.load(Marshal.dump(data))
|
|
327
|
-
SchemaBlueprint.new(deep_copy)
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def to_wire_model
|
|
331
|
-
to_schema.to_wire_model
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def ==(other)
|
|
335
|
-
to_hash == other.to_hash
|
|
336
|
-
end
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
class ProjectBuilder
|
|
340
|
-
attr_reader :title, :datasets, :reports, :metrics, :uploads, :users, :assert_report, :date_dimensions
|
|
341
|
-
|
|
342
|
-
class << self
|
|
343
|
-
def create_from_data(blueprint, title = 'Title')
|
|
344
|
-
pb = ProjectBuilder.new(title)
|
|
345
|
-
pb.data = blueprint.to_hash
|
|
346
|
-
pb
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
def create(title, options={}, &block)
|
|
350
|
-
pb = ProjectBuilder.new(title)
|
|
351
|
-
block.call(pb)
|
|
352
|
-
pb
|
|
353
|
-
end
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
def initialize(title)
|
|
357
|
-
@title = title
|
|
358
|
-
@datasets = []
|
|
359
|
-
@reports = []
|
|
360
|
-
@assert_tests = []
|
|
361
|
-
@metrics = []
|
|
362
|
-
@uploads = []
|
|
363
|
-
@users = []
|
|
364
|
-
@dashboards = []
|
|
365
|
-
@date_dimensions = []
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
def add_date_dimension(name, options = {})
|
|
369
|
-
dimension = {
|
|
370
|
-
urn: options[:urn],
|
|
371
|
-
name: name,
|
|
372
|
-
title: options[:title]
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
@date_dimensions << dimension
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
def add_dataset(name, &block)
|
|
379
|
-
builder = GoodData::Model::SchemaBuilder.new(name)
|
|
380
|
-
block.call(builder)
|
|
381
|
-
if @datasets.any? { |item| item[:name] == name }
|
|
382
|
-
ds = @datasets.find { |item| item[:name] == name }
|
|
383
|
-
index = @datasets.index(ds)
|
|
384
|
-
stuff = GoodData::Model.merge_dataset_columns(ds, builder.to_hash)
|
|
385
|
-
@datasets.delete_at(index)
|
|
386
|
-
@datasets.insert(index, stuff)
|
|
387
|
-
else
|
|
388
|
-
@datasets << builder.to_hash
|
|
389
|
-
end
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
def add_report(title, options={})
|
|
393
|
-
@reports << {:title => title}.merge(options)
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
def add_metric(title, options={})
|
|
397
|
-
@metrics << {:title => title}.merge(options)
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
def add_dashboard(title, &block)
|
|
401
|
-
db = DashboardBuilder.new(title)
|
|
402
|
-
block.call(db)
|
|
403
|
-
@dashboards << db.to_hash
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
def load_metrics(file)
|
|
407
|
-
new_metrics = MultiJson.load(open(file).read, :symbolize_keys => true)
|
|
408
|
-
@metrics = @metrics + new_metrics
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def load_datasets(file)
|
|
412
|
-
new_metrics = MultiJson.load(open(file).read, :symbolize_keys => true)
|
|
413
|
-
@datasets = @datasets + new_metrics
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
def assert_report(report, result)
|
|
417
|
-
@assert_tests << {:report => report, :result => result}
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
def upload(data, options={})
|
|
421
|
-
mode = options[:mode] || 'FULL'
|
|
422
|
-
dataset = options[:dataset]
|
|
423
|
-
@uploads << {
|
|
424
|
-
:source => data,
|
|
425
|
-
:mode => mode,
|
|
426
|
-
:dataset => dataset
|
|
427
|
-
}
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
def add_users(users)
|
|
431
|
-
@users << users
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
def to_json(options={})
|
|
435
|
-
eliminate_empty = options[:eliminate_empty] || false
|
|
436
|
-
|
|
437
|
-
if eliminate_empty
|
|
438
|
-
JSON.pretty_generate(to_hash.reject { |k, v| v.is_a?(Enumerable) && v.empty? })
|
|
439
|
-
else
|
|
440
|
-
JSON.pretty_generate(to_hash)
|
|
441
|
-
end
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
def to_hash
|
|
445
|
-
{
|
|
446
|
-
:title => @title,
|
|
447
|
-
:datasets => @datasets,
|
|
448
|
-
:uploads => @uploads,
|
|
449
|
-
:dashboards => @dashboards,
|
|
450
|
-
:metrics => @metrics,
|
|
451
|
-
:reports => @reports,
|
|
452
|
-
:users => @users,
|
|
453
|
-
:assert_tests => @assert_tests,
|
|
454
|
-
:date_dimensions => @date_dimensions
|
|
455
|
-
}
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
def get_dataset(name)
|
|
459
|
-
datasets.find { |d| d.name == name }
|
|
460
|
-
end
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
class DashboardBuilder
|
|
464
|
-
def initialize(title)
|
|
465
|
-
@title = title
|
|
466
|
-
@tabs = []
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
def add_tab(tab, &block)
|
|
470
|
-
tb = TabBuilder.new(tab)
|
|
471
|
-
block.call(tb)
|
|
472
|
-
@tabs << tb
|
|
473
|
-
tb
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
def to_hash
|
|
477
|
-
{
|
|
478
|
-
:name => @name,
|
|
479
|
-
:tabs => @tabs.map { |tab| tab.to_hash }
|
|
480
|
-
}
|
|
481
|
-
end
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
class TabBuilder
|
|
485
|
-
def initialize(title)
|
|
486
|
-
@title = title
|
|
487
|
-
@stuff = []
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
def add_report(options={})
|
|
491
|
-
@stuff << {:type => :report}.merge(options)
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
def to_hash
|
|
495
|
-
{
|
|
496
|
-
:title => @title,
|
|
497
|
-
:items => @stuff
|
|
498
|
-
}
|
|
499
|
-
end
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
class SchemaBuilder
|
|
503
|
-
attr_accessor :data
|
|
504
|
-
|
|
505
|
-
class << self
|
|
506
|
-
def create_from_data(blueprint)
|
|
507
|
-
sc = SchemaBuilder.new
|
|
508
|
-
sc.data = blueprint.to_hash
|
|
509
|
-
sc
|
|
510
|
-
end
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
def initialize(name=nil)
|
|
514
|
-
@data = {
|
|
515
|
-
:name => name,
|
|
516
|
-
:columns => []
|
|
517
|
-
}
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
def name
|
|
521
|
-
data[:name]
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
def columns
|
|
525
|
-
data[:columns]
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
def add_column(column_def)
|
|
529
|
-
columns.push(column_def)
|
|
530
|
-
self
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
def add_anchor(name, options={})
|
|
534
|
-
add_column({:type => :anchor, :name => name}.merge(options))
|
|
535
|
-
self
|
|
536
|
-
end
|
|
537
|
-
|
|
538
|
-
def add_attribute(name, options={})
|
|
539
|
-
add_column({:type => :attribute, :name => name}.merge(options))
|
|
540
|
-
self
|
|
541
|
-
end
|
|
542
|
-
|
|
543
|
-
def add_fact(name, options={})
|
|
544
|
-
add_column({:type => :fact, :name => name}.merge(options))
|
|
545
|
-
self
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
def add_label(name, options={})
|
|
549
|
-
add_column({:type => :label, :name => name}.merge(options))
|
|
550
|
-
self
|
|
551
|
-
end
|
|
552
|
-
|
|
553
|
-
def add_date(name, options={})
|
|
554
|
-
add_column({:type => :date, :name => name}.merge(options))
|
|
555
|
-
end
|
|
556
|
-
|
|
557
|
-
def add_reference(name, options={})
|
|
558
|
-
add_column({:type => :reference, :name => name}.merge(options))
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
def to_json
|
|
562
|
-
JSON.pretty_generate(to_hash)
|
|
563
|
-
end
|
|
564
|
-
|
|
565
|
-
def to_hash
|
|
566
|
-
data
|
|
567
|
-
end
|
|
568
|
-
|
|
569
|
-
def to_schema
|
|
570
|
-
Schema.new(to_hash)
|
|
571
|
-
end
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
class ProjectCreator
|
|
575
|
-
class << self
|
|
576
|
-
def migrate(options={})
|
|
577
|
-
spec = options[:spec] || fail('You need to provide spec for migration')
|
|
578
|
-
spec = spec.to_hash
|
|
579
|
-
|
|
580
|
-
token = options[:token]
|
|
581
|
-
project = options[:project] || GoodData::Project.create(:title => spec[:title], :auth_token => token)
|
|
582
|
-
fail('You need to specify token for project creation') if token.nil? && project.nil?
|
|
583
|
-
|
|
584
|
-
begin
|
|
585
|
-
GoodData.with_project(project) do |p|
|
|
586
|
-
# migrate_date_dimensions(p, spec[:date_dimensions] || [])
|
|
587
|
-
migrate_datasets(p, spec)
|
|
588
|
-
load(p, spec)
|
|
589
|
-
migrate_metrics(p, spec[:metrics] || [])
|
|
590
|
-
migrate_reports(p, spec[:reports] || [])
|
|
591
|
-
migrate_dashboards(p, spec[:dashboards] || [])
|
|
592
|
-
migrate_users(p, spec[:users] || [])
|
|
593
|
-
execute_tests(p, spec[:assert_tests] || [])
|
|
594
|
-
p
|
|
595
|
-
end
|
|
596
|
-
end
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
def migrate_date_dimensions(project, spec)
|
|
600
|
-
spec.each do |dd|
|
|
601
|
-
Model.add_schema(DateDimension.new(dd), project)
|
|
602
|
-
end
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
def migrate_datasets(project, spec)
|
|
606
|
-
bp = ProjectBlueprint.new(spec)
|
|
607
|
-
# schema = Schema.load(schema) unless schema.respond_to?(:to_maql_create)
|
|
608
|
-
# project = GoodData.project unless project
|
|
609
|
-
uri = "/gdc/projects/#{GoodData.project.pid}/model/diff"
|
|
610
|
-
result = GoodData.post(uri, bp.to_wire_model)
|
|
611
|
-
link = result['asyncTask']['link']['poll']
|
|
612
|
-
response = GoodData.get(link, :process => false)
|
|
613
|
-
# pp response
|
|
614
|
-
while response.code != 200
|
|
615
|
-
sleep 1
|
|
616
|
-
GoodData.connection.retryable(:tries => 3, :on => RestClient::InternalServerError) do
|
|
617
|
-
sleep 1
|
|
618
|
-
response = GoodData.get(link, :process => false)
|
|
619
|
-
# pp response
|
|
620
|
-
end
|
|
621
|
-
end
|
|
622
|
-
response = GoodData.get(link)
|
|
623
|
-
ldm_links = GoodData.get project.md[LDM_CTG]
|
|
624
|
-
ldm_uri = Links.new(ldm_links)[LDM_MANAGE_CTG]
|
|
625
|
-
chunks = response['projectModelDiff']['updateScripts'].find_all { |script| script['updateScript']['preserveData'] == true && script['updateScript']['cascadeDrops'] == false }.map { |x| x['updateScript']['maqlDdlChunks'] }.flatten
|
|
626
|
-
chunks.each do |chunk|
|
|
627
|
-
GoodData.post ldm_uri, {'manage' => {'maql' => chunk}}
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
bp.datasets.each do |ds|
|
|
631
|
-
schema = ds.to_schema
|
|
632
|
-
GoodData::ProjectMetadata["manifest_#{schema.name}"] = schema.to_manifest.to_json
|
|
633
|
-
end
|
|
634
|
-
end
|
|
635
|
-
|
|
636
|
-
def migrate_reports(project, spec)
|
|
637
|
-
spec.each do |report|
|
|
638
|
-
project.add_report(report)
|
|
639
|
-
end
|
|
640
|
-
end
|
|
641
|
-
|
|
642
|
-
def migrate_dashboards(project, spec)
|
|
643
|
-
spec.each do |dash|
|
|
644
|
-
project.add_dashboard(dash)
|
|
645
|
-
end
|
|
646
|
-
end
|
|
647
|
-
|
|
648
|
-
def migrate_metrics(project, spec)
|
|
649
|
-
spec.each do |metric|
|
|
650
|
-
project.add_metric(metric)
|
|
651
|
-
end
|
|
652
|
-
end
|
|
653
|
-
|
|
654
|
-
def migrate_users(project, spec)
|
|
655
|
-
spec.each do |user|
|
|
656
|
-
puts "Would migrate user #{user}"
|
|
657
|
-
# project.add_user(user)
|
|
658
|
-
end
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
def load(project, spec)
|
|
662
|
-
if spec.has_key?(:uploads)
|
|
663
|
-
spec[:uploads].each do |load|
|
|
664
|
-
schema = GoodData::Model::Schema.new(spec[:datasets].detect { |d| d[:name] == load[:dataset] })
|
|
665
|
-
project.upload(load[:source], schema, load[:mode])
|
|
666
|
-
end
|
|
667
|
-
end
|
|
668
|
-
end
|
|
669
|
-
|
|
670
|
-
def execute_tests(project, spec)
|
|
671
|
-
spec.each do |assert|
|
|
672
|
-
result = GoodData::ReportDefinition.execute(assert[:report])
|
|
673
|
-
fail "Test did not pass. Got #{result.table.inspect}, expected #{assert[:result].inspect}" if result.table != assert[:result]
|
|
674
|
-
end
|
|
675
|
-
end
|
|
676
|
-
end
|
|
677
|
-
end
|
|
678
|
-
|
|
679
|
-
class MdObject
|
|
680
|
-
attr_accessor :name, :title
|
|
681
|
-
|
|
682
|
-
def visual
|
|
683
|
-
"TITLE \"#{title_esc}\""
|
|
684
|
-
end
|
|
685
|
-
|
|
686
|
-
def title_esc
|
|
687
|
-
title.gsub(/"/, "\\\"")
|
|
688
|
-
end
|
|
689
|
-
|
|
690
|
-
##
|
|
691
|
-
# Generates an identifier from the object name by transliterating
|
|
692
|
-
# non-Latin character and then dropping non-alphanumerical characters.
|
|
693
|
-
#
|
|
694
|
-
def identifier
|
|
695
|
-
@identifier ||= "#{self.type_prefix}.#{name}"
|
|
696
|
-
end
|
|
697
|
-
end
|
|
698
|
-
|
|
699
|
-
##
|
|
700
|
-
# Server-side representation of a local data set; includes connection point,
|
|
701
|
-
# attributes and labels, facts, folders and corresponding pieces of physical
|
|
702
|
-
# model abstractions.
|
|
703
|
-
#
|
|
704
|
-
class Schema < MdObject
|
|
705
|
-
attr_reader :fields, :attributes, :facts, :folders, :references, :labels, :name, :title, :anchor
|
|
706
|
-
|
|
707
|
-
def self.load(file)
|
|
708
|
-
Schema.new JSON.load(open(file))
|
|
709
|
-
end
|
|
710
|
-
|
|
711
|
-
def initialize(config, name = 'Default Name', title = 'Default Title')
|
|
712
|
-
super()
|
|
713
|
-
@fields = []
|
|
714
|
-
@attributes = []
|
|
715
|
-
@facts = []
|
|
716
|
-
@folders = {
|
|
717
|
-
:facts => {},
|
|
718
|
-
:attributes => {}
|
|
719
|
-
}
|
|
720
|
-
@references = []
|
|
721
|
-
@labels = []
|
|
722
|
-
|
|
723
|
-
config[:name] = name unless config[:name]
|
|
724
|
-
config[:title] = config[:name] unless config[:title]
|
|
725
|
-
config[:title] = title unless config[:title]
|
|
726
|
-
config[:title] = config[:title].humanize
|
|
727
|
-
|
|
728
|
-
fail 'Schema name not specified' unless config[:name]
|
|
729
|
-
self.name = config[:name]
|
|
730
|
-
self.title = config[:title]
|
|
731
|
-
self.config = config
|
|
732
|
-
end
|
|
733
|
-
|
|
734
|
-
def config=(config)
|
|
735
|
-
config[:columns].each do |c|
|
|
736
|
-
case c[:type].to_s
|
|
737
|
-
when 'attribute'
|
|
738
|
-
add_attribute c
|
|
739
|
-
when 'fact'
|
|
740
|
-
add_fact c
|
|
741
|
-
when 'date'
|
|
742
|
-
add_date c
|
|
743
|
-
when 'anchor'
|
|
744
|
-
set_anchor c
|
|
745
|
-
when 'label'
|
|
746
|
-
add_label c
|
|
747
|
-
when 'reference'
|
|
748
|
-
add_reference c
|
|
749
|
-
else
|
|
750
|
-
fail "Unexpected type #{c[:type]} in #{c.inspect}"
|
|
751
|
-
end
|
|
752
|
-
end
|
|
753
|
-
@anchor = Anchor.new(nil, self) unless @anchor
|
|
754
|
-
end
|
|
755
|
-
|
|
756
|
-
def type_prefix
|
|
757
|
-
'dataset'
|
|
758
|
-
end
|
|
759
|
-
|
|
760
|
-
##
|
|
761
|
-
# Underlying fact table name
|
|
762
|
-
#
|
|
763
|
-
def table
|
|
764
|
-
@table ||= FACT_COLUMN_PREFIX + name
|
|
765
|
-
end
|
|
766
|
-
|
|
767
|
-
##
|
|
768
|
-
# Generates MAQL DDL script to drop this data set and included pieces
|
|
769
|
-
#
|
|
770
|
-
def to_maql_drop
|
|
771
|
-
maql = ''
|
|
772
|
-
[attributes, facts].each do |obj|
|
|
773
|
-
maql += obj.to_maql_drop
|
|
774
|
-
end
|
|
775
|
-
maql += "DROP {#{self.identifier}};\n"
|
|
776
|
-
end
|
|
777
|
-
|
|
778
|
-
##
|
|
779
|
-
# Generates MAQL DDL script to create this data set and included pieces
|
|
780
|
-
#
|
|
781
|
-
def to_maql_create
|
|
782
|
-
# TODO: Use template (.erb)
|
|
783
|
-
maql = "# Create the '#{self.title}' data set\n"
|
|
784
|
-
maql += "CREATE DATASET {#{self.identifier}} VISUAL (TITLE \"#{self.title}\");\n\n"
|
|
785
|
-
[attributes, facts, {1 => @anchor}].each do |objects|
|
|
786
|
-
objects.values.each do |obj|
|
|
787
|
-
maql += "# Create '#{obj.title}' and add it to the '#{self.title}' data set.\n"
|
|
788
|
-
maql += obj.to_maql_create
|
|
789
|
-
maql += "ALTER DATASET {#{self.identifier}} ADD {#{obj.identifier}};\n\n"
|
|
790
|
-
end
|
|
791
|
-
end
|
|
792
|
-
|
|
793
|
-
labels.each do |label|
|
|
794
|
-
maql += "# Creating Labels\n"
|
|
795
|
-
maql += label.to_maql_create
|
|
796
|
-
end
|
|
797
|
-
|
|
798
|
-
references.values.each do |ref|
|
|
799
|
-
maql += "# Creating references\n"
|
|
800
|
-
maql += ref.to_maql_create
|
|
801
|
-
end
|
|
802
|
-
|
|
803
|
-
folders_maql = "# Create folders\n"
|
|
804
|
-
(folders[:attributes].values + folders[:facts].values).each { |folder| folders_maql += folder.to_maql_create }
|
|
805
|
-
folders_maql + "\n" + maql + "SYNCHRONIZE {#{identifier}};\n"
|
|
806
|
-
end
|
|
807
|
-
|
|
808
|
-
def upload(path, project = nil, mode = 'FULL')
|
|
809
|
-
if path =~ URI::regexp
|
|
810
|
-
Tempfile.open('remote_file') do |temp|
|
|
811
|
-
temp << open(path).read
|
|
812
|
-
temp.flush
|
|
813
|
-
upload_data(temp, mode)
|
|
814
|
-
end
|
|
815
|
-
else
|
|
816
|
-
upload_data(path, mode)
|
|
817
|
-
end
|
|
818
|
-
end
|
|
819
|
-
|
|
820
|
-
def upload_data(path, mode)
|
|
821
|
-
GoodData::Model.upload_data(path, to_manifest(mode))
|
|
822
|
-
end
|
|
823
|
-
|
|
824
|
-
# Generates the SLI manifest describing the data loading
|
|
825
|
-
#
|
|
826
|
-
def to_manifest(mode = 'FULL')
|
|
827
|
-
{
|
|
828
|
-
'dataSetSLIManifest' => {
|
|
829
|
-
'parts' => fields.reduce([]) { |memo, f| val = f.to_manifest_part(mode); memo << val unless val.nil?; memo },
|
|
830
|
-
'dataSet' => self.identifier,
|
|
831
|
-
'file' => 'data.csv', # should be configurable
|
|
832
|
-
'csvParams' => {
|
|
833
|
-
'quoteChar' => '"',
|
|
834
|
-
'escapeChar' => '"',
|
|
835
|
-
'separatorChar' => ',',
|
|
836
|
-
'endOfLine' => "\n"
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
end
|
|
841
|
-
|
|
842
|
-
def to_wire_model
|
|
843
|
-
{
|
|
844
|
-
'dataset' => {
|
|
845
|
-
'identifier' => identifier,
|
|
846
|
-
'title' => title,
|
|
847
|
-
'anchor' => @anchor.to_wire_model,
|
|
848
|
-
'facts' => facts.map { |f| f.to_wire_model },
|
|
849
|
-
'attributes' => attributes.map { |a| a.to_wire_model },
|
|
850
|
-
'references' => references.map { |r| r.is_a?(DateReference) ? r.schema_ref : type_prefix + '.' + r.schema_ref }}
|
|
851
|
-
}
|
|
852
|
-
end
|
|
853
|
-
|
|
854
|
-
private
|
|
855
|
-
|
|
856
|
-
def add_attribute(column)
|
|
857
|
-
attribute = Attribute.new column, self
|
|
858
|
-
fields << attribute
|
|
859
|
-
attributes << attribute
|
|
860
|
-
add_attribute_folder(attribute.folder)
|
|
861
|
-
# folders[AttributeFolder.new(attribute.folder)] = 1 if attribute.folder
|
|
862
|
-
end
|
|
863
|
-
|
|
864
|
-
def add_attribute_folder(name)
|
|
865
|
-
return if name.nil?
|
|
866
|
-
return if folders[:attributes].has_key?(name)
|
|
867
|
-
folders[:attributes][name] = AttributeFolder.new(name)
|
|
868
|
-
end
|
|
869
|
-
|
|
870
|
-
def add_fact(column)
|
|
871
|
-
fact = Fact.new column, self
|
|
872
|
-
fields << fact
|
|
873
|
-
facts << fact
|
|
874
|
-
add_fact_folder(fact.folder)
|
|
875
|
-
# folders[FactFolder.new(fact.folder)] = 1 if fact.folder
|
|
876
|
-
end
|
|
877
|
-
|
|
878
|
-
def add_fact_folder(name)
|
|
879
|
-
return if name.nil?
|
|
880
|
-
return if folders[:facts].has_key?(name)
|
|
881
|
-
folders[:facts][name] = FactFolder.new(name)
|
|
882
|
-
end
|
|
883
|
-
|
|
884
|
-
def add_label(column)
|
|
885
|
-
label = Label.new(column, nil, self)
|
|
886
|
-
labels << label
|
|
887
|
-
fields << label
|
|
888
|
-
end
|
|
889
|
-
|
|
890
|
-
def add_reference(column)
|
|
891
|
-
reference = Reference.new(column, self)
|
|
892
|
-
fields << reference
|
|
893
|
-
references << reference
|
|
894
|
-
end
|
|
895
|
-
|
|
896
|
-
def add_date(column)
|
|
897
|
-
date = DateColumn.new column, self
|
|
898
|
-
@fields << date
|
|
899
|
-
date.parts.values.each { |p| @fields << p }
|
|
900
|
-
date.facts.each { |f| facts << f }
|
|
901
|
-
date.attributes.each { |a| attributes << a }
|
|
902
|
-
date.references.each { |r| references << r }
|
|
903
|
-
end
|
|
904
|
-
|
|
905
|
-
def set_anchor(column)
|
|
906
|
-
@anchor = Anchor.new column, self
|
|
907
|
-
@fields << @anchor
|
|
908
|
-
end
|
|
909
|
-
end
|
|
910
|
-
|
|
911
|
-
##
|
|
912
|
-
# This is a base class for server-side LDM elements such as attributes, labels and
|
|
913
|
-
# facts
|
|
914
|
-
#
|
|
915
|
-
class Column < MdObject
|
|
916
|
-
attr_accessor :folder, :name, :title, :schema
|
|
917
|
-
|
|
918
|
-
def initialize(hash, schema)
|
|
919
|
-
super()
|
|
920
|
-
raise ArgumentError.new("Schema must be provided, got #{schema.class}") unless schema.is_a? Schema
|
|
921
|
-
raise('Data set fields must have their names defined') if hash[:name].nil?
|
|
922
|
-
|
|
923
|
-
@name = hash[:name]
|
|
924
|
-
@title = hash[:title] || hash[:name].humanize
|
|
925
|
-
@folder = hash[:folder]
|
|
926
|
-
@schema = schema
|
|
927
|
-
end
|
|
928
|
-
|
|
929
|
-
##
|
|
930
|
-
# Generates an identifier from the object name by transliterating
|
|
931
|
-
# non-Latin character and then dropping non-alphanumerical characters.
|
|
932
|
-
#
|
|
933
|
-
def identifier
|
|
934
|
-
@identifier ||= "#{self.type_prefix}.#{@schema.name}.#{name}"
|
|
935
|
-
end
|
|
936
|
-
|
|
937
|
-
def to_maql_drop
|
|
938
|
-
"DROP {#{self.identifier}};\n"
|
|
939
|
-
end
|
|
940
|
-
|
|
941
|
-
def visual
|
|
942
|
-
visual = super
|
|
943
|
-
visual += ", FOLDER {#{folder_prefix}.#{(folder)}}" if folder
|
|
944
|
-
visual
|
|
945
|
-
end
|
|
946
|
-
|
|
947
|
-
def to_csv_header(row)
|
|
948
|
-
name
|
|
949
|
-
end
|
|
950
|
-
|
|
951
|
-
def to_csv_data(headers, row)
|
|
952
|
-
row[name]
|
|
953
|
-
end
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
# Overriden to prevent long strings caused by the @schema attribute
|
|
957
|
-
#
|
|
958
|
-
def inspect
|
|
959
|
-
to_s.sub(/>$/, " @title=#{@title.inspect}, @name=#{@name.inspect}, @folder=#{@folder.inspect}," \
|
|
960
|
-
" @schema=#{@schema.to_s.sub(/>$/, ' @title=' + @schema.name.inspect + '>')}" \
|
|
961
|
-
">")
|
|
962
|
-
end
|
|
963
|
-
end
|
|
964
|
-
|
|
965
|
-
##
|
|
966
|
-
# GoodData attribute abstraction
|
|
967
|
-
#
|
|
968
|
-
class Attribute < Column
|
|
969
|
-
attr_reader :primary_label, :labels
|
|
970
|
-
|
|
971
|
-
def type_prefix;
|
|
972
|
-
ATTRIBUTE_PREFIX;
|
|
973
|
-
end
|
|
974
|
-
|
|
975
|
-
def folder_prefix;
|
|
976
|
-
ATTRIBUTE_FOLDER_PREFIX;
|
|
977
|
-
end
|
|
978
|
-
|
|
979
|
-
def initialize(hash, schema)
|
|
980
|
-
super hash, schema
|
|
981
|
-
@labels = []
|
|
982
|
-
@primary_label = Label.new hash, self, schema
|
|
983
|
-
end
|
|
984
|
-
|
|
985
|
-
def table
|
|
986
|
-
@table ||= 'd_' + @schema.name + '_' + name
|
|
987
|
-
end
|
|
988
|
-
|
|
989
|
-
def key;
|
|
990
|
-
"#{@name}#{FK_SUFFIX}";
|
|
991
|
-
end
|
|
992
|
-
|
|
993
|
-
def to_maql_create
|
|
994
|
-
maql = "CREATE ATTRIBUTE {#{identifier}} VISUAL (#{visual})" \
|
|
995
|
-
+ " AS KEYS {#{table}.#{Model::FIELD_PK}} FULLSET;\n"
|
|
996
|
-
maql += @primary_label.to_maql_create if @primary_label
|
|
997
|
-
maql
|
|
998
|
-
end
|
|
999
|
-
|
|
1000
|
-
def to_manifest_part(mode)
|
|
1001
|
-
{
|
|
1002
|
-
'referenceKey' => 1,
|
|
1003
|
-
'populates' => [@primary_label.identifier],
|
|
1004
|
-
'mode' => mode,
|
|
1005
|
-
'columnName' => name
|
|
1006
|
-
}
|
|
1007
|
-
end
|
|
1008
|
-
|
|
1009
|
-
def to_wire_model
|
|
1010
|
-
{
|
|
1011
|
-
'attribute' => {
|
|
1012
|
-
'identifier' => identifier,
|
|
1013
|
-
'title' => title,
|
|
1014
|
-
'labels' => labels.map do |l|
|
|
1015
|
-
{
|
|
1016
|
-
'label' => {
|
|
1017
|
-
'identifier' => l.identifier,
|
|
1018
|
-
'title' => l.title,
|
|
1019
|
-
'type' => 'GDC.text'
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
end
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
end
|
|
1026
|
-
end
|
|
1027
|
-
|
|
1028
|
-
##
|
|
1029
|
-
# GoodData display form abstraction. Represents a default representation
|
|
1030
|
-
# of an attribute column or an additional representation defined in a LABEL
|
|
1031
|
-
# field
|
|
1032
|
-
#
|
|
1033
|
-
class Label < Column
|
|
1034
|
-
attr_accessor :attribute
|
|
1035
|
-
|
|
1036
|
-
def type_prefix;
|
|
1037
|
-
'label';
|
|
1038
|
-
end
|
|
1039
|
-
|
|
1040
|
-
# def initialize(hash, schema)
|
|
1041
|
-
def initialize(hash, attribute, schema)
|
|
1042
|
-
super hash, schema
|
|
1043
|
-
attribute = attribute.nil? ? schema.fields.find { |field| field.name === hash[:reference] } : attribute
|
|
1044
|
-
@attribute = attribute
|
|
1045
|
-
attribute.labels << self
|
|
1046
|
-
end
|
|
1047
|
-
|
|
1048
|
-
def to_maql_create
|
|
1049
|
-
'# LABEL FROM LABEL'
|
|
1050
|
-
"ALTER ATTRIBUTE {#{@attribute.identifier}} ADD LABELS {#{identifier}}" \
|
|
1051
|
-
+ " VISUAL (TITLE #{title.inspect}) AS {#{column}};\n"
|
|
1052
|
-
end
|
|
1053
|
-
|
|
1054
|
-
def to_manifest_part(mode)
|
|
1055
|
-
{
|
|
1056
|
-
'populates' => [identifier],
|
|
1057
|
-
'mode' => mode,
|
|
1058
|
-
'columnName' => name
|
|
1059
|
-
}
|
|
1060
|
-
end
|
|
1061
|
-
|
|
1062
|
-
def column
|
|
1063
|
-
"#{@attribute.table}.#{LABEL_COLUMN_PREFIX}#{name}"
|
|
1064
|
-
end
|
|
1065
|
-
|
|
1066
|
-
alias :inspect_orig :inspect
|
|
1067
|
-
|
|
1068
|
-
def inspect
|
|
1069
|
-
inspect_orig.sub(/>$/, " @attribute=#{@attribute.to_s.sub(/>$/, " @name=#{@attribute.name}")}>")
|
|
1070
|
-
end
|
|
1071
|
-
end
|
|
1072
|
-
|
|
1073
|
-
##
|
|
1074
|
-
# A GoodData attribute that represents a data set's connection point or a data set
|
|
1075
|
-
# without a connection point
|
|
1076
|
-
#
|
|
1077
|
-
class Anchor < Attribute
|
|
1078
|
-
def initialize(column, schema)
|
|
1079
|
-
if column then
|
|
1080
|
-
super
|
|
1081
|
-
else
|
|
1082
|
-
super({:type => 'anchor', :name => 'id'}, schema)
|
|
1083
|
-
@labels = []
|
|
1084
|
-
@primary_label = nil
|
|
1085
|
-
end
|
|
1086
|
-
end
|
|
1087
|
-
|
|
1088
|
-
def table
|
|
1089
|
-
@table ||= 'f_' + @schema.name
|
|
1090
|
-
end
|
|
1091
|
-
|
|
1092
|
-
def to_maql_create
|
|
1093
|
-
maql = super
|
|
1094
|
-
maql += "\n# Connect '#{self.title}' to all attributes of this data set\n"
|
|
1095
|
-
@schema.attributes.values.each do |c|
|
|
1096
|
-
maql += "ALTER ATTRIBUTE {#{c.identifier}} ADD KEYS " \
|
|
1097
|
-
+ "{#{table}.#{c.key}};\n"
|
|
1098
|
-
end
|
|
1099
|
-
maql
|
|
1100
|
-
end
|
|
1101
|
-
end
|
|
1102
|
-
|
|
1103
|
-
##
|
|
1104
|
-
# GoodData fact abstraction
|
|
1105
|
-
#
|
|
1106
|
-
class Fact < Column
|
|
1107
|
-
def type_prefix;
|
|
1108
|
-
FACT_PREFIX;
|
|
1109
|
-
end
|
|
1110
|
-
|
|
1111
|
-
def column_prefix;
|
|
1112
|
-
FACT_COLUMN_PREFIX;
|
|
1113
|
-
end
|
|
1114
|
-
|
|
1115
|
-
def folder_prefix;
|
|
1116
|
-
FACT_FOLDER_PREFIX;
|
|
1117
|
-
end
|
|
1118
|
-
|
|
1119
|
-
def table
|
|
1120
|
-
@schema.table
|
|
1121
|
-
end
|
|
1122
|
-
|
|
1123
|
-
def column
|
|
1124
|
-
@column ||= table + '.' + column_prefix + name
|
|
1125
|
-
end
|
|
1126
|
-
|
|
1127
|
-
def to_maql_create
|
|
1128
|
-
"CREATE FACT {#{self.identifier}} VISUAL (#{visual})" \
|
|
1129
|
-
+ " AS {#{column}};\n"
|
|
1130
|
-
end
|
|
1131
|
-
|
|
1132
|
-
def to_manifest_part(mode)
|
|
1133
|
-
{
|
|
1134
|
-
'populates' => [identifier],
|
|
1135
|
-
'mode' => mode,
|
|
1136
|
-
'columnName' => name
|
|
1137
|
-
}
|
|
1138
|
-
end
|
|
1139
|
-
|
|
1140
|
-
def to_wire_model
|
|
1141
|
-
{
|
|
1142
|
-
'fact' => {
|
|
1143
|
-
'identifier' => identifier,
|
|
1144
|
-
'title' => title
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
end
|
|
1148
|
-
end
|
|
1149
|
-
|
|
1150
|
-
##
|
|
1151
|
-
# Reference to another data set
|
|
1152
|
-
#
|
|
1153
|
-
class Reference < Column
|
|
1154
|
-
attr_accessor :reference, :schema_ref
|
|
1155
|
-
|
|
1156
|
-
def initialize(column, schema)
|
|
1157
|
-
super column, schema
|
|
1158
|
-
# pp column
|
|
1159
|
-
@name = column[:name]
|
|
1160
|
-
@reference = column[:reference]
|
|
1161
|
-
@schema_ref = column[:dataset]
|
|
1162
|
-
@schema = schema
|
|
1163
|
-
end
|
|
1164
|
-
|
|
1165
|
-
##
|
|
1166
|
-
# Generates an identifier of the referencing attribute using the
|
|
1167
|
-
# schema name derived from schemaReference and column name derived
|
|
1168
|
-
# from the reference key.
|
|
1169
|
-
#
|
|
1170
|
-
def identifier
|
|
1171
|
-
@identifier ||= "#{ATTRIBUTE_PREFIX}.#{@schema_ref}.#{@reference}"
|
|
1172
|
-
end
|
|
1173
|
-
|
|
1174
|
-
def key;
|
|
1175
|
-
"#{@name}_id";
|
|
1176
|
-
end
|
|
1177
|
-
|
|
1178
|
-
def label_column
|
|
1179
|
-
"#{LABEL_PREFIX}.#{@schema_ref}.#{@reference}"
|
|
1180
|
-
end
|
|
1181
|
-
|
|
1182
|
-
def to_maql_create
|
|
1183
|
-
"ALTER ATTRIBUTE {#{self.identifier}} ADD KEYS {#{@schema.table}.#{key}};\n"
|
|
1184
|
-
end
|
|
1185
|
-
|
|
1186
|
-
def to_maql_drop
|
|
1187
|
-
"ALTER ATTRIBUTE {#{self.identifier} DROP KEYS {#{@schema.table}.#{key}};\n"
|
|
1188
|
-
end
|
|
1189
|
-
|
|
1190
|
-
def to_manifest_part(mode)
|
|
1191
|
-
{
|
|
1192
|
-
'populates' => [label_column],
|
|
1193
|
-
'mode' => mode,
|
|
1194
|
-
'columnName' => name,
|
|
1195
|
-
'referenceKey' => 1
|
|
1196
|
-
}
|
|
1197
|
-
end
|
|
1198
|
-
end
|
|
1199
|
-
|
|
1200
|
-
##
|
|
1201
|
-
# Date as a reference to a date dimension
|
|
1202
|
-
#
|
|
1203
|
-
class DateReference < Reference
|
|
1204
|
-
attr_accessor :format, :output_format, :urn
|
|
1205
|
-
|
|
1206
|
-
def initialize(column, schema)
|
|
1207
|
-
super column, schema
|
|
1208
|
-
@output_format = column['format'] || 'dd/MM/yyyy'
|
|
1209
|
-
@format = @output_format.gsub('yyyy', '%Y').gsub('MM', '%m').gsub('dd', '%d')
|
|
1210
|
-
@urn = column[:urn] || 'URN:GOODDATA:DATE'
|
|
1211
|
-
end
|
|
1212
|
-
|
|
1213
|
-
def identifier
|
|
1214
|
-
@identifier ||= "#{@schema_ref}.#{DATE_ATTRIBUTE}"
|
|
1215
|
-
end
|
|
1216
|
-
|
|
1217
|
-
def to_manifest_part(mode)
|
|
1218
|
-
{
|
|
1219
|
-
'populates' => ["#{identifier}.#{DATE_ATTRIBUTE_DEFAULT_DISPLAY_FORM}"],
|
|
1220
|
-
'mode' => mode,
|
|
1221
|
-
'constraints' => {'date' => output_format},
|
|
1222
|
-
'columnName' => name,
|
|
1223
|
-
'referenceKey' => 1
|
|
1224
|
-
}
|
|
1225
|
-
end
|
|
1226
|
-
|
|
1227
|
-
# def to_maql_create
|
|
1228
|
-
# # urn:chefs_warehouse_fiscal:date
|
|
1229
|
-
# super_maql = super
|
|
1230
|
-
# maql = ""
|
|
1231
|
-
# # maql = "# Include date dimensions\n"
|
|
1232
|
-
# # maql += "INCLUDE TEMPLATE \"#{urn}\" MODIFY (IDENTIFIER \"#{name}\", TITLE \"#{title || name}\");\n"
|
|
1233
|
-
# maql += super_maql
|
|
1234
|
-
# end
|
|
1235
|
-
end
|
|
1236
|
-
|
|
1237
|
-
##
|
|
1238
|
-
# Date field that's not connected to a date dimension
|
|
1239
|
-
#
|
|
1240
|
-
class DateAttribute < Attribute
|
|
1241
|
-
def key;
|
|
1242
|
-
"#{DATE_COLUMN_PREFIX}#{super}";
|
|
1243
|
-
end
|
|
1244
|
-
|
|
1245
|
-
def to_manifest_part(mode)
|
|
1246
|
-
{
|
|
1247
|
-
'populates' => ['label.stuff.mmddyy'],
|
|
1248
|
-
'format' => 'unknown',
|
|
1249
|
-
'mode' => mode,
|
|
1250
|
-
'referenceKey' => 1
|
|
1251
|
-
}
|
|
1252
|
-
end
|
|
1253
|
-
end
|
|
1254
|
-
|
|
1255
|
-
##
|
|
1256
|
-
# Fact representation of a time of a day
|
|
1257
|
-
#
|
|
1258
|
-
class TimeFact < Fact
|
|
1259
|
-
def column_prefix;
|
|
1260
|
-
TIME_COLUMN_PREFIX;
|
|
1261
|
-
end
|
|
1262
|
-
|
|
1263
|
-
def type_prefix;
|
|
1264
|
-
TIME_FACT_PREFIX;
|
|
1265
|
-
end
|
|
1266
|
-
end
|
|
1267
|
-
|
|
1268
|
-
##
|
|
1269
|
-
# Time as a reference to a time-of-a-day dimension
|
|
1270
|
-
#
|
|
1271
|
-
class TimeReference < Reference
|
|
1272
|
-
end
|
|
1273
|
-
|
|
1274
|
-
##
|
|
1275
|
-
# Time field that's not connected to a time-of-a-day dimension
|
|
1276
|
-
#
|
|
1277
|
-
class TimeAttribute < Attribute
|
|
1278
|
-
def type_prefix;
|
|
1279
|
-
TIME_ATTRIBUTE_PREFIX;
|
|
1280
|
-
end
|
|
1281
|
-
|
|
1282
|
-
def key;
|
|
1283
|
-
"#{TIME_COLUMN_PREFIX}#{super}";
|
|
1284
|
-
end
|
|
1285
|
-
|
|
1286
|
-
def table;
|
|
1287
|
-
@table ||= "#{super}_tm";
|
|
1288
|
-
end
|
|
1289
|
-
end
|
|
1290
|
-
|
|
1291
|
-
##
|
|
1292
|
-
# Date column. A container holding the following
|
|
1293
|
-
# parts: date fact, a date reference or attribute and an optional time component
|
|
1294
|
-
# that contains a time fact and a time reference or attribute.
|
|
1295
|
-
#
|
|
1296
|
-
class DateColumn < Column
|
|
1297
|
-
attr_reader :parts, :facts, :attributes, :references
|
|
1298
|
-
|
|
1299
|
-
def initialize(column, schema)
|
|
1300
|
-
super column, schema
|
|
1301
|
-
@parts = {}; @facts = []; @attributes = []; @references = []
|
|
1302
|
-
|
|
1303
|
-
# @facts << @parts[:date_fact] = DateFact.new(column, schema)
|
|
1304
|
-
if column[:dataset] then
|
|
1305
|
-
@parts[:date_ref] = DateReference.new column, schema
|
|
1306
|
-
@references << @parts[:date_ref]
|
|
1307
|
-
else
|
|
1308
|
-
@attributes << @parts[:date_attr] = DateAttribute.new(column, schema)
|
|
1309
|
-
end
|
|
1310
|
-
# if column['datetime'] then
|
|
1311
|
-
# puts "*** datetime"
|
|
1312
|
-
# @facts << @parts[:time_fact] = TimeFact.new(column, schema)
|
|
1313
|
-
# if column['schema_reference'] then
|
|
1314
|
-
# @parts[:time_ref] = TimeReference.new column, schema
|
|
1315
|
-
# else
|
|
1316
|
-
# @attributes << @parts[:time_attr] = TimeAttribute.new(column, schema)
|
|
1317
|
-
# end
|
|
1318
|
-
# end
|
|
1319
|
-
end
|
|
1320
|
-
|
|
1321
|
-
def to_maql_create
|
|
1322
|
-
@parts.values.map { |v| v.to_maql_create }.join "\n"
|
|
1323
|
-
end
|
|
1324
|
-
|
|
1325
|
-
def to_maql_drop
|
|
1326
|
-
@parts.values.map { |v| v.to_maql_drop }.join "\n"
|
|
1327
|
-
end
|
|
1328
|
-
|
|
1329
|
-
def to_csv_header(row)
|
|
1330
|
-
SKIP_FIELD
|
|
1331
|
-
end
|
|
1332
|
-
|
|
1333
|
-
def to_csv_data(headers, row)
|
|
1334
|
-
SKIP_FIELD
|
|
1335
|
-
end
|
|
1336
|
-
|
|
1337
|
-
def to_manifest_part(mode)
|
|
1338
|
-
nil
|
|
1339
|
-
end
|
|
1340
|
-
end
|
|
1341
|
-
|
|
1342
|
-
##
|
|
1343
|
-
# Base class for GoodData attribute and fact folder abstractions
|
|
1344
|
-
#
|
|
1345
|
-
class Folder < MdObject
|
|
1346
|
-
def initialize(title)
|
|
1347
|
-
# TODO: should a super be here?
|
|
1348
|
-
# how to deal with name vs title?
|
|
1349
|
-
@title = title
|
|
1350
|
-
@name = GoodData::Helpers.sanitize_string(title)
|
|
1351
|
-
end
|
|
1352
|
-
|
|
1353
|
-
def to_maql_create
|
|
1354
|
-
"CREATE FOLDER {#{type_prefix}.#{name}}" \
|
|
1355
|
-
+ " VISUAL (#{visual}) TYPE #{type};\n"
|
|
1356
|
-
end
|
|
1357
|
-
end
|
|
1358
|
-
|
|
1359
|
-
##
|
|
1360
|
-
# GoodData attribute folder abstraction
|
|
1361
|
-
#
|
|
1362
|
-
class AttributeFolder < Folder
|
|
1363
|
-
def type;
|
|
1364
|
-
'ATTRIBUTE'
|
|
1365
|
-
end
|
|
1366
|
-
|
|
1367
|
-
def type_prefix;
|
|
1368
|
-
'dim'
|
|
1369
|
-
end
|
|
1370
|
-
end
|
|
1371
|
-
|
|
1372
|
-
##
|
|
1373
|
-
# GoodData fact folder abstraction
|
|
1374
|
-
#
|
|
1375
|
-
class FactFolder < Folder
|
|
1376
|
-
def type;
|
|
1377
|
-
'FACT'
|
|
1378
|
-
end
|
|
1379
|
-
|
|
1380
|
-
def type_prefix;
|
|
1381
|
-
'ffld'
|
|
1382
|
-
end
|
|
1383
|
-
end
|
|
1384
|
-
|
|
1385
|
-
class DateDimension < MdObject
|
|
1386
|
-
def initialize(spec={})
|
|
1387
|
-
super()
|
|
1388
|
-
@name = spec[:name]
|
|
1389
|
-
@title = spec[:title] || @name
|
|
1390
|
-
@urn = spec[:urn] || 'URN:GOODDATA:DATE'
|
|
1391
|
-
end
|
|
1392
|
-
|
|
1393
|
-
def to_maql_create
|
|
1394
|
-
# urn = "urn:chefs_warehouse_fiscal:date"
|
|
1395
|
-
# title = "title"
|
|
1396
|
-
# name = "name"
|
|
1397
|
-
|
|
1398
|
-
maql = ''
|
|
1399
|
-
maql += "INCLUDE TEMPLATE \"#{@urn}\" MODIFY (IDENTIFIER \"#{@name}\", TITLE \"#{@title}\");"
|
|
1400
|
-
maql
|
|
1401
|
-
end
|
|
1402
|
-
end
|
|
1403
102
|
end
|
|
1404
|
-
end
|
|
103
|
+
end
|