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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/Changelog.md +38 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +302 -0
  5. data/VERSION +1 -1
  6. data/lib/mondrian/jars/commons-collections-3.2.jar +0 -0
  7. data/lib/mondrian/jars/commons-logging-1.1.1.jar +0 -0
  8. data/lib/mondrian/jars/commons-math-1.1.jar +0 -0
  9. data/lib/mondrian/jars/eigenbase-properties-1.1.2.jar +0 -0
  10. data/lib/mondrian/jars/eigenbase-resgen-1.3.1.jar +0 -0
  11. data/lib/mondrian/jars/eigenbase-xom-1.3.1.jar +0 -0
  12. data/lib/mondrian/jars/{javacup.jar → javacup-10k.jar} +0 -0
  13. data/lib/mondrian/jars/log4j-1.2.14.jar +0 -0
  14. data/lib/mondrian/jars/mondrian.jar +0 -0
  15. data/lib/mondrian/jars/olap4j-1.0.1.539.jar +0 -0
  16. data/lib/mondrian/olap.rb +2 -1
  17. data/lib/mondrian/olap/connection.rb +163 -32
  18. data/lib/mondrian/olap/cube.rb +163 -24
  19. data/lib/mondrian/olap/error.rb +57 -0
  20. data/lib/mondrian/olap/query.rb +52 -17
  21. data/lib/mondrian/olap/result.rb +298 -6
  22. data/lib/mondrian/olap/schema.rb +220 -29
  23. data/lib/mondrian/olap/schema_element.rb +31 -11
  24. data/lib/mondrian/olap/schema_udf.rb +331 -0
  25. data/lib/mondrian/olap/version.rb +5 -0
  26. data/spec/connection_role_spec.rb +130 -0
  27. data/spec/connection_spec.rb +36 -1
  28. data/spec/cube_spec.rb +137 -7
  29. data/spec/fixtures/MondrianTest.xml +4 -4
  30. data/spec/mondrian_spec.rb +53 -0
  31. data/spec/query_spec.rb +294 -11
  32. data/spec/rake_tasks.rb +8 -8
  33. data/spec/schema_definition_spec.rb +845 -26
  34. data/spec/spec_helper.rb +26 -17
  35. data/spec/support/matchers/be_like.rb +2 -2
  36. metadata +296 -237
  37. data/.rspec +0 -2
  38. data/Gemfile +0 -18
  39. data/README.rdoc +0 -221
  40. data/RUNNING_TESTS.rdoc +0 -66
  41. data/Rakefile +0 -46
  42. data/lib/mondrian/jars/commons-collections-3.1.jar +0 -0
  43. data/lib/mondrian/jars/commons-logging-1.0.4.jar +0 -0
  44. data/lib/mondrian/jars/commons-math-1.0.jar +0 -0
  45. data/lib/mondrian/jars/eigenbase-properties.jar +0 -0
  46. data/lib/mondrian/jars/eigenbase-resgen.jar +0 -0
  47. data/lib/mondrian/jars/eigenbase-xom.jar +0 -0
  48. data/lib/mondrian/jars/log4j-1.2.8.jar +0 -0
  49. data/lib/mondrian/jars/olap4j.jar +0 -0
  50. data/mondrian-olap.gemspec +0 -126
@@ -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
- parent_member = if parent_member_segment_names.empty?
141
- return root_member_names unless has_all?
142
- all_member
143
- else
144
- @dimension.cube.member_by_segments(*parent_member_segment_names)
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
- @raw_level.getMembers.size
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
- @raw_level.getMembers.map{|m| Member.new(m)}
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
- @raw_member.getChildMembers.map{|m| Member.new(m)}
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
- raw_level = @raw_member.getLevel
234
- raw_levels = raw_level.getHierarchy.getLevels
235
- current_level_index = raw_levels.indexOf(raw_level)
236
- descendants_level_index = raw_levels.indexOfName(level)
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
- return nil unless descendants_level_index > current_level_index
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
- members = [self]
241
- (descendants_level_index - current_level_index).times do
242
- members = members.map do |member|
243
- member.children
244
- end.flatten
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
@@ -43,19 +43,23 @@ module Mondrian
43
43
  RUBY
44
44
  end
45
45
 
46
- def crossjoin(*axis_members)
47
- raise ArgumentError, "cannot use crossjoin method before axis or with_set method" unless @current_set
48
- raise ArgumentError, "specify list of members for crossjoin method" if axis_members.empty?
49
- members = axis_members.length == 1 && axis_members[0].is_a?(Array) ? axis_members[0] : axis_members
50
- @current_set.replace [:crossjoin, @current_set.clone, members]
51
- self
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] == :crossjoin
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] == :crossjoin
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
- options = params.last.is_a?(Hash) ? params.pop : nil
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
- @connection.execute to_mdx
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
- options_string << ", #{option.to_s.upcase} = #{quote_value(value)}"
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 members.length == 1
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 tupple MDX expression
332
+ # generate tuple MDX expression
298
333
  else
299
- where_to_mdx_tupple
334
+ where_to_mdx_tuple
300
335
  end
301
336
  end
302
337
 
303
- def where_to_mdx_tupple
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