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
@@ -39,6 +39,41 @@ describe "Connection" do
39
39
  @olap.should be_connected
40
40
  end
41
41
 
42
+ # to check that correct database dialect is loaded by ServiceDiscovery detected class loader
43
+ it "should use corresponding Mondrian dialect" do
44
+ # read private "schema" field
45
+ schema_field = @olap.raw_schema.getClass.getDeclaredField("schema")
46
+ schema_field.setAccessible(true)
47
+ private_schema = schema_field.get(@olap.raw_schema)
48
+ private_schema.getDialect.java_class.name.should == case MONDRIAN_DRIVER
49
+ when 'mysql' then 'mondrian.spi.impl.MySqlDialect'
50
+ when 'postgresql' then 'mondrian.spi.impl.PostgreSqlDialect'
51
+ when 'oracle' then 'mondrian.spi.impl.OracleDialect'
52
+ when 'luciddb' then 'mondrian.spi.impl.LucidDbDialect'
53
+ when 'mssql' then 'mondrian.spi.impl.MicrosoftSqlServerDialect'
54
+ when 'sqlserver' then 'mondrian.spi.impl.MicrosoftSqlServerDialect'
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ describe "locale" do
61
+ %w(en en_US de de_DE).each do |locale|
62
+ it "should set #{locale} locale from connection parameters" do
63
+ @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS_WITH_CATALOG.merge(:locale => locale))
64
+ @olap.locale.should == locale
65
+ @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS_WITH_CATALOG.merge(:locale => locale.to_sym))
66
+ @olap.locale.should == locale.to_s
67
+ end
68
+
69
+ it "should set #{locale} locale using setter method" do
70
+ @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS_WITH_CATALOG)
71
+ @olap.locale = locale
72
+ @olap.locale.should == locale
73
+ @olap.locale = locale.to_sym
74
+ @olap.locale.should == locale.to_s
75
+ end
76
+ end
42
77
  end
43
78
 
44
79
  describe "close" do
@@ -53,4 +88,4 @@ describe "Connection" do
53
88
 
54
89
  end
55
90
 
56
- end
91
+ end
@@ -3,24 +3,43 @@ require "spec_helper"
3
3
  describe "Cube" do
4
4
  before(:all) do
5
5
  @schema = Mondrian::OLAP::Schema.define do
6
+ measures_caption 'Measures caption'
7
+
6
8
  cube 'Sales' do
9
+ description 'Sales description'
10
+ caption 'Sales caption'
11
+ annotations :foo => 'bar'
7
12
  table 'sales'
8
13
  dimension 'Gender', :foreign_key => 'customer_id' do
14
+ description 'Gender description'
15
+ caption 'Gender caption'
9
16
  hierarchy :has_all => true, :primary_key => 'id' do
17
+ description 'Gender hierarchy description'
18
+ caption 'Gender hierarchy caption'
19
+ all_member_name 'All Genders'
20
+ all_member_caption 'All Genders caption'
10
21
  table 'customers'
11
- level 'Gender', :column => 'gender', :unique_members => true
22
+ level 'Gender', :column => 'gender', :unique_members => true,
23
+ :description => 'Gender level description', :caption => 'Gender level caption' do
24
+ # Dimension values SQL generated by caption_expression fails on PostgreSQL and MS SQL
25
+ if %w(mysql oracle).include?(MONDRIAN_DRIVER)
26
+ caption_expression do
27
+ sql "'dummy'"
28
+ end
29
+ end
30
+ end
12
31
  end
13
32
  end
14
- dimension 'Customers', :foreign_key => 'customer_id' do
15
- hierarchy :has_all => true, :all_member_name => 'All Customers', :primary_key => 'id' do
33
+ dimension 'Customers', :foreign_key => 'customer_id', :annotations => {:foo => 'bar'} do
34
+ hierarchy :has_all => true, :all_member_name => 'All Customers', :primary_key => 'id', :annotations => {:foo => 'bar'} do
16
35
  table 'customers'
17
- level 'Country', :column => 'country', :unique_members => true
36
+ level 'Country', :column => 'country', :unique_members => true, :annotations => {:foo => 'bar'}
18
37
  level 'State Province', :column => 'state_province', :unique_members => true
19
38
  level 'City', :column => 'city', :unique_members => false
20
39
  level 'Name', :column => 'fullname', :unique_members => true
21
40
  end
22
41
  end
23
- calculated_member 'Non-USA' do
42
+ calculated_member 'Non-USA', :annotations => {:foo => 'bar'} do
24
43
  dimension 'Customers'
25
44
  formula '[Customers].[All Customers] - [Customers].[USA]'
26
45
  end
@@ -37,8 +56,9 @@ describe "Cube" do
37
56
  level 'Week', :column => 'weak_of_year', :type => 'Numeric', :unique_members => false, :level_type => 'TimeWeeks'
38
57
  end
39
58
  end
40
- measure 'Unit Sales', :column => 'unit_sales', :aggregator => 'sum'
59
+ measure 'Unit Sales', :column => 'unit_sales', :aggregator => 'sum', :annotations => {:foo => 'bar'}
41
60
  measure 'Store Sales', :column => 'store_sales', :aggregator => 'sum'
61
+ measure 'Store Cost', :column => 'store_cost', :aggregator => 'sum', :visible => false
42
62
  end
43
63
  end
44
64
  @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge :schema => @schema)
@@ -60,6 +80,18 @@ describe "Cube" do
60
80
  @olap.cube('Sales').name.should == 'Sales'
61
81
  end
62
82
 
83
+ it "should get cube description" do
84
+ @olap.cube('Sales').description.should == 'Sales description'
85
+ end
86
+
87
+ it "should get cube caption" do
88
+ @olap.cube('Sales').caption.should == 'Sales caption'
89
+ end
90
+
91
+ it "should get cube annotations" do
92
+ @olap.cube('Sales').annotations.should == {'foo' => 'bar'}
93
+ end
94
+
63
95
  describe "dimensions" do
64
96
  before(:all) do
65
97
  @cube = @olap.cube('Sales')
@@ -78,6 +110,18 @@ describe "Cube" do
78
110
  @cube.dimension('Gender').name.should == 'Gender'
79
111
  end
80
112
 
113
+ it "should return nil when getting dimension with invalid name" do
114
+ @cube.dimension('invalid').should be_nil
115
+ end
116
+
117
+ it "should get dimension description" do
118
+ @cube.dimension('Gender').description.should == 'Gender description'
119
+ end
120
+
121
+ it "should get dimension caption" do
122
+ @cube.dimension('Gender').caption.should == 'Gender caption'
123
+ end
124
+
81
125
  it "should get dimension full name" do
82
126
  @cube.dimension('Gender').full_name.should == '[Gender]'
83
127
  end
@@ -86,11 +130,23 @@ describe "Cube" do
86
130
  @cube.dimension('Measures').should be_measures
87
131
  end
88
132
 
133
+ it "should get measures caption" do
134
+ @cube.dimension('Measures').caption.should == 'Measures caption'
135
+ end
136
+
89
137
  it "should get dimension type" do
90
138
  @cube.dimension('Gender').dimension_type.should == :standard
91
139
  @cube.dimension('Time').dimension_type.should == :time
92
140
  @cube.dimension('Measures').dimension_type.should == :measures
93
141
  end
142
+
143
+ it "should get dimension annotations" do
144
+ @cube.dimension('Customers').annotations.should == {'foo' => 'bar'}
145
+ end
146
+
147
+ it "should get dimension empty annotations" do
148
+ @cube.dimension('Gender').annotations.should == {}
149
+ end
94
150
  end
95
151
 
96
152
  describe "dimension hierarchies" do
@@ -104,6 +160,14 @@ describe "Cube" do
104
160
  hierarchies[0].name.should == 'Gender'
105
161
  end
106
162
 
163
+ it "should get hierarchy description" do
164
+ hierarchies = @cube.dimension('Gender').hierarchies.first.description.should == 'Gender hierarchy description'
165
+ end
166
+
167
+ it "should get hierarchy caption" do
168
+ hierarchies = @cube.dimension('Gender').hierarchies.first.caption.should == 'Gender hierarchy caption'
169
+ end
170
+
107
171
  it "should get hierarchy names" do
108
172
  @cube.dimension('Time').hierarchy_names.should == ['Time', 'Time.Weekly']
109
173
  end
@@ -112,6 +176,10 @@ describe "Cube" do
112
176
  @cube.dimension('Time').hierarchy('Time.Weekly').name.should == 'Time.Weekly'
113
177
  end
114
178
 
179
+ it "should return nil when getting hierarchy with invalid name" do
180
+ @cube.dimension('Time').hierarchy('invalid').should be_nil
181
+ end
182
+
115
183
  it "should get default hierarchy" do
116
184
  @cube.dimension('Time').hierarchy.name.should == 'Time'
117
185
  end
@@ -132,6 +200,14 @@ describe "Cube" do
132
200
  it "should get hierarchy level members count" do
133
201
  @cube.dimension('Gender').hierarchy.levels.map(&:members_count).should == [1, 2]
134
202
  end
203
+
204
+ it "should get hierarchy annotations" do
205
+ @cube.dimension('Customers').hierarchy.annotations.should == {'foo' => 'bar'}
206
+ end
207
+
208
+ it "should get hierarchy empty annotations" do
209
+ @cube.dimension('Gender').hierarchy.annotations.should == {}
210
+ end
135
211
  end
136
212
 
137
213
  describe "hierarchy values" do
@@ -177,11 +253,23 @@ describe "Cube" do
177
253
 
178
254
  end
179
255
 
180
- describe "level members" do
256
+ describe "hierarchy levels" do
181
257
  before(:all) do
182
258
  @cube = @olap.cube('Sales')
183
259
  end
184
260
 
261
+ it "should get level description" do
262
+ @cube.dimension('Gender').hierarchy.level('Gender').description.should == 'Gender level description'
263
+ end
264
+
265
+ it "should get level caption" do
266
+ @cube.dimension('Gender').hierarchy.level('Gender').caption.should == 'Gender level caption'
267
+ end
268
+
269
+ it "should return nil when getting level with invalid name" do
270
+ @cube.dimension('Gender').hierarchy.level('invalid').should be_nil
271
+ end
272
+
185
273
  it "should get primary hierarchy level members" do
186
274
  @cube.dimension('Customers').hierarchy.level('Country').members.
187
275
  map(&:name).should == ['Canada', 'Mexico', 'USA']
@@ -191,6 +279,15 @@ describe "Cube" do
191
279
  @cube.dimension('Time').hierarchy('Time.Weekly').level('Year').members.
192
280
  map(&:name).should == ['2010', '2011']
193
281
  end
282
+
283
+ it "should get level annotations" do
284
+ @cube.dimension('Customers').hierarchy.level('Country').annotations.should == {'foo' => 'bar'}
285
+ end
286
+
287
+ it "should get level empty annotations" do
288
+ @cube.dimension('Gender').hierarchy.level('Gender').annotations.should == {}
289
+ end
290
+
194
291
  end
195
292
 
196
293
  describe "members" do
@@ -203,6 +300,15 @@ describe "Cube" do
203
300
  @cube.member('[Customers].[USA].[OR]').name.should == 'OR'
204
301
  end
205
302
 
303
+ it "should return all member caption" do
304
+ @cube.member('[Gender].[All Genders]').caption.should == 'All Genders caption'
305
+ end
306
+
307
+ it "should return member caption from expression" do
308
+ @cube.member('[Gender].[F]').caption.should ==
309
+ (%w(mysql oracle).include?(MONDRIAN_DRIVER) ? 'dummy' : 'F')
310
+ end
311
+
206
312
  it "should not return member for invalid full name" do
207
313
  @cube.member('[Gender].[invalid]').should be_nil
208
314
  end
@@ -250,6 +356,10 @@ describe "Cube" do
250
356
  @cube.member('[Customers].[Non-USA]').should be_calculated
251
357
  end
252
358
 
359
+ it "should not be calculated in query when calculated member defined in schema" do
360
+ @cube.member('[Customers].[Non-USA]').should_not be_calculated_in_query
361
+ end
362
+
253
363
  it "should not be calculated when normal member" do
254
364
  @cube.member('[Customers].[USA]').should_not be_calculated
255
365
  end
@@ -274,6 +384,26 @@ describe "Cube" do
274
384
  @cube.member('[Time].[2011]').dimension_type.should == :time
275
385
  end
276
386
 
387
+ it "should be visble when member is visible" do
388
+ @cube.member('[Measures].[Store Sales]').should be_visible
389
+ end
390
+
391
+ it "should not be visble when member is not visible" do
392
+ @cube.member('[Measures].[Store Cost]').should_not be_visible
393
+ end
394
+
395
+ it "should get measure annotations" do
396
+ @cube.member('[Measures].[Unit Sales]').annotations.should == {'foo' => 'bar'}
397
+ end
398
+
399
+ it "should get measure empty annotations" do
400
+ @cube.member('[Measures].[Store Sales]').annotations.should == {}
401
+ end
402
+
403
+ it "should get member empty annotations" do
404
+ @cube.member('[Customers].[USA]').annotations.should == {}
405
+ end
406
+
277
407
  end
278
408
 
279
409
  end
@@ -56,11 +56,11 @@
56
56
  <SQL dialect="oracle">
57
57
  fname || ' ' || lname
58
58
  </SQL>
59
- <SQL dialect="postgres">
59
+ <SQL dialect="postgresql">
60
60
  "fname" || ' ' || "lname"
61
61
  </SQL>
62
62
  <SQL dialect="mysql">
63
- CONCAT(`customer`.`fname`, ' ', `customer`.`lname`)
63
+ CONCAT(`customers`.`fname`, ' ', `customers`.`lname`)
64
64
  </SQL>
65
65
  <SQL dialect="luciddb">
66
66
  "fname" || ' ' || "lname"
@@ -73,11 +73,11 @@ fullname
73
73
  <SQL dialect="oracle">
74
74
  fname || ' ' || lname
75
75
  </SQL>
76
- <SQL dialect="postgres">
76
+ <SQL dialect="postgresql">
77
77
  "fname" || ' ' || "lname"
78
78
  </SQL>
79
79
  <SQL dialect="mysql">
80
- CONCAT(`customer`.`fname`, ' ', `customer`.`lname`)
80
+ CONCAT(`customers`.`fname`, ' ', `customers`.`lname`)
81
81
  </SQL>
82
82
  <SQL dialect="luciddb">
83
83
  "fname" || ' ' || "lname"
@@ -0,0 +1,53 @@
1
+ require "spec_helper"
2
+
3
+ describe "Mondrian features" do
4
+ before(:all) do
5
+ @schema = Mondrian::OLAP::Schema.define do
6
+ cube 'Sales' do
7
+ table 'sales'
8
+ dimension 'Gender', :foreign_key => 'customer_id' do
9
+ hierarchy :has_all => true, :primary_key => 'id' do
10
+ table 'customers'
11
+ level 'Gender', :column => 'gender', :unique_members => true
12
+ end
13
+ end
14
+ dimension 'Customers', :foreign_key => 'customer_id' do
15
+ hierarchy :has_all => true, :all_member_name => 'All Customers', :primary_key => 'id' do
16
+ table 'customers'
17
+ level 'Country', :column => 'country', :unique_members => true
18
+ level 'State Province', :column => 'state_province', :unique_members => true
19
+ level 'City', :column => 'city', :unique_members => false
20
+ level 'Name', :column => 'fullname', :unique_members => true
21
+ end
22
+ end
23
+ dimension 'Time', :foreign_key => 'time_id', :type => 'TimeDimension' do
24
+ hierarchy :has_all => false, :primary_key => 'id' do
25
+ table 'time'
26
+ level 'Year', :column => 'the_year', :type => 'Numeric', :unique_members => true, :level_type => 'TimeYears'
27
+ level 'Quarter', :column => 'quarter', :unique_members => false, :level_type => 'TimeQuarters'
28
+ level 'Month', :column => 'month_of_year', :type => 'Numeric', :unique_members => false, :level_type => 'TimeMonths'
29
+ end
30
+ hierarchy 'Weekly', :has_all => false, :primary_key => 'id' do
31
+ table 'time'
32
+ level 'Year', :column => 'the_year', :type => 'Numeric', :unique_members => true, :level_type => 'TimeYears'
33
+ level 'Week', :column => 'weak_of_year', :type => 'Numeric', :unique_members => false, :level_type => 'TimeWeeks'
34
+ end
35
+ end
36
+ measure 'Unit Sales', :column => 'unit_sales', :aggregator => 'sum'
37
+ measure 'Store Sales', :column => 'store_sales', :aggregator => 'sum'
38
+ end
39
+ end
40
+ @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge :schema => @schema)
41
+ end
42
+
43
+ # test for http://jira.pentaho.com/browse/MONDRIAN-1050
44
+ it "should order rows by DateTime expression" do
45
+ lambda do
46
+ @olap.from('Sales').
47
+ columns('[Measures].[Unit Sales]').
48
+ rows('[Customers].children').order('Now()', :asc).
49
+ execute
50
+ end.should_not raise_error
51
+ end
52
+
53
+ end
@@ -165,6 +165,13 @@ describe "Query" do
165
165
  end
166
166
  end
167
167
 
168
+ describe "nonempty_crossjoin" do
169
+ it "should do nonempty_crossjoin of several dimensions" do
170
+ @query.rows('[Product].children').nonempty_crossjoin('[Customers].[Canada]', '[Customers].[USA]')
171
+ @query.rows.should == [:nonempty_crossjoin, ['[Product].children'], ['[Customers].[Canada]', '[Customers].[USA]']]
172
+ end
173
+ end
174
+
168
175
  describe "nonempty" do
169
176
  it "should limit to set of members with nonempty values" do
170
177
  @query.rows('[Product].children').nonempty
@@ -225,6 +232,12 @@ describe "Query" do
225
232
  [:hierarchize, ['[Customers].[Country].Members', '[Customers].[City].Members']]]
226
233
  end
227
234
 
235
+ it "should hierarchize last set of nonempty_crossjoin" do
236
+ @query.rows('[Product].children').nonempty_crossjoin('[Customers].[Country].Members', '[Customers].[City].Members').hierarchize
237
+ @query.rows.should == [:nonempty_crossjoin, ['[Product].children'],
238
+ [:hierarchize, ['[Customers].[Country].Members', '[Customers].[City].Members']]]
239
+ end
240
+
228
241
  it "should hierarchize all crossjoin" do
229
242
  @query.rows('[Product].children').crossjoin('[Customers].[Country].Members', '[Customers].[City].Members').hierarchize_all
230
243
  @query.rows.should == [:hierarchize, [:crossjoin, ['[Product].children'],
@@ -249,6 +262,12 @@ describe "Query" do
249
262
  @query.rows.should == [:crossjoin, ['[Product].children'],
250
263
  [:except, ['[Customers].[Country].Members'], ['[Customers].[USA]']]]
251
264
  end
265
+
266
+ it "should except from last set of nonempty_crossjoin" do
267
+ @query.rows('[Product].children').nonempty_crossjoin('[Customers].[Country].Members').except('[Customers].[USA]')
268
+ @query.rows.should == [:nonempty_crossjoin, ['[Product].children'],
269
+ [:except, ['[Customers].[Country].Members'], ['[Customers].[USA]']]]
270
+ end
252
271
  end
253
272
 
254
273
  describe "filter" do
@@ -283,6 +302,11 @@ describe "Query" do
283
302
  @query.where('[Customers].[USA]').crossjoin('[Time].[2011].[Q1]', '[Time].[2011].[Q2]')
284
303
  @query.where.should == [:crossjoin, ['[Customers].[USA]'], ['[Time].[2011].[Q1]', '[Time].[2011].[Q2]']]
285
304
  end
305
+
306
+ it "should do nonempty_crossjoin of where conditions" do
307
+ @query.where('[Customers].[USA]').nonempty_crossjoin('[Time].[2011].[Q1]', '[Time].[2011].[Q2]')
308
+ @query.where.should == [:nonempty_crossjoin, ['[Customers].[USA]'], ['[Time].[2011].[Q1]', '[Time].[2011].[Q2]']]
309
+ end
286
310
  end
287
311
 
288
312
  describe "with member" do
@@ -329,6 +353,15 @@ describe "Query" do
329
353
  ]
330
354
  ]
331
355
  end
356
+
357
+ it "should accept definition with nonempty_crossjoin" do
358
+ @query.with_set('SelectedRows').as('[Product].children').nonempty_crossjoin('[Customers].[Canada]', '[Customers].[USA]')
359
+ @query.with.should == [
360
+ [ :set, 'SelectedRows',
361
+ [:nonempty_crossjoin, ['[Product].children'], ['[Customers].[Canada]', '[Customers].[USA]']]
362
+ ]
363
+ ]
364
+ end
332
365
  end
333
366
 
334
367
  describe "to MDX" do
@@ -380,6 +413,18 @@ describe "Query" do
380
413
  SQL
381
414
  end
382
415
 
416
+ it "should return query with nonempty_crossjoin" do
417
+ @query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
418
+ rows('[Product].children').nonempty_crossjoin('[Customers].[Canada]', '[Customers].[USA]').
419
+ where('[Time].[2010].[Q1]').
420
+ to_mdx.should be_like <<-SQL
421
+ SELECT {[Measures].[Unit Sales], [Measures].[Store Sales]} ON COLUMNS,
422
+ NONEMPTYCROSSJOIN([Product].children, {[Customers].[Canada], [Customers].[USA]}) ON ROWS
423
+ FROM [Sales]
424
+ WHERE ([Time].[2010].[Q1])
425
+ SQL
426
+ end
427
+
383
428
  it "should return query with where with several same dimension members" do
384
429
  @query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
385
430
  rows('[Product].children').
@@ -392,6 +437,18 @@ describe "Query" do
392
437
  SQL
393
438
  end
394
439
 
440
+ it "should return query with where with several different dimension members returned by function" do
441
+ @query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
442
+ rows('[Product].children').
443
+ where('Head([Customers].Members).Item(0)', 'Head([Gender].Members).Item(0)').
444
+ to_mdx.should be_like <<-SQL
445
+ SELECT {[Measures].[Unit Sales], [Measures].[Store Sales]} ON COLUMNS,
446
+ [Product].children ON ROWS
447
+ FROM [Sales]
448
+ WHERE (Head([Customers].Members).Item(0), Head([Gender].Members).Item(0))
449
+ SQL
450
+ end
451
+
395
452
  it "should return query with where with crossjoin" do
396
453
  @query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
397
454
  rows('[Product].children').
@@ -400,7 +457,19 @@ describe "Query" do
400
457
  SELECT {[Measures].[Unit Sales], [Measures].[Store Sales]} ON COLUMNS,
401
458
  [Product].children ON ROWS
402
459
  FROM [Sales]
403
- WHERE CROSSJOIN([Customers].[USA], {[Time].[2011].[Q1], [Time].[2011].[Q2]})
460
+ WHERE CROSSJOIN({[Customers].[USA]}, {[Time].[2011].[Q1], [Time].[2011].[Q2]})
461
+ SQL
462
+ end
463
+
464
+ it "should return query with where with nonempty_crossjoin" do
465
+ @query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
466
+ rows('[Product].children').
467
+ where('[Customers].[USA]').nonempty_crossjoin('[Time].[2011].[Q1]', '[Time].[2011].[Q2]').
468
+ to_mdx.should be_like <<-SQL
469
+ SELECT {[Measures].[Unit Sales], [Measures].[Store Sales]} ON COLUMNS,
470
+ [Product].children ON ROWS
471
+ FROM [Sales]
472
+ WHERE NONEMPTYCROSSJOIN({[Customers].[USA]}, {[Time].[2011].[Q1], [Time].[2011].[Q2]})
404
473
  SQL
405
474
  end
406
475
 
@@ -501,7 +570,7 @@ describe "Query" do
501
570
  rows('[Customers].[Country].Members').except('[Customers].[USA]').
502
571
  to_mdx.should be_like <<-SQL
503
572
  SELECT {[Measures].[Unit Sales], [Measures].[Store Sales]} ON COLUMNS,
504
- EXCEPT([Customers].[Country].Members, [Customers].[USA]) ON ROWS
573
+ EXCEPT([Customers].[Country].Members, {[Customers].[USA]}) ON ROWS
505
574
  FROM [Sales]
506
575
  SQL
507
576
  end
@@ -540,21 +609,21 @@ describe "Query" do
540
609
  @query.
541
610
  with_member('[Measures].[ProfitPct]').
542
611
  as('Val((Measures.[Store Sales] - Measures.[Store Cost]) / Measures.[Store Sales])',
543
- :solve_order => 1, :format_string => 'Percent').
612
+ :solve_order => 1, :format_string => 'Percent', :caption => 'Profit %').
544
613
  with_member('[Measures].[ProfitValue]').
545
614
  as('[Measures].[Store Sales] * [Measures].[ProfitPct]',
546
- :solve_order => 2, :format_string => 'Currency').
615
+ :solve_order => 2, :cell_formatter => 'CurrencyFormatter').
547
616
  columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
548
617
  rows('[Product].children').
549
618
  where('[Time].[2010].[Q1]', '[Customers].[USA].[CA]').
550
619
  to_mdx.should be_like <<-SQL
551
620
  WITH
552
- MEMBER [Measures].[ProfitPct] AS
621
+ MEMBER [Measures].[ProfitPct] AS
553
622
  'Val((Measures.[Store Sales] - Measures.[Store Cost]) / Measures.[Store Sales])',
554
- SOLVE_ORDER = 1, FORMAT_STRING = 'Percent'
555
- MEMBER [Measures].[ProfitValue] AS
623
+ SOLVE_ORDER = 1, FORMAT_STRING = 'Percent', $caption = 'Profit %'
624
+ MEMBER [Measures].[ProfitValue] AS
556
625
  '[Measures].[Store Sales] * [Measures].[ProfitPct]',
557
- SOLVE_ORDER = 2, FORMAT_STRING = 'Currency'
626
+ SOLVE_ORDER = 2, CELL_FORMATTER = 'rubyobj.Mondrian.OLAP.Schema.CellFormatter.CurrencyFormatterUdf'
558
627
  SELECT {[Measures].[Unit Sales], [Measures].[Store Sales]} ON COLUMNS,
559
628
  [Product].children ON ROWS
560
629
  FROM [Sales]
@@ -571,11 +640,11 @@ describe "Query" do
571
640
  rows('SelectedRows').
572
641
  to_mdx.should be_like <<-SQL
573
642
  WITH
574
- SET SelectedRows AS
643
+ SET SelectedRows AS
575
644
  'CROSSJOIN([Product].children, {[Customers].[Canada], [Customers].[USA]})'
576
645
  MEMBER [Measures].[Profit] AS
577
646
  '[Measures].[Store Sales] - [Measures].[Store Cost]'
578
- SELECT [Measures].[Profit] ON COLUMNS,
647
+ SELECT {[Measures].[Profit]} ON COLUMNS,
579
648
  SelectedRows ON ROWS
580
649
  FROM [Sales]
581
650
  SQL
@@ -612,4 +681,218 @@ describe "Query" do
612
681
 
613
682
  end
614
683
 
615
- end
684
+ describe "errors" do
685
+ before(:each) do
686
+ @query = @olap.from('Sales')
687
+ end
688
+
689
+ it "should raise error when invalid MDX statement" do
690
+ expect {
691
+ @olap.execute "SELECT dummy FROM"
692
+ }.to raise_error {|e|
693
+ e.should be_kind_of(Mondrian::OLAP::Error)
694
+ e.message.should == 'org.olap4j.OlapException: mondrian gave exception while parsing query'
695
+ e.root_cause_message.should == "Syntax error at line 1, column 14, token 'FROM'"
696
+ }
697
+ end
698
+
699
+ it "should raise error when invalid MDX object" do
700
+ expect {
701
+ @query.columns('[Measures].[Dummy]').execute
702
+ }.to raise_error {|e|
703
+ e.should be_kind_of(Mondrian::OLAP::Error)
704
+ e.message.should == 'org.olap4j.OlapException: mondrian gave exception while parsing query'
705
+ e.root_cause_message.should == "MDX object '[Measures].[Dummy]' not found in cube 'Sales'"
706
+ }
707
+ end
708
+
709
+ it "should raise error when invalid formula" do
710
+ expect {
711
+ @query.with_member('[Measures].[Dummy]').as('Dummy(123)').
712
+ columns('[Measures].[Dummy]').execute
713
+ }.to raise_error {|e|
714
+ e.should be_kind_of(Mondrian::OLAP::Error)
715
+ e.message.should == 'org.olap4j.OlapException: mondrian gave exception while parsing query'
716
+ e.root_cause_message.should == "No function matches signature 'Dummy(<Numeric Expression>)'"
717
+ }
718
+ end
719
+
720
+ end
721
+
722
+ describe "drill through cell" do
723
+ before(:all) do
724
+ @query = @olap.from('Sales')
725
+ @result = @query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
726
+ rows('[Product].children').
727
+ where('[Time].[2010].[Q1]', '[Customers].[USA].[CA]').
728
+ execute
729
+ @drill_through = @result.drill_through(:row => 0, :column => 0)
730
+ end
731
+
732
+ it "should return column types" do
733
+ @drill_through.column_types.should == [
734
+ :INT, :VARCHAR, :INT, :INT, :INT,
735
+ :VARCHAR, :VARCHAR, :VARCHAR, :VARCHAR, :VARCHAR, :VARCHAR,
736
+ :VARCHAR, :VARCHAR, :VARCHAR, :INT,
737
+ :VARCHAR,
738
+ :DECIMAL
739
+ ]
740
+ end if MONDRIAN_DRIVER == 'mysql'
741
+
742
+ it "should return column names" do
743
+ # ignore calculated customer full name column name which is shown differently on each database
744
+ @drill_through.column_names[0..12].should == %w(
745
+ the_year quarter month_of_year week_of_year day_of_month
746
+ product_family product_department product_category product_subcategory brand_name product_name
747
+ state_province city
748
+ )
749
+ @drill_through.column_names[14..16].should == %w(
750
+ id gender unit_sales
751
+ )
752
+ end if %w(mysql postgresql).include? MONDRIAN_DRIVER
753
+
754
+ it "should return table names" do
755
+ @drill_through.table_names.should == [
756
+ "time", "time", "time", "time", "time",
757
+ "product_classes", "product_classes", "product_classes", "product_classes", "products", "products",
758
+ "customers", "customers", "", "customers",
759
+ "customers",
760
+ "sales"
761
+ ]
762
+ end if %w(mysql postgresql).include? MONDRIAN_DRIVER
763
+
764
+ it "should return column labels" do
765
+ @drill_through.column_labels.should == [
766
+ "Year", "Quarter", "Month", "Week", "Day",
767
+ "Product Family", "Product Department", "Product Category", "Product Subcategory", "Brand Name", "Product Name",
768
+ "State Province", "City", "Name", "Name (Key)",
769
+ "Gender",
770
+ "Unit Sales"
771
+ ]
772
+ end
773
+
774
+ it "should return row values" do
775
+ @drill_through.rows.size.should == 15 # number of generated test rows
776
+ end
777
+
778
+ it "should return correct row value types" do
779
+ @drill_through.rows.first.map(&:class).should ==
780
+ case MONDRIAN_DRIVER
781
+ when "oracle"
782
+ [
783
+ BigDecimal, String, BigDecimal, BigDecimal, BigDecimal,
784
+ String, String, String, String, String, String,
785
+ String, String, String, BigDecimal,
786
+ String,
787
+ BigDecimal
788
+ ]
789
+ when 'mssql'
790
+ [
791
+ Fixnum, String, Fixnum, Fixnum, Fixnum,
792
+ String, String, String, String, String, String,
793
+ String, String, String, BigDecimal,
794
+ String,
795
+ BigDecimal
796
+ ]
797
+ else
798
+ [
799
+ Fixnum, String, Fixnum, Fixnum, Fixnum,
800
+ String, String, String, String, String, String,
801
+ String, String, String, Fixnum,
802
+ String,
803
+ BigDecimal
804
+ ]
805
+ end
806
+ end
807
+
808
+ it "should return only specified max rows" do
809
+ drill_through = @result.drill_through(:row => 0, :column => 0, :max_rows => 10)
810
+ drill_through.rows.size.should == 10
811
+ end
812
+ end
813
+
814
+ describe "drill through cell with return" do
815
+ before(:all) do
816
+ @query = @olap.from('Sales')
817
+ @result = @query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
818
+ rows('[Product].children').
819
+ # where('[Time].[2010].[Q1]', '[Customers].[USA].[CA]').
820
+ where('[Time].[2010].[Q1]').
821
+ execute
822
+ end
823
+
824
+ it "should return only specified fields in specified order" do
825
+ @drill_through = @result.drill_through(:row => 0, :column => 0, :return => [
826
+ '[Time].[Month]',
827
+ '[Customers].[City]',
828
+ '[Product].[Product Family]',
829
+ '[Measures].[Unit Sales]', '[Measures].[Store Sales]'
830
+ ])
831
+ @drill_through.column_labels.should == [
832
+ "Month",
833
+ "City",
834
+ "Product Family",
835
+ "Unit Sales", "Store Sales"
836
+ ]
837
+ end
838
+
839
+ it "should return only nonempty measures" do
840
+ @drill_through = @result.drill_through(:row => 0, :column => 0,
841
+ :return => "[Measures].[Unit Sales], [Measures].[Store Sales]",
842
+ :nonempty => "[Measures].[Unit Sales]"
843
+ )
844
+ @drill_through.column_labels.should == [
845
+ "Unit Sales", "Store Sales"
846
+ ]
847
+ @drill_through.rows.all?{|r| r.any?{|c| c}}.should be_true
848
+ end
849
+
850
+ end
851
+
852
+ describe "drill through statement" do
853
+ before(:all) do
854
+ @query = @olap.from('Sales').
855
+ columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
856
+ rows('[Product].children').
857
+ where('[Time].[2010].[Q1]', '[Customers].[USA].[CA]')
858
+ end
859
+
860
+ it "should return column labels" do
861
+ @drill_through = @query.execute_drill_through
862
+ @drill_through.column_labels.should == [
863
+ "Year", "Quarter", "Month", "Week", "Day",
864
+ "Product Family", "Product Department", "Product Category", "Product Subcategory", "Brand Name", "Product Name",
865
+ "State Province", "City", "Name", "Name (Key)",
866
+ "Gender",
867
+ "Unit Sales"
868
+ ]
869
+ end
870
+
871
+ it "should return row values" do
872
+ @drill_through = @query.execute_drill_through
873
+ @drill_through.rows.size.should == 15 # number of generated test rows
874
+ end
875
+
876
+ it "should return only specified max rows" do
877
+ drill_through = @query.execute_drill_through(:max_rows => 10)
878
+ drill_through.rows.size.should == 10
879
+ end
880
+
881
+ it "should return only specified fields" do
882
+ @drill_through = @query.execute_drill_through(:return => [
883
+ '[Time].[Month]',
884
+ '[Product].[Product Family]',
885
+ '[Customers].[City]',
886
+ '[Measures].[Unit Sales]', '[Measures].[Store Sales]'
887
+ ])
888
+ @drill_through.column_labels.should == [
889
+ "Month",
890
+ "Product Family",
891
+ "City",
892
+ "Unit Sales", "Store Sales"
893
+ ]
894
+ end
895
+
896
+ end
897
+
898
+ end