mondrian-olap 0.5.0 → 1.2.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 (54) hide show
  1. checksums.yaml +5 -5
  2. data/Changelog.md +86 -0
  3. data/LICENSE-Mondrian.txt +87 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +43 -40
  6. data/VERSION +1 -1
  7. data/lib/mondrian/jars/commons-collections-3.2.2.jar +0 -0
  8. data/lib/mondrian/jars/commons-dbcp-1.4.jar +0 -0
  9. data/lib/mondrian/jars/commons-io-2.2.jar +0 -0
  10. data/lib/mondrian/jars/commons-lang-2.6.jar +0 -0
  11. data/lib/mondrian/jars/commons-logging-1.2.jar +0 -0
  12. data/lib/mondrian/jars/commons-pool-1.5.7.jar +0 -0
  13. data/lib/mondrian/jars/commons-vfs2-2.2.jar +0 -0
  14. data/lib/mondrian/jars/eigenbase-xom-1.3.5.jar +0 -0
  15. data/lib/mondrian/jars/guava-17.0.jar +0 -0
  16. data/lib/mondrian/jars/log4j-1.2.17.jar +0 -0
  17. data/lib/mondrian/jars/log4j.properties +2 -4
  18. data/lib/mondrian/jars/mondrian-9.1.0.0.jar +0 -0
  19. data/lib/mondrian/jars/olap4j-1.2.0.jar +0 -0
  20. data/lib/mondrian/olap/connection.rb +252 -67
  21. data/lib/mondrian/olap/cube.rb +63 -2
  22. data/lib/mondrian/olap/error.rb +37 -8
  23. data/lib/mondrian/olap/query.rb +41 -21
  24. data/lib/mondrian/olap/result.rb +163 -44
  25. data/lib/mondrian/olap/schema.rb +42 -3
  26. data/lib/mondrian/olap/schema_element.rb +25 -6
  27. data/lib/mondrian/olap/schema_udf.rb +21 -16
  28. data/spec/connection_role_spec.rb +69 -13
  29. data/spec/connection_spec.rb +3 -2
  30. data/spec/cube_cache_control_spec.rb +261 -0
  31. data/spec/cube_spec.rb +32 -4
  32. data/spec/fixtures/MondrianTest.xml +1 -6
  33. data/spec/fixtures/MondrianTestOracle.xml +1 -6
  34. data/spec/mondrian_spec.rb +71 -1
  35. data/spec/query_spec.rb +323 -25
  36. data/spec/rake_tasks.rb +253 -159
  37. data/spec/schema_definition_spec.rb +314 -61
  38. data/spec/spec_helper.rb +115 -45
  39. data/spec/support/data/customers.csv +10902 -0
  40. data/spec/support/data/product_classes.csv +101 -0
  41. data/spec/support/data/products.csv +101 -0
  42. data/spec/support/data/sales.csv +101 -0
  43. data/spec/support/data/time.csv +731 -0
  44. metadata +126 -124
  45. data/LICENSE-Mondrian.html +0 -259
  46. data/lib/mondrian/jars/commons-collections-3.2.jar +0 -0
  47. data/lib/mondrian/jars/commons-dbcp-1.2.1.jar +0 -0
  48. data/lib/mondrian/jars/commons-logging-1.1.1.jar +0 -0
  49. data/lib/mondrian/jars/commons-pool-1.2.jar +0 -0
  50. data/lib/mondrian/jars/commons-vfs-1.0.jar +0 -0
  51. data/lib/mondrian/jars/eigenbase-xom-1.3.1.jar +0 -0
  52. data/lib/mondrian/jars/log4j-1.2.14.jar +0 -0
  53. data/lib/mondrian/jars/mondrian.jar +0 -0
  54. data/lib/mondrian/jars/olap4j-1.0.1.539.jar +0 -0
@@ -1,3 +1,5 @@
1
+ require 'forwardable'
2
+
1
3
  module Mondrian
2
4
  module OLAP
3
5
  module Annotated
@@ -16,6 +18,8 @@ module Mondrian
16
18
  end
17
19
 
18
20
  class Cube
21
+ extend Forwardable
22
+
19
23
  def self.get(connection, name)
20
24
  if raw_cube = connection.raw_schema.getCubes.get(name)
21
25
  Cube.new(connection, raw_cube)
@@ -25,6 +29,7 @@ module Mondrian
25
29
  def initialize(connection, raw_cube)
26
30
  @connection = connection
27
31
  @raw_cube = raw_cube
32
+ @cache_control = CacheControl.new(@connection, self)
28
33
  end
29
34
 
30
35
  attr_reader :raw_cube
@@ -46,6 +51,10 @@ module Mondrian
46
51
  annotations_for(@raw_cube)
47
52
  end
48
53
 
54
+ def visible?
55
+ @raw_cube.isVisible
56
+ end
57
+
49
58
  def dimensions
50
59
  @dimenstions ||= @raw_cube.getDimensions.map{|d| Dimension.new(self, d)}
51
60
  end
@@ -73,6 +82,9 @@ module Mondrian
73
82
  raw_member = @raw_cube.lookupMember(segment_list)
74
83
  raw_member && Member.new(raw_member)
75
84
  end
85
+
86
+ def_delegators :@cache_control, :flush_region_cache_with_segments, :flush_region_cache_with_segments
87
+ def_delegators :@cache_control, :flush_region_cache_with_full_names, :flush_region_cache_with_full_names
76
88
  end
77
89
 
78
90
  class Dimension
@@ -132,6 +144,10 @@ module Mondrian
132
144
  annotations_for(@raw_dimension)
133
145
  end
134
146
 
147
+ def visible?
148
+ @raw_dimension.isVisible
149
+ end
150
+
135
151
  end
136
152
 
137
153
  class Hierarchy
@@ -207,6 +223,10 @@ module Mondrian
207
223
  annotations_for(@raw_hierarchy)
208
224
  end
209
225
 
226
+ def visible?
227
+ @raw_hierarchy.isVisible
228
+ end
229
+
210
230
  end
211
231
 
212
232
  class Level
@@ -260,6 +280,10 @@ module Mondrian
260
280
  annotations_for(@raw_level)
261
281
  end
262
282
 
283
+ def visible?
284
+ @raw_level.isVisible
285
+ end
286
+
263
287
  end
264
288
 
265
289
  class Member
@@ -358,6 +382,10 @@ module Mondrian
358
382
  end
359
383
  end
360
384
 
385
+ def mondrian_member
386
+ @raw_member.unwrap(Java::MondrianOlap::Member.java_class)
387
+ end
388
+
361
389
  include Annotated
362
390
  def annotations
363
391
  annotations_for(@raw_member)
@@ -374,17 +402,50 @@ module Mondrian
374
402
  end
375
403
 
376
404
  def cell_formatter_name
405
+ if cf = cell_formatter
406
+ cf.class.name.split('::').last.gsub(/Udf\z/, '')
407
+ end
408
+ end
409
+
410
+ def cell_formatter
377
411
  if dimension_type == :measures
378
412
  cube_measure = raw_member.unwrap(Java::MondrianOlap::Member.java_class)
379
413
  if value_formatter = cube_measure.getFormatter
380
414
  f = value_formatter.java_class.declared_field('cf')
381
415
  f.accessible = true
382
- cf = f.value(value_formatter)
383
- cf.class.name.split('::').last.gsub(/Udf\z/, '')
416
+ f.value(value_formatter)
384
417
  end
385
418
  end
386
419
  end
420
+ end
421
+
422
+ class CacheControl
423
+ def initialize(connection, cube)
424
+ @connection = connection
425
+ @cube = cube
426
+ @mondrian_cube = @cube.raw_cube.unwrap(Java::MondrianOlap::Cube.java_class)
427
+ @cache_control = @connection.raw_cache_control
428
+ end
387
429
 
430
+ def flush_region_cache_with_segments(*segment_names)
431
+ members = segment_names.map { |name| @cube.member_by_segments(*name).mondrian_member }
432
+ flush(members)
433
+ end
434
+
435
+ def flush_region_cache_with_full_names(*full_names)
436
+ members = full_names.map { |name| @cube.member(*name).mondrian_member }
437
+ flush(members)
438
+ end
439
+
440
+ private
441
+
442
+ def flush(members)
443
+ regions = members.map do |member|
444
+ @cache_control.create_member_region(member, true)
445
+ end
446
+ regions << @cache_control.create_measures_region(@mondrian_cube)
447
+ @cache_control.flush(@cache_control.create_crossjoin_region(*regions))
448
+ end
388
449
  end
389
450
  end
390
451
  end
@@ -6,25 +6,43 @@ module Mondrian
6
6
  class Error < StandardError
7
7
  # root_cause will be nil if there is no cause for wrapped native error
8
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
9
+ attr_reader :native_error, :root_cause_message, :root_cause, :profiling_handler
10
10
 
11
- def initialize(native_error)
11
+ def initialize(native_error, options = {})
12
12
  @native_error = native_error
13
13
  get_root_cause
14
- super(native_error.message)
14
+ super(native_error.toString)
15
15
  add_root_cause_to_backtrace
16
+ get_profiling(options)
16
17
  end
17
18
 
18
- def self.wrap_native_exception
19
+ def self.wrap_native_exception(options = {})
19
20
  yield
20
- rescue NativeException => e
21
- if e.message =~ NATIVE_ERROR_REGEXP
22
- raise Mondrian::OLAP::Error.new(e)
21
+ # TokenMgrError for some unknown reason extends java.lang.Error which normally should not be rescued
22
+ rescue Java::JavaLang::Exception, Java::MondrianParser::TokenMgrError => e
23
+ if e.toString =~ NATIVE_ERROR_REGEXP
24
+ raise Mondrian::OLAP::Error.new(e, options)
23
25
  else
24
26
  raise
25
27
  end
26
28
  end
27
29
 
30
+ def profiling_plan
31
+ if profiling_handler && (plan = profiling_handler.plan)
32
+ plan.gsub("\r\n", "\n")
33
+ end
34
+ end
35
+
36
+ def profiling_timing
37
+ profiling_handler.timing if profiling_handler
38
+ end
39
+
40
+ def profiling_timing_string
41
+ if profiling_timing && (timing_string = profiling_timing.toString)
42
+ timing_string.gsub("\r\n", "\n")
43
+ end
44
+ end
45
+
28
46
  private
29
47
 
30
48
  def get_root_cause
@@ -44,7 +62,7 @@ module Mondrian
44
62
  bt = @native_error.backtrace
45
63
  if @root_cause
46
64
  root_cause_bt = Array(@root_cause.backtrace)
47
- root_cause_bt[0,5].reverse.each do |bt_line|
65
+ root_cause_bt[0, 10].reverse.each do |bt_line|
48
66
  bt.unshift "root cause: #{bt_line}"
49
67
  end
50
68
  bt.unshift "root cause: #{@root_cause.java_class.name}: #{@root_cause.message.chomp}"
@@ -52,6 +70,17 @@ module Mondrian
52
70
  set_backtrace bt
53
71
  end
54
72
 
73
+ def get_profiling(options)
74
+ if statement = options[:profiling_statement]
75
+ f = Java::mondrian.olap4j.MondrianOlap4jStatement.java_class.declared_field("openCellSet")
76
+ f.accessible = true
77
+ if cell_set = f.value(statement)
78
+ cell_set.close
79
+ @profiling_handler = statement.getProfileHandler
80
+ end
81
+ end
82
+ end
83
+
55
84
  end
56
85
  end
57
86
  end
@@ -34,7 +34,7 @@ module Mondrian
34
34
  end
35
35
  end
36
36
 
37
- AXIS_ALIASES = %w(columns rows pages sections chapters)
37
+ AXIS_ALIASES = %w(columns rows pages chapters sections)
38
38
  AXIS_ALIASES.each_with_index do |axis, i|
39
39
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
40
40
  def #{axis}(*axis_members)
@@ -47,7 +47,7 @@ module Mondrian
47
47
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
48
48
  def #{method}(*axis_members)
49
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?
50
+ raise ArgumentError, "specify set of members for #{method} method" if axis_members.empty?
51
51
  members = axis_members.length == 1 && axis_members[0].is_a?(Array) ? axis_members[0] : axis_members
52
52
  @current_set.replace [:#{method}, @current_set.clone, members]
53
53
  self
@@ -57,7 +57,7 @@ module Mondrian
57
57
 
58
58
  def except(*axis_members)
59
59
  raise ArgumentError, "cannot use except method before axis or with_set method" unless @current_set
60
- raise ArgumentError, "specify list of members for except method" if axis_members.empty?
60
+ raise ArgumentError, "specify set of members for except method" if axis_members.empty?
61
61
  members = axis_members.length == 1 && axis_members[0].is_a?(Array) ? axis_members[0] : axis_members
62
62
  if [:crossjoin, :nonempty_crossjoin].include? @current_set[0]
63
63
  @current_set[2] = [:except, @current_set[2], members]
@@ -73,7 +73,13 @@ module Mondrian
73
73
  self
74
74
  end
75
75
 
76
- def filter(condition, options={})
76
+ def distinct
77
+ raise ArgumentError, "cannot use distinct method before axis method" unless @current_set
78
+ @current_set.replace [:distinct, @current_set.clone]
79
+ self
80
+ end
81
+
82
+ def filter(condition, options = {})
77
83
  raise ArgumentError, "cannot use filter method before axis or with_set method" unless @current_set
78
84
  @current_set.replace [:filter, @current_set.clone, condition]
79
85
  @current_set << options[:as] if options[:as]
@@ -87,6 +93,19 @@ module Mondrian
87
93
  self
88
94
  end
89
95
 
96
+ def generate(*axis_members)
97
+ raise ArgumentError, "cannot use generate method before axis or with_set method" unless @current_set
98
+ all = if axis_members.last == :all
99
+ axis_members.pop
100
+ 'ALL'
101
+ end
102
+ raise ArgumentError, "specify set of members for generate method" if axis_members.empty?
103
+ members = axis_members.length == 1 && axis_members[0].is_a?(Array) ? axis_members[0] : axis_members
104
+ @current_set.replace [:generate, @current_set.clone, members]
105
+ @current_set << all if all
106
+ self
107
+ end
108
+
90
109
  VALID_ORDERS = ['ASC', 'BASC', 'DESC', 'BDESC']
91
110
 
92
111
  def order(expression, direction)
@@ -119,7 +138,7 @@ module Mondrian
119
138
  end
120
139
  end
121
140
 
122
- def hierarchize(order=nil, all=nil)
141
+ def hierarchize(order = nil, all = nil)
123
142
  raise ArgumentError, "cannot use hierarchize method before axis or with_set method" unless @current_set
124
143
  order = order && order.to_s.upcase
125
144
  raise ArgumentError, "invalid hierarchize order #{order.inspect}" unless order.nil? || order == 'POST'
@@ -133,7 +152,7 @@ module Mondrian
133
152
  self
134
153
  end
135
154
 
136
- def hierarchize_all(order=nil)
155
+ def hierarchize_all(order = nil)
137
156
  hierarchize(order, :all)
138
157
  end
139
158
 
@@ -216,20 +235,16 @@ module Mondrian
216
235
  mdx
217
236
  end
218
237
 
219
- def execute
220
- Error.wrap_native_exception do
221
- @connection.execute to_mdx
222
- end
238
+ def execute(parameters = {})
239
+ @connection.execute to_mdx, parameters
223
240
  end
224
241
 
225
242
  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
243
+ drill_through_mdx = "DRILLTHROUGH "
244
+ drill_through_mdx << "MAXROWS #{options[:max_rows]} " if options[:max_rows]
245
+ drill_through_mdx << to_mdx
246
+ drill_through_mdx << " RETURN #{Array(options[:return]).join(',')}" if options[:return]
247
+ @connection.execute_drill_through drill_through_mdx
233
248
  end
234
249
 
235
250
  private
@@ -281,10 +296,10 @@ module Mondrian
281
296
  }
282
297
 
283
298
  def members_to_mdx(members)
284
- # if only one member which does not end with ]
299
+ # if only one member which does not end with ] or .Item(...)
285
300
  # then assume it is expression which returns set
286
301
  # TODO: maybe always include also single expressions in {...} to avoid some edge cases?
287
- if members.length == 1 && members[0][-1,1] != ']'
302
+ if members.length == 1 && members[0] !~ /(\]|\.Item\(\d+\))\z/i
288
303
  members[0]
289
304
  elsif members[0].is_a?(Symbol)
290
305
  case members[0]
@@ -296,9 +311,13 @@ module Mondrian
296
311
  "EXCEPT(#{members_to_mdx(members[1])}, #{members_to_mdx(members[2])})"
297
312
  when :nonempty
298
313
  "NON EMPTY #{members_to_mdx(members[1])}"
314
+ when :distinct
315
+ "DISTINCT(#{members_to_mdx(members[1])})"
299
316
  when :filter
300
317
  as_alias = members[3] ? " AS #{members[3]}" : nil
301
318
  "FILTER(#{members_to_mdx(members[1])}#{as_alias}, #{members[2]})"
319
+ when :generate
320
+ "GENERATE(#{members_to_mdx(members[1])}, #{members_to_mdx(members[2])}#{members[3] && ", #{members[3]}"})"
302
321
  when :order
303
322
  "ORDER(#{members_to_mdx(members[1])}, #{expression_to_mdx(members[2])}, #{members[3]})"
304
323
  when :top_count, :bottom_count
@@ -357,8 +376,9 @@ module Mondrian
357
376
  end
358
377
 
359
378
  def extract_dimension_name(full_name)
360
- if full_name =~ /\A[^\[]*\[([^\]]+)\]/
361
- $1
379
+ # "[Foo [Bar]]].[Baz]" => "Foo [Bar]"
380
+ if full_name
381
+ full_name.gsub(/\A\[|\]\z/, '').split('].[').first.try(:gsub, ']]', ']')
362
382
  end
363
383
  end
364
384
  end
@@ -3,11 +3,15 @@ require 'bigdecimal'
3
3
  module Mondrian
4
4
  module OLAP
5
5
  class Result
6
- def initialize(connection, raw_cell_set)
6
+ def initialize(connection, raw_cell_set, options = {})
7
7
  @connection = connection
8
8
  @raw_cell_set = raw_cell_set
9
+ @profiling_handler = options[:profiling_handler]
10
+ @total_duration = options[:total_duration]
9
11
  end
10
12
 
13
+ attr_reader :raw_cell_set, :profiling_handler, :total_duration
14
+
11
15
  def axes_count
12
16
  axes.length
13
17
  end
@@ -103,6 +107,32 @@ module Mondrian
103
107
  end
104
108
  end
105
109
 
110
+ def profiling_plan
111
+ if profiling_handler
112
+ @raw_cell_set.close
113
+ if plan = profiling_handler.plan
114
+ plan.gsub("\r\n", "\n")
115
+ end
116
+ end
117
+ end
118
+
119
+ def profiling_timing
120
+ if profiling_handler
121
+ @raw_cell_set.close
122
+ profiling_handler.timing
123
+ end
124
+ end
125
+
126
+ def profiling_mark_full(name, duration)
127
+ profiling_timing && profiling_timing.markFull(name, duration)
128
+ end
129
+
130
+ def profiling_timing_string
131
+ if profiling_timing && (timing_string = profiling_timing.toString)
132
+ timing_string.gsub("\r\n", "\n")
133
+ end
134
+ end
135
+
106
136
  # Specify drill through cell position, for example, as
107
137
  # :row => 0, :cell => 1
108
138
  # Specify max returned rows with :max_rows parameter
@@ -175,7 +205,7 @@ module Mondrian
175
205
  if @raw_result_set.next
176
206
  row_values = []
177
207
  column_types.each_with_index do |column_type, i|
178
- row_values << Result.java_to_ruby_value(@raw_result_set.getObject(i+1), column_type)
208
+ row_values << Result.java_to_ruby_value(@raw_result_set.getObject(i + 1), column_type)
179
209
  end
180
210
  row_values
181
211
  else
@@ -240,25 +270,34 @@ module Mondrian
240
270
  end
241
271
 
242
272
  def self.generate_drill_through_sql(rolap_cell, result, params)
243
- return_field_names, return_expressions, nonempty_columns = parse_return_fields(result, params)
273
+ nonempty_columns, return_fields = parse_return_fields(result, params)
274
+ return_expressions = return_fields.map{|field| field[:member]}
244
275
 
245
276
  sql_non_extended = rolap_cell.getDrillThroughSQL(return_expressions, false)
246
277
  sql_extended = rolap_cell.getDrillThroughSQL(return_expressions, true)
247
278
 
248
- if sql_non_extended =~ /\Aselect (.*) from (.*) where (.*) order by (.*)\Z/
279
+ if sql_non_extended =~ /\Aselect (.*) from (.*) where (.*) order by (.*)\Z/m
280
+ non_extended_from = $2
281
+ non_extended_where = $3
282
+ # the latest Mondrian version sometimes returns sql_non_extended without order by
283
+ elsif sql_non_extended =~ /\Aselect (.*) from (.*) where (.*)\Z/m
249
284
  non_extended_from = $2
250
285
  non_extended_where = $3
286
+ # if drill through total measure with just all members selection
287
+ elsif sql_non_extended =~ /\Aselect (.*) from (.*)\Z/m
288
+ non_extended_from = $2
289
+ non_extended_where = "1 = 1" # dummy true condition
251
290
  else
252
291
  raise ArgumentError, "cannot parse drill through SQL: #{sql_non_extended}"
253
292
  end
254
293
 
255
- if sql_extended =~ /\Aselect (.*) from (.*) where (.*) order by (.*)\Z/
294
+ if sql_extended =~ /\Aselect (.*) from (.*) where (.*) order by (.*)\Z/m
256
295
  extended_select = $1
257
296
  extended_from = $2
258
297
  extended_where = $3
259
298
  extended_order_by = $4
260
299
  # if only measures are selected then there will be no order by
261
- elsif sql_extended =~ /\Aselect (.*) from (.*) where (.*)\Z/
300
+ elsif sql_extended =~ /\Aselect (.*) from (.*) where (.*)\Z/m
262
301
  extended_select = $1
263
302
  extended_from = $2
264
303
  extended_where = $3
@@ -267,25 +306,31 @@ module Mondrian
267
306
  raise ArgumentError, "cannot parse drill through SQL: #{sql_extended}"
268
307
  end
269
308
 
270
- return_column_positions = {}
271
-
272
- if return_field_names && !return_field_names.empty?
273
- new_select = extended_select.split(/,\s*/).map do |part|
274
- column_name, column_alias = part.split(' as ')
275
- field_name = column_alias[1..-2].gsub(' (Key)', '')
276
- position = return_field_names.index(field_name) || 9999
277
- return_column_positions[column_name] = position
278
- [part, position]
279
- end.sort_by(&:last).map(&:first).join(', ')
280
-
281
- new_order_by = extended_order_by.split(/,\s*/).map do |part|
282
- column_name, asc_desc = part.split(/\s+/)
283
- position = return_column_positions[column_name] || 9999
284
- [part, position]
285
- end.sort_by(&:last).map(&:first).join(', ')
309
+ if return_fields.present?
310
+ new_select_columns = []
311
+ new_order_by_columns = []
312
+ new_group_by_columns = []
313
+ group_by = params[:group_by]
314
+
315
+ return_fields.size.times do |i|
316
+ column_alias = return_fields[i][:column_alias]
317
+ new_select_columns <<
318
+ if column_expression = return_fields[i][:column_expression]
319
+ new_order_by_columns << column_expression
320
+ new_group_by_columns << column_expression if group_by && return_fields[i][:type] != :measure
321
+ "#{column_expression} AS #{column_alias}"
322
+ else
323
+ "'' AS #{column_alias}"
324
+ end
325
+ end
326
+
327
+ new_select = new_select_columns.join(', ')
328
+ new_order_by = new_order_by_columns.join(', ')
329
+ new_group_by = new_group_by_columns.join(', ')
286
330
  else
287
331
  new_select = extended_select
288
332
  new_order_by = extended_order_by
333
+ new_group_by = ''
289
334
  end
290
335
 
291
336
  new_from_parts = non_extended_from.split(/,\s*/)
@@ -321,40 +366,96 @@ module Mondrian
321
366
  end
322
367
 
323
368
  sql = "select #{new_select} from #{new_from} where #{new_where}"
369
+ sql << " group by #{new_group_by}" unless new_group_by.empty?
324
370
  sql << " order by #{new_order_by}" unless new_order_by.empty?
325
371
  sql
326
372
  end
327
373
 
328
374
  def self.parse_return_fields(result, params)
329
- return_field_names = []
330
- return_expressions = nil
331
375
  nonempty_columns = []
376
+ return_fields = []
332
377
 
333
378
  if params[:return] || params[:nonempty]
334
379
  rolap_cube = result.getCube
335
380
  schema_reader = rolap_cube.getSchemaReader
381
+ dialect = result.getCube.getSchema.getDialect
382
+ sql_query = Java::mondrian.rolap.sql.SqlQuery.new(dialect)
383
+
384
+ if fields = params[:return]
385
+ fields = fields.split(/,\s*/) if fields.is_a? String
386
+ fields.each do |field|
387
+ return_fields << case field
388
+ when /\AName\((.*)\)\z/i then
389
+ { member_full_name: $1, type: :name }
390
+ when /\AProperty\((.*)\s*,\s*'(.*)'\)\z/i then
391
+ { member_full_name: $1, type: :property, name: $2 }
392
+ else
393
+ { member_full_name: field }
394
+ end
395
+ end
336
396
 
337
- if return_fields = params[:return]
338
- return_fields = return_fields.split(/,\s*/) if return_fields.is_a?(String)
339
- return_expressions = return_fields.map do |return_field|
397
+ # Old versions of Oracle had a limit of 30 character identifiers.
398
+ # Do not limit it for other databases (as e.g. in MySQL aliases can be longer than column names)
399
+ max_alias_length = dialect.getMaxColumnNameLength
400
+ max_alias_length = nil if max_alias_length && max_alias_length > 30
401
+
402
+ return_fields.size.times do |i|
403
+ member_full_name = return_fields[i][:member_full_name]
340
404
  begin
341
- segment_list = Java::MondrianOlap::Util.parseIdentifier(return_field)
342
- return_field_names << segment_list.to_a.last.name
405
+ segment_list = Java::MondrianOlap::Util.parseIdentifier(member_full_name)
343
406
  rescue Java::JavaLang::IllegalArgumentException
344
- raise ArgumentError, "invalid return field #{return_field}"
407
+ raise ArgumentError, "invalid return field #{member_full_name}"
345
408
  end
346
409
 
410
+ # if this is property field then the name is initialized already
411
+ return_fields[i][:name] ||= segment_list.to_a.last.name
347
412
  level_or_member = schema_reader.lookupCompound rolap_cube, segment_list, false, 0
413
+ return_fields[i][:member] = level_or_member
414
+
415
+ if level_or_member.is_a? Java::MondrianOlap::Member
416
+ raise ArgumentError, "cannot use calculated member #{member_full_name} as return field" if level_or_member.isCalculated
417
+ elsif !level_or_member.is_a? Java::MondrianOlap::Level
418
+ raise ArgumentError, "return field #{member_full_name} should be level or measure"
419
+ end
420
+
421
+ return_fields[i][:column_expression] = case return_fields[i][:type]
422
+ when :name
423
+ if level_or_member.respond_to? :getNameExp
424
+ level_or_member.getNameExp.getExpression sql_query
425
+ end
426
+ when :property
427
+ if property = level_or_member.getProperties.to_a.detect{|p| p.getName == return_fields[i][:name]}
428
+ # property.getExp is a protected method therefore
429
+ # use a workaround to get the value from the field
430
+ f = property.java_class.declared_field("exp")
431
+ f.accessible = true
432
+ if column = f.value(property)
433
+ column.getExpression sql_query
434
+ end
435
+ end
436
+ else
437
+ if level_or_member.respond_to? :getKeyExp
438
+ return_fields[i][:type] = :key
439
+ level_or_member.getKeyExp.getExpression sql_query
440
+ else
441
+ return_fields[i][:type] = :measure
442
+ column_expression = level_or_member.getMondrianDefExpression.getExpression sql_query
443
+ if params[:group_by]
444
+ level_or_member.getAggregator.getExpression column_expression
445
+ else
446
+ column_expression
447
+ end
448
+ end
449
+ end
348
450
 
349
- case level_or_member
350
- when Java::MondrianOlap::Level
351
- Java::MondrianMdx::LevelExpr.new level_or_member
352
- when Java::MondrianOlap::Member
353
- raise ArgumentError, "cannot use calculated member #{return_field} as return field" if level_or_member.isCalculated
354
- Java::mondrian.mdx.MemberExpr.new level_or_member
451
+ column_alias = if return_fields[i][:type] == :key
452
+ "#{return_fields[i][:name]} (Key)"
355
453
  else
356
- raise ArgumentError, "return field #{return_field} should be level or measure"
454
+ return_fields[i][:name]
357
455
  end
456
+ return_fields[i][:column_alias] = dialect.quoteIdentifier(
457
+ max_alias_length ? column_alias[0, max_alias_length] : column_alias
458
+ )
358
459
  end
359
460
  end
360
461
 
@@ -364,23 +465,22 @@ module Mondrian
364
465
  begin
365
466
  segment_list = Java::MondrianOlap::Util.parseIdentifier(nonempty_field)
366
467
  rescue Java::JavaLang::IllegalArgumentException
367
- raise ArgumentError, "invalid return field #{return_field}"
468
+ raise ArgumentError, "invalid return field #{nonempty_field}"
368
469
  end
369
470
  member = schema_reader.lookupCompound rolap_cube, segment_list, false, 0
370
471
  if member.is_a? Java::MondrianOlap::Member
371
- raise ArgumentError, "cannot use calculated member #{return_field} as nonempty field" if member.isCalculated
472
+ raise ArgumentError, "cannot use calculated member #{nonempty_field} as nonempty field" if member.isCalculated
372
473
  sql_query = member.getStarMeasure.getSqlQuery
373
474
  member.getStarMeasure.generateExprString(sql_query)
374
475
  else
375
- raise ArgumentError, "nonempty field #{return_field} should be measure"
476
+ raise ArgumentError, "nonempty field #{nonempty_field} should be measure"
376
477
  end
377
478
  end
378
479
  end
379
480
  end
380
481
 
381
- [return_field_names, return_expressions, nonempty_columns]
482
+ [nonempty_columns, return_fields]
382
483
  end
383
-
384
484
  end
385
485
 
386
486
  def self.java_to_ruby_value(value, column_type = nil)
@@ -389,6 +489,8 @@ module Mondrian
389
489
  value
390
490
  when Java::JavaMath::BigDecimal
391
491
  BigDecimal(value.to_s)
492
+ when Java::JavaSql::Clob
493
+ clob_to_string(value)
392
494
  else
393
495
  value
394
496
  end
@@ -396,11 +498,28 @@ module Mondrian
396
498
 
397
499
  private
398
500
 
501
+ def self.clob_to_string(value)
502
+ if reader = value.getCharacterStream
503
+ buffered_reader = Java::JavaIo::BufferedReader.new(reader)
504
+ result = []
505
+ while str = buffered_reader.readLine
506
+ result << str
507
+ end
508
+ result.join("\n")
509
+ end
510
+ ensure
511
+ if buffered_reader
512
+ buffered_reader.close
513
+ elsif reader
514
+ reader.close
515
+ end
516
+ end
517
+
399
518
  def axes
400
519
  @axes ||= @raw_cell_set.getAxes
401
520
  end
402
521
 
403
- def axis_positions(map_method, join_with=false)
522
+ def axis_positions(map_method, join_with = false)
404
523
  axes.map do |axis|
405
524
  axis.getPositions.map do |position|
406
525
  names = position.getMembers.map do |member|
@@ -429,7 +548,7 @@ module Mondrian
429
548
  :chapters => 4
430
549
  }.freeze
431
550
 
432
- def recursive_values(value_method, axes_sequence, current_index, cell_params=[])
551
+ def recursive_values(value_method, axes_sequence, current_index, cell_params = [])
433
552
  if axis_number = axes_sequence[current_index]
434
553
  axis_number = AXIS_SYMBOL_TO_NUMBER[axis_number] if axis_number.is_a?(Symbol)
435
554
  positions_size = axes[axis_number].getPositions.size