mondrian-olap 0.3.0 → 0.5.0
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 +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
|