mondrian-olap 0.5.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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