mondrian-olap 0.8.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -79,7 +79,7 @@ module Mondrian
79
79
  self
80
80
  end
81
81
 
82
- def filter(condition, options={})
82
+ def filter(condition, options = {})
83
83
  raise ArgumentError, "cannot use filter method before axis or with_set method" unless @current_set
84
84
  @current_set.replace [:filter, @current_set.clone, condition]
85
85
  @current_set << options[:as] if options[:as]
@@ -138,7 +138,7 @@ module Mondrian
138
138
  end
139
139
  end
140
140
 
141
- def hierarchize(order=nil, all=nil)
141
+ def hierarchize(order = nil, all = nil)
142
142
  raise ArgumentError, "cannot use hierarchize method before axis or with_set method" unless @current_set
143
143
  order = order && order.to_s.upcase
144
144
  raise ArgumentError, "invalid hierarchize order #{order.inspect}" unless order.nil? || order == 'POST'
@@ -152,7 +152,7 @@ module Mondrian
152
152
  self
153
153
  end
154
154
 
155
- def hierarchize_all(order=nil)
155
+ def hierarchize_all(order = nil)
156
156
  hierarchize(order, :all)
157
157
  end
158
158
 
@@ -236,19 +236,15 @@ module Mondrian
236
236
  end
237
237
 
238
238
  def execute(parameters = {})
239
- Error.wrap_native_exception do
240
- @connection.execute to_mdx, parameters
241
- end
239
+ @connection.execute to_mdx, parameters
242
240
  end
243
241
 
244
242
  def execute_drill_through(options = {})
245
- Error.wrap_native_exception do
246
- drill_through_mdx = "DRILLTHROUGH "
247
- drill_through_mdx << "MAXROWS #{options[:max_rows]} " if options[:max_rows]
248
- drill_through_mdx << to_mdx
249
- drill_through_mdx << " RETURN #{Array(options[:return]).join(',')}" if options[:return]
250
- @connection.execute_drill_through drill_through_mdx
251
- 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
252
248
  end
253
249
 
254
250
  private
@@ -300,10 +296,10 @@ module Mondrian
300
296
  }
301
297
 
302
298
  def members_to_mdx(members)
303
- # if only one member which does not end with ]
299
+ # if only one member which does not end with ] or .Item(...)
304
300
  # then assume it is expression which returns set
305
301
  # TODO: maybe always include also single expressions in {...} to avoid some edge cases?
306
- if members.length == 1 && members[0][-1,1] != ']'
302
+ if members.length == 1 && members[0] !~ /(\]|\.Item\(\d+\))\z/i
307
303
  members[0]
308
304
  elsif members[0].is_a?(Symbol)
309
305
  case members[0]
@@ -380,8 +376,9 @@ module Mondrian
380
376
  end
381
377
 
382
378
  def extract_dimension_name(full_name)
383
- if full_name =~ /\A[^\[]*\[([^\]]+)\]/
384
- $1
379
+ # "[Foo [Bar]]].[Baz]" => "Foo [Bar]"
380
+ if full_name
381
+ full_name.gsub(/\A\[|\]\z/, '').split('].[').first.try(:gsub, ']]', ']')
385
382
  end
386
383
  end
387
384
  end
@@ -3,12 +3,14 @@ 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
 
11
- attr_reader :raw_cell_set
13
+ attr_reader :raw_cell_set, :profiling_handler, :total_duration
12
14
 
13
15
  def axes_count
14
16
  axes.length
@@ -105,6 +107,32 @@ module Mondrian
105
107
  end
106
108
  end
107
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
+
108
136
  # Specify drill through cell position, for example, as
109
137
  # :row => 0, :cell => 1
110
138
  # Specify max returned rows with :max_rows parameter
@@ -177,7 +205,7 @@ module Mondrian
177
205
  if @raw_result_set.next
178
206
  row_values = []
179
207
  column_types.each_with_index do |column_type, i|
180
- 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)
181
209
  end
182
210
  row_values
183
211
  else
@@ -248,24 +276,28 @@ module Mondrian
248
276
  sql_non_extended = rolap_cell.getDrillThroughSQL(return_expressions, false)
249
277
  sql_extended = rolap_cell.getDrillThroughSQL(return_expressions, true)
250
278
 
251
- 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
252
284
  non_extended_from = $2
253
285
  non_extended_where = $3
254
286
  # if drill through total measure with just all members selection
255
- elsif sql_non_extended =~ /\Aselect (.*) from (.*)\Z/
287
+ elsif sql_non_extended =~ /\Aselect (.*) from (.*)\Z/m
256
288
  non_extended_from = $2
257
289
  non_extended_where = "1 = 1" # dummy true condition
258
290
  else
259
291
  raise ArgumentError, "cannot parse drill through SQL: #{sql_non_extended}"
260
292
  end
261
293
 
262
- if sql_extended =~ /\Aselect (.*) from (.*) where (.*) order by (.*)\Z/
294
+ if sql_extended =~ /\Aselect (.*) from (.*) where (.*) order by (.*)\Z/m
263
295
  extended_select = $1
264
296
  extended_from = $2
265
297
  extended_where = $3
266
298
  extended_order_by = $4
267
299
  # if only measures are selected then there will be no order by
268
- elsif sql_extended =~ /\Aselect (.*) from (.*) where (.*)\Z/
300
+ elsif sql_extended =~ /\Aselect (.*) from (.*) where (.*)\Z/m
269
301
  extended_select = $1
270
302
  extended_from = $2
271
303
  extended_where = $3
@@ -282,13 +314,14 @@ module Mondrian
282
314
 
283
315
  return_fields.size.times do |i|
284
316
  column_alias = return_fields[i][:column_alias]
285
- new_select_columns << if column_expression = return_fields[i][:column_expression]
286
- new_order_by_columns << column_expression
287
- new_group_by_columns << column_expression if group_by && return_fields[i][:type] != :measure
288
- "#{column_expression} AS #{column_alias}"
289
- else
290
- "'' AS #{column_alias}"
291
- end
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
292
325
  end
293
326
 
294
327
  new_select = new_select_columns.join(', ')
@@ -361,7 +394,7 @@ module Mondrian
361
394
  end
362
395
  end
363
396
 
364
- return_fields.size.times do | i |
397
+ return_fields.size.times do |i|
365
398
  member_full_name = return_fields[i][:member_full_name]
366
399
  begin
367
400
  segment_list = Java::MondrianOlap::Util.parseIdentifier(member_full_name)
@@ -381,34 +414,34 @@ module Mondrian
381
414
  end
382
415
 
383
416
  return_fields[i][:column_expression] = case return_fields[i][:type]
384
- when :name
385
- if level_or_member.respond_to? :getNameExp
386
- level_or_member.getNameExp.getExpression sql_query
387
- end
388
- when :property
389
- if property = level_or_member.getProperties.to_a.detect{|p| p.getName == return_fields[i][:name]}
390
- # property.getExp is a protected method therefore
391
- # use a workaround to get the value from the field
392
- f = property.java_class.declared_field("exp")
393
- f.accessible = true
394
- if column = f.value(property)
395
- column.getExpression sql_query
396
- end
417
+ when :name
418
+ if level_or_member.respond_to? :getNameExp
419
+ level_or_member.getNameExp.getExpression sql_query
420
+ end
421
+ when :property
422
+ if property = level_or_member.getProperties.to_a.detect{|p| p.getName == return_fields[i][:name]}
423
+ # property.getExp is a protected method therefore
424
+ # use a workaround to get the value from the field
425
+ f = property.java_class.declared_field("exp")
426
+ f.accessible = true
427
+ if column = f.value(property)
428
+ column.getExpression sql_query
397
429
  end
430
+ end
431
+ else
432
+ if level_or_member.respond_to? :getKeyExp
433
+ return_fields[i][:type] = :key
434
+ level_or_member.getKeyExp.getExpression sql_query
398
435
  else
399
- if level_or_member.respond_to? :getKeyExp
400
- return_fields[i][:type] = :key
401
- level_or_member.getKeyExp.getExpression sql_query
436
+ return_fields[i][:type] = :measure
437
+ column_expression = level_or_member.getMondrianDefExpression.getExpression sql_query
438
+ if params[:group_by]
439
+ level_or_member.getAggregator.getExpression column_expression
402
440
  else
403
- return_fields[i][:type] = :measure
404
- column_expression = level_or_member.getMondrianDefExpression.getExpression sql_query
405
- if params[:group_by]
406
- level_or_member.getAggregator.getExpression column_expression
407
- else
408
- column_expression
409
- end
441
+ column_expression
410
442
  end
411
443
  end
444
+ end
412
445
 
413
446
  column_alias = if return_fields[i][:type] == :key
414
447
  "#{return_fields[i][:name]} (Key)"
@@ -479,7 +512,7 @@ module Mondrian
479
512
  @axes ||= @raw_cell_set.getAxes
480
513
  end
481
514
 
482
- def axis_positions(map_method, join_with=false)
515
+ def axis_positions(map_method, join_with = false)
483
516
  axes.map do |axis|
484
517
  axis.getPositions.map do |position|
485
518
  names = position.getMembers.map do |member|
@@ -508,7 +541,7 @@ module Mondrian
508
541
  :chapters => 4
509
542
  }.freeze
510
543
 
511
- def recursive_values(value_method, axes_sequence, current_index, cell_params=[])
544
+ def recursive_values(value_method, axes_sequence, current_index, cell_params = [])
512
545
  if axis_number = axes_sequence[current_index]
513
546
  axis_number = AXIS_SYMBOL_TO_NUMBER[axis_number] if axis_number.is_a?(Symbol)
514
547
  positions_size = axes[axis_number].getPositions.size
@@ -497,6 +497,7 @@ module Mondrian
497
497
  end
498
498
 
499
499
  class Annotation < SchemaElement
500
+ attributes :name
500
501
  content :text
501
502
  end
502
503
 
@@ -67,14 +67,30 @@ module Mondrian
67
67
  attr_reader pluralize(name).to_sym
68
68
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
69
69
  def #{name}(name=nil, attributes = {}, &block)
70
- @#{pluralize(name)} << Schema::#{camel_case(name)}.new(name, attributes, self, &block)
70
+ new_element = Schema::#{camel_case(name)}.new(name, attributes, self, &block)
71
+ @#{pluralize(name)} << new_element
72
+ new_element
71
73
  end
72
74
  RUBY
75
+ if name == :annotations
76
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
77
+ def annotations_hash
78
+ hash = {}
79
+ @annotationss.each do |annotations|
80
+ annotations.annotations.each do |annotation|
81
+ hash[annotation.name] = annotation.content
82
+ end
83
+ end
84
+ hash
85
+ end
86
+ RUBY
87
+ end
73
88
  end
74
89
  end
75
90
 
76
- def self.content(type=nil)
91
+ def self.content(type = nil)
77
92
  return @content if type.nil?
93
+ attr_reader :content
78
94
  @content = type
79
95
  end
80
96
 
@@ -86,7 +102,7 @@ module Mondrian
86
102
  @xml_fragments << string
87
103
  end
88
104
 
89
- def to_xml(options={})
105
+ def to_xml(options = {})
90
106
  options[:upcase_data_dictionary] = @upcase_data_dictionary unless @upcase_data_dictionary.nil?
91
107
  Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
92
108
  add_to_xml(xml, options)
@@ -125,7 +141,7 @@ module Mondrian
125
141
  def xmlized_attributes(options)
126
142
  # data dictionary values should be in uppercase if schema defined with :upcase_data_dictionary => true
127
143
  # or by default when using Oracle or LucidDB driver (can be overridden by :upcase_data_dictionary => false)
128
- upcase_attributes = if options[:upcase_data_dictionary].nil? && %w(oracle luciddb).include?(options[:driver]) ||
144
+ upcase_attributes = if options[:upcase_data_dictionary].nil? && %w(oracle luciddb snowflake).include?(options[:driver]) ||
129
145
  options[:upcase_data_dictionary]
130
146
  self.class.data_dictionary_names
131
147
  else
@@ -17,7 +17,7 @@ module Mondrian
17
17
 
18
18
  def coffeescript_function(arguments_string, text)
19
19
  # construct function to ensure that last expression is returned
20
- coffee_text = "#{arguments_string} ->\n" << text.gsub(/^/,' ')
20
+ coffee_text = "#{arguments_string} ->\n" << text.gsub(/^/, ' ')
21
21
  javascript_text = CoffeeScript.compile(coffee_text, :bare => true)
22
22
  # remove function definition first and last lines
23
23
  javascript_text = javascript_text.strip.lines.to_a[1..-2].join
@@ -67,7 +67,7 @@ module Mondrian
67
67
  end
68
68
 
69
69
  def ruby_formatter_java_class_name(name)
70
- "rubyobj.#{self.class.name.gsub('::','.')}.#{ruby_formatter_name_to_class_name(name)}"
70
+ "rubyobj.#{self.class.name.gsub('::', '.')}.#{ruby_formatter_name_to_class_name(name)}"
71
71
  end
72
72
 
73
73
  end
@@ -180,19 +180,21 @@ JS
180
180
  add_method_signature("getSyntax", [Java::mondrian.olap.Syntax])
181
181
 
182
182
  UDF_SCALAR_TYPES = {
183
- "Numeric" => Java::mondrian.olap.type.NumericType,
184
- "String" => Java::mondrian.olap.type.StringType,
185
- "Boolean" => Java::mondrian.olap.type.BooleanType,
186
- "DateTime" => Java::mondrian.olap.type.DateTimeType,
187
- "Decimal" => Java::mondrian.olap.type.DecimalType,
188
- "Scalar" => Java::mondrian.olap.type.ScalarType
183
+ 'Numeric' => Java::mondrian.olap.type.NumericType,
184
+ 'String' => Java::mondrian.olap.type.StringType,
185
+ 'Boolean' => Java::mondrian.olap.type.BooleanType,
186
+ 'DateTime' => Java::mondrian.olap.type.DateTimeType,
187
+ 'Decimal' => Java::mondrian.olap.type.DecimalType,
188
+ 'Scalar' => Java::mondrian.olap.type.ScalarType
189
189
  }
190
190
  UDF_OTHER_TYPES = {
191
- "Member" => Java::mondrian.olap.type.MemberType::Unknown,
192
- "Set" => Java::mondrian.olap.type.SetType.new(Java::mondrian.olap.type.MemberType::Unknown),
193
- "Hierarchy" => Java::mondrian.olap.type.HierarchyType.new(nil, nil),
194
- "Level" => Java::mondrian.olap.type.LevelType::Unknown
191
+ 'Member' => Java::mondrian.olap.type.MemberType::Unknown,
192
+ 'Tuple' => Java::mondrian.olap.type.TupleType.new([].to_java(Java::mondrian.olap.type.Type)),
193
+ 'Hierarchy' => Java::mondrian.olap.type.HierarchyType.new(nil, nil),
194
+ 'Level' => Java::mondrian.olap.type.LevelType::Unknown
195
195
  }
196
+ UDF_OTHER_TYPES['Set'] = UDF_OTHER_TYPES['MemberSet'] = Java::mondrian.olap.type.SetType.new(UDF_OTHER_TYPES['Member'])
197
+ UDF_OTHER_TYPES['TupleSet'] = Java::mondrian.olap.type.SetType.new(UDF_OTHER_TYPES['Tuple'])
196
198
 
197
199
  def getParameterTypes
198
200
  @parameterTypes ||= self.class.parameters.map{|p| get_java_type(p)}
@@ -214,7 +216,7 @@ JS
214
216
 
215
217
  def execute(evaluator, arguments)
216
218
  values = []
217
- self.class.parameters.each_with_index do |p,i|
219
+ self.class.parameters.each_with_index do |p, i|
218
220
  value = UDF_SCALAR_TYPES[p] ? arguments[i].evaluateScalar(evaluator) : arguments[i].evaluate(evaluator)
219
221
  values << value
220
222
  end
@@ -239,9 +241,12 @@ JS
239
241
  end
240
242
 
241
243
  def self.stringified_type(type)
242
- type = stringify(type)
243
- raise ArgumentError, "invalid user defined function type #{type.inspect}" unless UDF_SCALAR_TYPES[type] || UDF_OTHER_TYPES[type]
244
- type
244
+ type_as_string = stringify(type)
245
+ if UDF_SCALAR_TYPES[type_as_string] || UDF_OTHER_TYPES[type_as_string]
246
+ type_as_string
247
+ else
248
+ raise ArgumentError, "Invalid user defined function type #{type.inspect}"
249
+ end
245
250
  end
246
251
 
247
252
  def self.stringify(arg)
@@ -4,15 +4,21 @@ describe "Connection role" do
4
4
 
5
5
  describe "create connection" do
6
6
  before(:each) do
7
- @role_name = role_name = 'California manager'
8
- @role_name2 = role_name2 = 'Dummy, with comma'
7
+ @all_roles = [
8
+ @role_name = role_name = 'California manager',
9
+ @role_name2 = role_name2 = 'Dummy, with comma',
10
+ @simple_role_name = simple_role_name = 'USA manager',
11
+ @union_role_name = union_role_name = 'Union California manager',
12
+ @intermediate_union_role_name = intermediate_union_role_name = "Intermediate #{union_role_name}"
13
+ ]
14
+
9
15
  @schema = Mondrian::OLAP::Schema.define do
10
16
  cube 'Sales' do
11
17
  table 'sales'
12
18
  dimension 'Gender', :foreign_key => 'customer_id' do
13
19
  hierarchy :has_all => true, :primary_key => 'id' do
14
20
  table 'customers'
15
- level 'Gender', :column => 'gender', :unique_members => true
21
+ level 'Gender', :column => 'gender', :unique_members => true, :hide_member_if => 'IfBlankName'
16
22
  end
17
23
  end
18
24
  dimension 'Customers', :foreign_key => 'customer_id' do
@@ -49,6 +55,26 @@ describe "Connection role" do
49
55
  end
50
56
  role role_name2
51
57
 
58
+ role simple_role_name do
59
+ schema_grant :access => 'none' do
60
+ cube_grant :cube => 'Sales', :access => 'all' do
61
+ hierarchy_grant :hierarchy => '[Customers]', :access => 'custom', :bottom_level => '[Customers].[State Province]' do
62
+ member_grant :member => '[Customers].[USA]', :access => 'all'
63
+ end
64
+ end
65
+ end
66
+ end
67
+ role intermediate_union_role_name do
68
+ union do
69
+ role_usage role_name: simple_role_name
70
+ end
71
+ end
72
+ role union_role_name do
73
+ union do
74
+ role_usage role_name: intermediate_union_role_name
75
+ end
76
+ end
77
+
52
78
  # to test that Role elements are generated before UserDefinedFunction
53
79
  user_defined_function 'Factorial' do
54
80
  ruby do
@@ -63,12 +89,16 @@ describe "Connection role" do
63
89
  @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge :schema => @schema)
64
90
  end
65
91
 
92
+ after(:each) do
93
+ @olap.role_name = nil if @olap
94
+ end
95
+
66
96
  it "should connect" do
67
97
  @olap.should be_connected
68
98
  end
69
99
 
70
100
  it "should get available role names" do
71
- @olap.available_role_names.should == [@role_name, @role_name2]
101
+ @olap.available_role_names.sort.should == @all_roles.sort
72
102
  end
73
103
 
74
104
  it "should not get role name if not set" do
@@ -113,17 +143,40 @@ describe "Connection role" do
113
143
  # end
114
144
 
115
145
  it "should not get non-visible member when role name set in connection parameters" do
116
- @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge :schema => @schema,
117
- :role => @role_name)
118
- @cube = @olap.cube('Sales')
119
- @cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
146
+ olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge schema: @schema, role: @role_name)
147
+ cube = olap.cube('Sales')
148
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
120
149
  end
121
150
 
122
151
  it "should not get non-visible member when several role names set in connection parameters" do
123
- @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge :schema => @schema,
124
- :roles => [@role_name, @role_name2])
125
- @cube = @olap.cube('Sales')
126
- @cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
152
+ olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge schema: @schema, roles: [@role_name, @role_name2])
153
+ cube = olap.cube('Sales')
154
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
155
+ end
156
+
157
+ it "should see members from ragged dimensions when using single role" do
158
+ # Workaround for a Mondrian bug which does not allow access to ragged dimensions when using single role.
159
+ # This syntax will create a union role with one role.
160
+ @olap.role_names = [@role_name]
161
+ cube = @olap.cube('Sales')
162
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
163
+ cube.member('[Gender].[All Genders]').should_not be_nil
164
+ end
165
+
166
+ it "should see members from ragged dimensions when using multiple roles" do
167
+ @olap.role_names = [@role_name, @role_name2]
168
+ cube = @olap.cube('Sales')
169
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
170
+ cube.member('[Gender].[All Genders]').should_not be_nil
171
+ end
172
+
173
+ # Test patch for UnionRoleImpl getBottomLevelDepth method
174
+ it "should see member as drillable when using union of union role" do
175
+ @olap.role_names = [@union_role_name]
176
+ cube = @olap.cube('Sales')
177
+ cube.member('[Customers].[All Customers]').should be_drillable
178
+ cube.member('[Customers].[All Customers].[USA]').should be_drillable
179
+ cube.member('[Customers].[All Customers].[USA].[CA]').should_not be_drillable
127
180
  end
128
181
 
129
182
  end