mondrian-olap 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Changelog.md +38 -0
- data/LICENSE.txt +1 -1
- data/README.md +302 -0
- data/VERSION +1 -1
- data/lib/mondrian/jars/commons-collections-3.2.jar +0 -0
- data/lib/mondrian/jars/commons-logging-1.1.1.jar +0 -0
- data/lib/mondrian/jars/commons-math-1.1.jar +0 -0
- data/lib/mondrian/jars/eigenbase-properties-1.1.2.jar +0 -0
- data/lib/mondrian/jars/eigenbase-resgen-1.3.1.jar +0 -0
- data/lib/mondrian/jars/eigenbase-xom-1.3.1.jar +0 -0
- data/lib/mondrian/jars/{javacup.jar → javacup-10k.jar} +0 -0
- data/lib/mondrian/jars/log4j-1.2.14.jar +0 -0
- data/lib/mondrian/jars/mondrian.jar +0 -0
- data/lib/mondrian/jars/olap4j-1.0.1.539.jar +0 -0
- data/lib/mondrian/olap.rb +2 -1
- data/lib/mondrian/olap/connection.rb +163 -32
- data/lib/mondrian/olap/cube.rb +163 -24
- data/lib/mondrian/olap/error.rb +57 -0
- data/lib/mondrian/olap/query.rb +52 -17
- data/lib/mondrian/olap/result.rb +298 -6
- data/lib/mondrian/olap/schema.rb +220 -29
- data/lib/mondrian/olap/schema_element.rb +31 -11
- data/lib/mondrian/olap/schema_udf.rb +331 -0
- data/lib/mondrian/olap/version.rb +5 -0
- data/spec/connection_role_spec.rb +130 -0
- data/spec/connection_spec.rb +36 -1
- data/spec/cube_spec.rb +137 -7
- data/spec/fixtures/MondrianTest.xml +4 -4
- data/spec/mondrian_spec.rb +53 -0
- data/spec/query_spec.rb +294 -11
- data/spec/rake_tasks.rb +8 -8
- data/spec/schema_definition_spec.rb +845 -26
- data/spec/spec_helper.rb +26 -17
- data/spec/support/matchers/be_like.rb +2 -2
- metadata +296 -237
- data/.rspec +0 -2
- data/Gemfile +0 -18
- data/README.rdoc +0 -221
- data/RUNNING_TESTS.rdoc +0 -66
- data/Rakefile +0 -46
- data/lib/mondrian/jars/commons-collections-3.1.jar +0 -0
- data/lib/mondrian/jars/commons-logging-1.0.4.jar +0 -0
- data/lib/mondrian/jars/commons-math-1.0.jar +0 -0
- data/lib/mondrian/jars/eigenbase-properties.jar +0 -0
- data/lib/mondrian/jars/eigenbase-resgen.jar +0 -0
- data/lib/mondrian/jars/eigenbase-xom.jar +0 -0
- data/lib/mondrian/jars/log4j-1.2.8.jar +0 -0
- data/lib/mondrian/jars/olap4j.jar +0 -0
- data/mondrian-olap.gemspec +0 -126
data/lib/mondrian/olap/cube.rb
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
module Mondrian
|
2
2
|
module OLAP
|
3
|
+
module Annotated
|
4
|
+
private
|
5
|
+
|
6
|
+
def annotations_for(raw_element)
|
7
|
+
@annotations ||= begin
|
8
|
+
annotated = raw_element.unwrap(Java::MondrianOlap::Annotated.java_class)
|
9
|
+
annotations_hash = annotated.getAnnotationMap.to_hash
|
10
|
+
annotations_hash.each do |key, annotation|
|
11
|
+
annotations_hash[key] = annotation.getValue
|
12
|
+
end
|
13
|
+
annotations_hash
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
3
18
|
class Cube
|
4
19
|
def self.get(connection, name)
|
5
20
|
if raw_cube = connection.raw_schema.getCubes.get(name)
|
@@ -12,10 +27,25 @@ module Mondrian
|
|
12
27
|
@raw_cube = raw_cube
|
13
28
|
end
|
14
29
|
|
30
|
+
attr_reader :raw_cube
|
31
|
+
|
15
32
|
def name
|
16
33
|
@name ||= @raw_cube.getName
|
17
34
|
end
|
18
35
|
|
36
|
+
def description
|
37
|
+
@description ||= @raw_cube.getDescription
|
38
|
+
end
|
39
|
+
|
40
|
+
def caption
|
41
|
+
@caption ||= @raw_cube.getCaption
|
42
|
+
end
|
43
|
+
|
44
|
+
include Annotated
|
45
|
+
def annotations
|
46
|
+
annotations_for(@raw_cube)
|
47
|
+
end
|
48
|
+
|
19
49
|
def dimensions
|
20
50
|
@dimenstions ||= @raw_cube.getDimensions.map{|d| Dimension.new(self, d)}
|
21
51
|
end
|
@@ -51,12 +81,20 @@ module Mondrian
|
|
51
81
|
@raw_dimension = raw_dimension
|
52
82
|
end
|
53
83
|
|
54
|
-
attr_reader :cube
|
84
|
+
attr_reader :cube, :raw_dimension
|
55
85
|
|
56
86
|
def name
|
57
87
|
@name ||= @raw_dimension.getName
|
58
88
|
end
|
59
89
|
|
90
|
+
def description
|
91
|
+
@description ||= @raw_dimension.getDescription
|
92
|
+
end
|
93
|
+
|
94
|
+
def caption
|
95
|
+
@caption ||= @raw_dimension.getCaption
|
96
|
+
end
|
97
|
+
|
60
98
|
def full_name
|
61
99
|
@full_name ||= @raw_dimension.getUniqueName
|
62
100
|
end
|
@@ -88,6 +126,12 @@ module Mondrian
|
|
88
126
|
:standard
|
89
127
|
end
|
90
128
|
end
|
129
|
+
|
130
|
+
include Annotated
|
131
|
+
def annotations
|
132
|
+
annotations_for(@raw_dimension)
|
133
|
+
end
|
134
|
+
|
91
135
|
end
|
92
136
|
|
93
137
|
class Hierarchy
|
@@ -96,10 +140,20 @@ module Mondrian
|
|
96
140
|
@raw_hierarchy = raw_hierarchy
|
97
141
|
end
|
98
142
|
|
143
|
+
attr_reader :raw_hierarchy
|
144
|
+
|
99
145
|
def name
|
100
146
|
@name ||= @raw_hierarchy.getName
|
101
147
|
end
|
102
148
|
|
149
|
+
def description
|
150
|
+
@description ||= @raw_hierarchy.getDescription
|
151
|
+
end
|
152
|
+
|
153
|
+
def caption
|
154
|
+
@caption ||= @raw_hierarchy.getCaption
|
155
|
+
end
|
156
|
+
|
103
157
|
def levels
|
104
158
|
@levels = @raw_hierarchy.getLevels.map{|l| Level.new(self, l)}
|
105
159
|
end
|
@@ -137,14 +191,22 @@ module Mondrian
|
|
137
191
|
end
|
138
192
|
|
139
193
|
def child_names(*parent_member_segment_names)
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
194
|
+
Error.wrap_native_exception do
|
195
|
+
parent_member = if parent_member_segment_names.empty?
|
196
|
+
return root_member_names unless has_all?
|
197
|
+
all_member
|
198
|
+
else
|
199
|
+
@dimension.cube.member_by_segments(*parent_member_segment_names)
|
200
|
+
end
|
201
|
+
parent_member && parent_member.children.map{|m| m.name}
|
145
202
|
end
|
146
|
-
parent_member && parent_member.children.map{|m| m.name}
|
147
203
|
end
|
204
|
+
|
205
|
+
include Annotated
|
206
|
+
def annotations
|
207
|
+
annotations_for(@raw_hierarchy)
|
208
|
+
end
|
209
|
+
|
148
210
|
end
|
149
211
|
|
150
212
|
class Level
|
@@ -153,10 +215,20 @@ module Mondrian
|
|
153
215
|
@raw_level = raw_level
|
154
216
|
end
|
155
217
|
|
218
|
+
attr_reader :raw_level
|
219
|
+
|
156
220
|
def name
|
157
221
|
@name ||= @raw_level.getName
|
158
222
|
end
|
159
223
|
|
224
|
+
def description
|
225
|
+
@description ||= @raw_level.getDescription
|
226
|
+
end
|
227
|
+
|
228
|
+
def caption
|
229
|
+
@caption ||= @raw_level.getCaption
|
230
|
+
end
|
231
|
+
|
160
232
|
def depth
|
161
233
|
@raw_level.getDepth
|
162
234
|
end
|
@@ -170,14 +242,24 @@ module Mondrian
|
|
170
242
|
if cardinality >= 0
|
171
243
|
cardinality
|
172
244
|
else
|
173
|
-
|
245
|
+
Error.wrap_native_exception do
|
246
|
+
@raw_level.getMembers.size
|
247
|
+
end
|
174
248
|
end
|
175
249
|
end
|
176
250
|
end
|
177
251
|
|
178
252
|
def members
|
179
|
-
|
253
|
+
Error.wrap_native_exception do
|
254
|
+
@raw_level.getMembers.map{|m| Member.new(m)}
|
255
|
+
end
|
180
256
|
end
|
257
|
+
|
258
|
+
include Annotated
|
259
|
+
def annotations
|
260
|
+
annotations_for(@raw_level)
|
261
|
+
end
|
262
|
+
|
181
263
|
end
|
182
264
|
|
183
265
|
class Member
|
@@ -185,18 +267,32 @@ module Mondrian
|
|
185
267
|
@raw_member = raw_member
|
186
268
|
end
|
187
269
|
|
270
|
+
attr_reader :raw_member
|
271
|
+
|
188
272
|
def name
|
189
|
-
@raw_member.getName
|
273
|
+
@name ||= @raw_member.getName
|
190
274
|
end
|
191
275
|
|
192
276
|
def full_name
|
193
|
-
@raw_member.getUniqueName
|
277
|
+
@full_name ||= @raw_member.getUniqueName
|
278
|
+
end
|
279
|
+
|
280
|
+
def caption
|
281
|
+
@caption ||= @raw_member.getCaption
|
194
282
|
end
|
195
283
|
|
196
284
|
def calculated?
|
197
285
|
@raw_member.isCalculated
|
198
286
|
end
|
199
287
|
|
288
|
+
def calculated_in_query?
|
289
|
+
@raw_member.isCalculatedInQuery
|
290
|
+
end
|
291
|
+
|
292
|
+
def visible?
|
293
|
+
@raw_member.isVisible
|
294
|
+
end
|
295
|
+
|
200
296
|
def all_member?
|
201
297
|
@raw_member.isAll
|
202
298
|
end
|
@@ -226,26 +322,69 @@ module Mondrian
|
|
226
322
|
end
|
227
323
|
|
228
324
|
def children
|
229
|
-
|
325
|
+
Error.wrap_native_exception do
|
326
|
+
@raw_member.getChildMembers.map{|m| Member.new(m)}
|
327
|
+
end
|
230
328
|
end
|
231
329
|
|
232
330
|
def descendants_at_level(level)
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
331
|
+
Error.wrap_native_exception do
|
332
|
+
raw_level = @raw_member.getLevel
|
333
|
+
raw_levels = raw_level.getHierarchy.getLevels
|
334
|
+
current_level_index = raw_levels.indexOf(raw_level)
|
335
|
+
descendants_level_index = raw_levels.indexOfName(level)
|
336
|
+
|
337
|
+
return nil unless descendants_level_index > current_level_index
|
338
|
+
|
339
|
+
members = [self]
|
340
|
+
(descendants_level_index - current_level_index).times do
|
341
|
+
members = members.map do |member|
|
342
|
+
member.children
|
343
|
+
end.flatten
|
344
|
+
end
|
345
|
+
members
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def property_value(name)
|
350
|
+
if property = @raw_member.getProperties.get(name)
|
351
|
+
@raw_member.getPropertyValue(property)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def property_formatted_value(name)
|
356
|
+
if property = @raw_member.getProperties.get(name)
|
357
|
+
@raw_member.getPropertyFormattedValue(property)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
include Annotated
|
362
|
+
def annotations
|
363
|
+
annotations_for(@raw_member)
|
364
|
+
end
|
237
365
|
|
238
|
-
|
366
|
+
def format_string
|
367
|
+
format_exp = property_value('FORMAT_EXP')
|
368
|
+
if format_exp && format_exp =~ /\A"(.*)"\z/
|
369
|
+
format_exp = $1
|
370
|
+
end
|
371
|
+
if format_exp && !format_exp.empty?
|
372
|
+
format_exp
|
373
|
+
end
|
374
|
+
end
|
239
375
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
376
|
+
def cell_formatter_name
|
377
|
+
if dimension_type == :measures
|
378
|
+
cube_measure = raw_member.unwrap(Java::MondrianOlap::Member.java_class)
|
379
|
+
if value_formatter = cube_measure.getFormatter
|
380
|
+
f = value_formatter.java_class.declared_field('cf')
|
381
|
+
f.accessible = true
|
382
|
+
cf = f.value(value_formatter)
|
383
|
+
cf.class.name.split('::').last.gsub(/Udf\z/, '')
|
384
|
+
end
|
245
385
|
end
|
246
|
-
members
|
247
386
|
end
|
248
387
|
|
249
388
|
end
|
250
389
|
end
|
251
|
-
end
|
390
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Mondrian
|
2
|
+
module OLAP
|
3
|
+
|
4
|
+
NATIVE_ERROR_REGEXP = /^(org\.olap4j\.|mondrian\.|java\.lang\.reflect\.UndeclaredThrowableException\: Mondrian Error\:)/
|
5
|
+
|
6
|
+
class Error < StandardError
|
7
|
+
# root_cause will be nil if there is no cause for wrapped native error
|
8
|
+
# root_cause_message will have either root_cause message or wrapped native error message
|
9
|
+
attr_reader :native_error, :root_cause_message, :root_cause
|
10
|
+
|
11
|
+
def initialize(native_error)
|
12
|
+
@native_error = native_error
|
13
|
+
get_root_cause
|
14
|
+
super(native_error.message)
|
15
|
+
add_root_cause_to_backtrace
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.wrap_native_exception
|
19
|
+
yield
|
20
|
+
rescue NativeException => e
|
21
|
+
if e.message =~ NATIVE_ERROR_REGEXP
|
22
|
+
raise Mondrian::OLAP::Error.new(e)
|
23
|
+
else
|
24
|
+
raise
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def get_root_cause
|
31
|
+
@root_cause = nil
|
32
|
+
e = @native_error
|
33
|
+
while e.respond_to?(:cause) && (cause = e.cause)
|
34
|
+
@root_cause = e = cause
|
35
|
+
end
|
36
|
+
message = e.message
|
37
|
+
if message =~ /\AMondrian Error:(.*)\Z/m
|
38
|
+
message = $1
|
39
|
+
end
|
40
|
+
@root_cause_message = message
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_root_cause_to_backtrace
|
44
|
+
bt = @native_error.backtrace
|
45
|
+
if @root_cause
|
46
|
+
root_cause_bt = Array(@root_cause.backtrace)
|
47
|
+
root_cause_bt[0,5].reverse.each do |bt_line|
|
48
|
+
bt.unshift "root cause: #{bt_line}"
|
49
|
+
end
|
50
|
+
bt.unshift "root cause: #{@root_cause.java_class.name}: #{@root_cause.message.chomp}"
|
51
|
+
end
|
52
|
+
set_backtrace bt
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/mondrian/olap/query.rb
CHANGED
@@ -43,19 +43,23 @@ module Mondrian
|
|
43
43
|
RUBY
|
44
44
|
end
|
45
45
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
46
|
+
%w(crossjoin nonempty_crossjoin).each do |method|
|
47
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
48
|
+
def #{method}(*axis_members)
|
49
|
+
raise ArgumentError, "cannot use #{method} method before axis or with_set method" unless @current_set
|
50
|
+
raise ArgumentError, "specify list of members for #{method} method" if axis_members.empty?
|
51
|
+
members = axis_members.length == 1 && axis_members[0].is_a?(Array) ? axis_members[0] : axis_members
|
52
|
+
@current_set.replace [:#{method}, @current_set.clone, members]
|
53
|
+
self
|
54
|
+
end
|
55
|
+
RUBY
|
52
56
|
end
|
53
57
|
|
54
58
|
def except(*axis_members)
|
55
59
|
raise ArgumentError, "cannot use except method before axis or with_set method" unless @current_set
|
56
60
|
raise ArgumentError, "specify list of members for except method" if axis_members.empty?
|
57
61
|
members = axis_members.length == 1 && axis_members[0].is_a?(Array) ? axis_members[0] : axis_members
|
58
|
-
if @current_set[0]
|
62
|
+
if [:crossjoin, :nonempty_crossjoin].include? @current_set[0]
|
59
63
|
@current_set[2] = [:except, @current_set[2], members]
|
60
64
|
else
|
61
65
|
@current_set.replace [:except, @current_set.clone, members]
|
@@ -119,7 +123,7 @@ module Mondrian
|
|
119
123
|
raise ArgumentError, "cannot use hierarchize method before axis or with_set method" unless @current_set
|
120
124
|
order = order && order.to_s.upcase
|
121
125
|
raise ArgumentError, "invalid hierarchize order #{order.inspect}" unless order.nil? || order == 'POST'
|
122
|
-
if all.nil? && @current_set[0]
|
126
|
+
if all.nil? && [:crossjoin, :nonempty_crossjoin].include?(@current_set[0])
|
123
127
|
@current_set[2] = [:hierarchize, @current_set[2]]
|
124
128
|
@current_set[2] << order if order
|
125
129
|
else
|
@@ -185,7 +189,15 @@ module Mondrian
|
|
185
189
|
# definition of calculated member
|
186
190
|
else
|
187
191
|
member_definition = @with.last
|
188
|
-
|
192
|
+
if params.last.is_a?(Hash)
|
193
|
+
options = params.pop
|
194
|
+
# if formatter does not include . then it should be ruby formatter name
|
195
|
+
if (formatter = options[:cell_formatter]) && !formatter.include?('.')
|
196
|
+
options = options.merge(:cell_formatter => Mondrian::OLAP::Schema::CellFormatter.new(formatter).class_name)
|
197
|
+
end
|
198
|
+
else
|
199
|
+
options = nil
|
200
|
+
end
|
189
201
|
raise ArgumentError, "cannot use 'as' method before with_member method" unless member_definition &&
|
190
202
|
member_definition[0] == :member && member_definition.length == 2
|
191
203
|
raise ArgumentError, "calculated member definition should be single expression" unless params.length == 1
|
@@ -205,7 +217,19 @@ module Mondrian
|
|
205
217
|
end
|
206
218
|
|
207
219
|
def execute
|
208
|
-
|
220
|
+
Error.wrap_native_exception do
|
221
|
+
@connection.execute to_mdx
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def execute_drill_through(options = {})
|
226
|
+
Error.wrap_native_exception do
|
227
|
+
drill_through_mdx = "DRILLTHROUGH "
|
228
|
+
drill_through_mdx << "MAXROWS #{options[:max_rows]} " if options[:max_rows]
|
229
|
+
drill_through_mdx << to_mdx
|
230
|
+
drill_through_mdx << " RETURN #{Array(options[:return]).join(',')}" if options[:return]
|
231
|
+
@connection.execute_drill_through drill_through_mdx
|
232
|
+
end
|
209
233
|
end
|
210
234
|
|
211
235
|
private
|
@@ -220,7 +244,13 @@ module Mondrian
|
|
220
244
|
options = definition[3]
|
221
245
|
options_string = ''
|
222
246
|
options && options.each do |option, value|
|
223
|
-
|
247
|
+
option_name = case option
|
248
|
+
when :caption
|
249
|
+
'$caption'
|
250
|
+
else
|
251
|
+
option.to_s.upcase
|
252
|
+
end
|
253
|
+
options_string << ", #{option_name} = #{quote_value(value)}"
|
224
254
|
end
|
225
255
|
"MEMBER #{member_name} AS #{quote_value(expression)}#{options_string}"
|
226
256
|
when :set
|
@@ -251,12 +281,17 @@ module Mondrian
|
|
251
281
|
}
|
252
282
|
|
253
283
|
def members_to_mdx(members)
|
254
|
-
if
|
284
|
+
# if only one member which does not end with ]
|
285
|
+
# then assume it is expression which returns set
|
286
|
+
# TODO: maybe always include also single expressions in {...} to avoid some edge cases?
|
287
|
+
if members.length == 1 && members[0][-1,1] != ']'
|
255
288
|
members[0]
|
256
289
|
elsif members[0].is_a?(Symbol)
|
257
290
|
case members[0]
|
258
291
|
when :crossjoin
|
259
292
|
"CROSSJOIN(#{members_to_mdx(members[1])}, #{members_to_mdx(members[2])})"
|
293
|
+
when :nonempty_crossjoin
|
294
|
+
"NONEMPTYCROSSJOIN(#{members_to_mdx(members[1])}, #{members_to_mdx(members[2])})"
|
260
295
|
when :except
|
261
296
|
"EXCEPT(#{members_to_mdx(members[1])}, #{members_to_mdx(members[2])})"
|
262
297
|
when :nonempty
|
@@ -294,13 +329,13 @@ module Mondrian
|
|
294
329
|
if @where[0].is_a?(Symbol) ||
|
295
330
|
@where.length > 1 && @where.map{|full_name| extract_dimension_name(full_name)}.uniq.length == 1
|
296
331
|
members_to_mdx(@where)
|
297
|
-
# generate
|
332
|
+
# generate tuple MDX expression
|
298
333
|
else
|
299
|
-
|
334
|
+
where_to_mdx_tuple
|
300
335
|
end
|
301
336
|
end
|
302
337
|
|
303
|
-
def
|
338
|
+
def where_to_mdx_tuple
|
304
339
|
mdx = '('
|
305
340
|
mdx << @where.map do |condition|
|
306
341
|
condition
|
@@ -322,10 +357,10 @@ module Mondrian
|
|
322
357
|
end
|
323
358
|
|
324
359
|
def extract_dimension_name(full_name)
|
325
|
-
if full_name =~ /\A
|
360
|
+
if full_name =~ /\A[^\[]*\[([^\]]+)\]/
|
326
361
|
$1
|
327
362
|
end
|
328
363
|
end
|
329
364
|
end
|
330
365
|
end
|
331
|
-
end
|
366
|
+
end
|