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.
- checksums.yaml +5 -5
- data/Changelog.md +86 -0
- data/LICENSE-Mondrian.txt +87 -0
- data/LICENSE.txt +1 -1
- data/README.md +43 -40
- data/VERSION +1 -1
- data/lib/mondrian/jars/commons-collections-3.2.2.jar +0 -0
- data/lib/mondrian/jars/commons-dbcp-1.4.jar +0 -0
- data/lib/mondrian/jars/commons-io-2.2.jar +0 -0
- data/lib/mondrian/jars/commons-lang-2.6.jar +0 -0
- data/lib/mondrian/jars/commons-logging-1.2.jar +0 -0
- data/lib/mondrian/jars/commons-pool-1.5.7.jar +0 -0
- data/lib/mondrian/jars/commons-vfs2-2.2.jar +0 -0
- data/lib/mondrian/jars/eigenbase-xom-1.3.5.jar +0 -0
- data/lib/mondrian/jars/guava-17.0.jar +0 -0
- data/lib/mondrian/jars/log4j-1.2.17.jar +0 -0
- data/lib/mondrian/jars/log4j.properties +2 -4
- data/lib/mondrian/jars/mondrian-9.1.0.0.jar +0 -0
- data/lib/mondrian/jars/olap4j-1.2.0.jar +0 -0
- data/lib/mondrian/olap/connection.rb +252 -67
- data/lib/mondrian/olap/cube.rb +63 -2
- data/lib/mondrian/olap/error.rb +37 -8
- data/lib/mondrian/olap/query.rb +41 -21
- data/lib/mondrian/olap/result.rb +163 -44
- data/lib/mondrian/olap/schema.rb +42 -3
- data/lib/mondrian/olap/schema_element.rb +25 -6
- data/lib/mondrian/olap/schema_udf.rb +21 -16
- data/spec/connection_role_spec.rb +69 -13
- data/spec/connection_spec.rb +3 -2
- data/spec/cube_cache_control_spec.rb +261 -0
- data/spec/cube_spec.rb +32 -4
- data/spec/fixtures/MondrianTest.xml +1 -6
- data/spec/fixtures/MondrianTestOracle.xml +1 -6
- data/spec/mondrian_spec.rb +71 -1
- data/spec/query_spec.rb +323 -25
- data/spec/rake_tasks.rb +253 -159
- data/spec/schema_definition_spec.rb +314 -61
- data/spec/spec_helper.rb +115 -45
- data/spec/support/data/customers.csv +10902 -0
- data/spec/support/data/product_classes.csv +101 -0
- data/spec/support/data/products.csv +101 -0
- data/spec/support/data/sales.csv +101 -0
- data/spec/support/data/time.csv +731 -0
- metadata +126 -124
- data/LICENSE-Mondrian.html +0 -259
- data/lib/mondrian/jars/commons-collections-3.2.jar +0 -0
- data/lib/mondrian/jars/commons-dbcp-1.2.1.jar +0 -0
- data/lib/mondrian/jars/commons-logging-1.1.1.jar +0 -0
- data/lib/mondrian/jars/commons-pool-1.2.jar +0 -0
- data/lib/mondrian/jars/commons-vfs-1.0.jar +0 -0
- data/lib/mondrian/jars/eigenbase-xom-1.3.1.jar +0 -0
- data/lib/mondrian/jars/log4j-1.2.14.jar +0 -0
- data/lib/mondrian/jars/mondrian.jar +0 -0
- data/lib/mondrian/jars/olap4j-1.0.1.539.jar +0 -0
data/spec/cube_spec.rb
CHANGED
@@ -10,17 +10,21 @@ describe "Cube" do
|
|
10
10
|
caption 'Sales caption'
|
11
11
|
annotations :foo => 'bar'
|
12
12
|
table 'sales'
|
13
|
+
visible true
|
13
14
|
dimension 'Gender', :foreign_key => 'customer_id' do
|
14
15
|
description 'Gender description'
|
15
16
|
caption 'Gender caption'
|
17
|
+
visible true
|
16
18
|
hierarchy :has_all => true, :primary_key => 'id' do
|
17
19
|
description 'Gender hierarchy description'
|
18
20
|
caption 'Gender hierarchy caption'
|
19
21
|
all_member_name 'All Genders'
|
20
22
|
all_member_caption 'All Genders caption'
|
21
23
|
table 'customers'
|
24
|
+
visible true
|
22
25
|
level 'Gender', :column => 'gender', :unique_members => true,
|
23
26
|
:description => 'Gender level description', :caption => 'Gender level caption' do
|
27
|
+
visible true
|
24
28
|
# Dimension values SQL generated by caption_expression fails on PostgreSQL and MS SQL
|
25
29
|
if %w(mysql oracle).include?(MONDRIAN_DRIVER)
|
26
30
|
caption_expression do
|
@@ -56,6 +60,10 @@ describe "Cube" do
|
|
56
60
|
level 'Week', :column => 'weak_of_year', :type => 'Numeric', :unique_members => false, :level_type => 'TimeWeeks'
|
57
61
|
end
|
58
62
|
end
|
63
|
+
calculated_member 'Last week' do
|
64
|
+
hierarchy '[Time.Weekly]'
|
65
|
+
formula 'Tail([Time.Weekly].[Week].Members).Item(0)'
|
66
|
+
end
|
59
67
|
measure 'Unit Sales', :column => 'unit_sales', :aggregator => 'sum', :annotations => {:foo => 'bar'}
|
60
68
|
measure 'Store Sales', :column => 'store_sales', :aggregator => 'sum'
|
61
69
|
measure 'Store Cost', :column => 'store_cost', :aggregator => 'sum', :visible => false
|
@@ -92,6 +100,10 @@ describe "Cube" do
|
|
92
100
|
@olap.cube('Sales').annotations.should == {'foo' => 'bar'}
|
93
101
|
end
|
94
102
|
|
103
|
+
it "should be visible" do
|
104
|
+
@olap.cube('Sales').should be_visible
|
105
|
+
end
|
106
|
+
|
95
107
|
describe "dimensions" do
|
96
108
|
before(:all) do
|
97
109
|
@cube = @olap.cube('Sales')
|
@@ -147,6 +159,11 @@ describe "Cube" do
|
|
147
159
|
it "should get dimension empty annotations" do
|
148
160
|
@cube.dimension('Gender').annotations.should == {}
|
149
161
|
end
|
162
|
+
|
163
|
+
it "should be visible" do
|
164
|
+
@cube.dimension('Gender').should be_visible
|
165
|
+
end
|
166
|
+
|
150
167
|
end
|
151
168
|
|
152
169
|
describe "dimension hierarchies" do
|
@@ -208,6 +225,11 @@ describe "Cube" do
|
|
208
225
|
it "should get hierarchy empty annotations" do
|
209
226
|
@cube.dimension('Gender').hierarchy.annotations.should == {}
|
210
227
|
end
|
228
|
+
|
229
|
+
it "should be visible" do
|
230
|
+
@cube.dimension('Gender').hierarchies.first.should be_visible
|
231
|
+
end
|
232
|
+
|
211
233
|
end
|
212
234
|
|
213
235
|
describe "hierarchy values" do
|
@@ -288,6 +310,10 @@ describe "Cube" do
|
|
288
310
|
@cube.dimension('Gender').hierarchy.level('Gender').annotations.should == {}
|
289
311
|
end
|
290
312
|
|
313
|
+
it "should be visible" do
|
314
|
+
@cube.dimension('Gender').hierarchy.level('Gender').should be_visible
|
315
|
+
end
|
316
|
+
|
291
317
|
end
|
292
318
|
|
293
319
|
describe "members" do
|
@@ -356,6 +382,10 @@ describe "Cube" do
|
|
356
382
|
@cube.member('[Customers].[Non-USA]').should be_calculated
|
357
383
|
end
|
358
384
|
|
385
|
+
it "should be calculated when member is calculated in non-default hierarchy" do
|
386
|
+
@cube.member('[Time.Weekly].[Last week]').should be_calculated
|
387
|
+
end
|
388
|
+
|
359
389
|
it "should not be calculated in query when calculated member defined in schema" do
|
360
390
|
@cube.member('[Customers].[Non-USA]').should_not be_calculated_in_query
|
361
391
|
end
|
@@ -384,11 +414,11 @@ describe "Cube" do
|
|
384
414
|
@cube.member('[Time].[2011]').dimension_type.should == :time
|
385
415
|
end
|
386
416
|
|
387
|
-
it "should be
|
417
|
+
it "should be visible when member is visible" do
|
388
418
|
@cube.member('[Measures].[Store Sales]').should be_visible
|
389
419
|
end
|
390
420
|
|
391
|
-
it "should not be
|
421
|
+
it "should not be visible when member is not visible" do
|
392
422
|
@cube.member('[Measures].[Store Cost]').should_not be_visible
|
393
423
|
end
|
394
424
|
|
@@ -403,7 +433,5 @@ describe "Cube" do
|
|
403
433
|
it "should get member empty annotations" do
|
404
434
|
@cube.member('[Customers].[USA]').annotations.should == {}
|
405
435
|
end
|
406
|
-
|
407
436
|
end
|
408
|
-
|
409
437
|
end
|
@@ -61,9 +61,6 @@ fname || ' ' || lname
|
|
61
61
|
</SQL>
|
62
62
|
<SQL dialect="mysql">
|
63
63
|
CONCAT(`customers`.`fname`, ' ', `customers`.`lname`)
|
64
|
-
</SQL>
|
65
|
-
<SQL dialect="luciddb">
|
66
|
-
"fname" || ' ' || "lname"
|
67
64
|
</SQL>
|
68
65
|
<SQL dialect="generic">
|
69
66
|
fullname
|
@@ -78,15 +75,13 @@ fname || ' ' || lname
|
|
78
75
|
</SQL>
|
79
76
|
<SQL dialect="mysql">
|
80
77
|
CONCAT(`customers`.`fname`, ' ', `customers`.`lname`)
|
81
|
-
</SQL>
|
82
|
-
<SQL dialect="luciddb">
|
83
|
-
"fname" || ' ' || "lname"
|
84
78
|
</SQL>
|
85
79
|
<SQL dialect="generic">
|
86
80
|
fullname
|
87
81
|
</SQL>
|
88
82
|
</OrdinalExpression>
|
89
83
|
<Property name="Gender" column="gender"/>
|
84
|
+
<Property name="Description" column="description"/>
|
90
85
|
</Level>
|
91
86
|
</Hierarchy>
|
92
87
|
</Dimension>
|
@@ -61,9 +61,6 @@ fname || ' ' || lname
|
|
61
61
|
</SQL>
|
62
62
|
<SQL dialect="mysql">
|
63
63
|
CONCAT(`customer`.`fname`, ' ', `customer`.`lname`)
|
64
|
-
</SQL>
|
65
|
-
<SQL dialect="luciddb">
|
66
|
-
fname || ' ' || lname
|
67
64
|
</SQL>
|
68
65
|
<SQL dialect="generic">
|
69
66
|
FULLNAME
|
@@ -78,15 +75,13 @@ fname || ' ' || lname
|
|
78
75
|
</SQL>
|
79
76
|
<SQL dialect="mysql">
|
80
77
|
CONCAT(`customer`.`fname`, ' ', `customer`.`lname`)
|
81
|
-
</SQL>
|
82
|
-
<SQL dialect="luciddb">
|
83
|
-
fname || ' ' || lname
|
84
78
|
</SQL>
|
85
79
|
<SQL dialect="generic">
|
86
80
|
FULLNAME
|
87
81
|
</SQL>
|
88
82
|
</OrdinalExpression>
|
89
83
|
<Property name="Gender" column="GENDER"/>
|
84
|
+
<Property name="Description" column="DESCRIPTION"/>
|
90
85
|
</Level>
|
91
86
|
</Hierarchy>
|
92
87
|
</Dimension>
|
data/spec/mondrian_spec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
require "spec_helper"
|
2
4
|
|
3
5
|
describe "Mondrian features" do
|
@@ -11,13 +13,39 @@ describe "Mondrian features" do
|
|
11
13
|
level 'Gender', :column => 'gender', :unique_members => true
|
12
14
|
end
|
13
15
|
end
|
16
|
+
dimension 'Promotions', :foreign_key => 'promotion_id' do
|
17
|
+
hierarchy :has_all => true, :primary_key => 'id' do
|
18
|
+
table 'promotions'
|
19
|
+
level 'Promotion', :column => 'id', :name_column => 'promotion', :unique_members => true, :ordinal_column => 'sequence', :type => 'Numeric'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
dimension 'Linked Promotions', :foreign_key => 'customer_id' do
|
23
|
+
hierarchy :has_all => true, :primary_key => 'id', :primary_key_table => 'customers' do
|
24
|
+
join :left_key => 'related_fullname', :right_key => 'fullname' do
|
25
|
+
table "customers"
|
26
|
+
join :left_key => "promotion_id", :right_key => "id" do
|
27
|
+
table "customers", :alias => "customers_bt"
|
28
|
+
table "promotions"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
level 'Promotion', :column => 'id', :name_column => 'promotion', :unique_members => true, :table => 'promotions', :ordinal_column => 'sequence', :type => 'Numeric', :approx_row_count => 10
|
32
|
+
end
|
33
|
+
end
|
14
34
|
dimension 'Customers', :foreign_key => 'customer_id' do
|
15
35
|
hierarchy :has_all => true, :all_member_name => 'All Customers', :primary_key => 'id' do
|
16
36
|
table 'customers'
|
17
37
|
level 'Country', :column => 'country', :unique_members => true
|
18
38
|
level 'State Province', :column => 'state_province', :unique_members => true
|
19
39
|
level 'City', :column => 'city', :unique_members => false
|
20
|
-
level 'Name', :column => 'fullname', :unique_members => true
|
40
|
+
level 'Name', :column => 'fullname', :unique_members => true do
|
41
|
+
property 'Related name', :column => 'related_fullname', :type => "String"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
hierarchy 'ID', :has_all => true, :all_member_name => 'All Customers', :primary_key => 'id' do
|
45
|
+
table 'customers'
|
46
|
+
level 'ID', :column => 'id', :type => 'Numeric', :internal_type => 'long', :unique_members => true do
|
47
|
+
property 'Name', :column => 'fullname'
|
48
|
+
end
|
21
49
|
end
|
22
50
|
end
|
23
51
|
dimension 'Time', :foreign_key => 'time_id', :type => 'TimeDimension' do
|
@@ -50,4 +78,46 @@ describe "Mondrian features" do
|
|
50
78
|
end.should_not raise_error
|
51
79
|
end
|
52
80
|
|
81
|
+
# test for https://jira.pentaho.com/browse/MONDRIAN-2683
|
82
|
+
it "should order crossjoin of rows" do
|
83
|
+
lambda do
|
84
|
+
@olap.from('Sales').
|
85
|
+
columns('[Measures].[Unit Sales]').
|
86
|
+
rows('[Customers].[Country].Members').crossjoin('[Gender].[Gender].Members').
|
87
|
+
order('[Measures].[Unit Sales]', :bdesc).
|
88
|
+
execute
|
89
|
+
end.should_not raise_error
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should generate correct member name from large number key" do
|
93
|
+
result = @olap.from('Sales').
|
94
|
+
columns("Filter([Customers.ID].[ID].Members, [Customers.ID].CurrentMember.Properties('Name') = 'Big Number')").
|
95
|
+
execute
|
96
|
+
result.column_names.should == ["10000000000"]
|
97
|
+
end
|
98
|
+
|
99
|
+
# test for https://jira.pentaho.com/browse/MONDRIAN-990
|
100
|
+
it "should return result when diacritical marks used" do
|
101
|
+
full_name = '[Customers].[USA].[CA].[Rīga]'
|
102
|
+
result = @olap.from('Sales').columns(full_name).execute
|
103
|
+
result.column_full_names.should == [full_name]
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should execute MDX with join tables" do
|
107
|
+
# Load dimension members in Mondrian cache as the problem occurred when searching members in the cache
|
108
|
+
@olap.from('Sales').columns('CROSSJOIN({[Linked Promotions].[Promotion].[Promotion 2]}, [Customers].[Name].Members)').execute
|
109
|
+
|
110
|
+
mdx = <<~MDX
|
111
|
+
SELECT
|
112
|
+
NON EMPTY FILTER(
|
113
|
+
CROSSJOIN({[Linked Promotions].[Promotion].[Promotion 2]}, [Customers].[Name].Members),
|
114
|
+
(([Measures].[Unit Sales]) <> 0)
|
115
|
+
) ON ROWS,
|
116
|
+
[Measures].[Unit Sales] ON COLUMNS
|
117
|
+
FROM [Sales]
|
118
|
+
MDX
|
119
|
+
|
120
|
+
expect { @olap.execute mdx }.not_to raise_error
|
121
|
+
end
|
122
|
+
|
53
123
|
end
|
data/spec/query_spec.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
describe "Query" do
|
4
|
-
def
|
5
|
-
ActiveRecord::Base.connection.quote_table_name(name)
|
4
|
+
def qt(name)
|
5
|
+
ActiveRecord::Base.connection.quote_table_name(name.to_s)
|
6
6
|
end
|
7
7
|
|
8
8
|
before(:all) do
|
@@ -21,9 +21,9 @@ describe "Query" do
|
|
21
21
|
FROM sales
|
22
22
|
LEFT JOIN products ON sales.product_id = products.id
|
23
23
|
LEFT JOIN product_classes ON products.product_class_id = product_classes.id
|
24
|
-
LEFT JOIN #{
|
24
|
+
LEFT JOIN #{qt :time} ON sales.time_id = #{qt :time}.id
|
25
25
|
LEFT JOIN customers ON sales.customer_id = customers.id
|
26
|
-
WHERE #{
|
26
|
+
WHERE #{qt :time}.the_year = 2010 AND #{qt :time}.quarter = 'Q1'
|
27
27
|
AND customers.country = 'USA' AND customers.state_province = 'CA'
|
28
28
|
GROUP BY product_classes.product_family
|
29
29
|
ORDER BY product_classes.product_family
|
@@ -179,6 +179,13 @@ describe "Query" do
|
|
179
179
|
end
|
180
180
|
end
|
181
181
|
|
182
|
+
describe "distinct" do
|
183
|
+
it "should limit to set of distinct tuples" do
|
184
|
+
@query.rows('[Product].children').distinct.nonempty.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]')
|
185
|
+
@query.rows.should == [:nonempty, [:distinct, ["[Product].children"]]]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
182
189
|
describe "order" do
|
183
190
|
it "should order by one measure" do
|
184
191
|
@query.rows('[Product].children').order('[Measures].[Unit Sales]', :bdesc)
|
@@ -282,6 +289,18 @@ describe "Query" do
|
|
282
289
|
end
|
283
290
|
end
|
284
291
|
|
292
|
+
describe "generate" do
|
293
|
+
it "should generate new set" do
|
294
|
+
@query.rows('[Customers].[Country].Members').generate('[Customers].CurrentMember')
|
295
|
+
@query.rows.should == [:generate, ['[Customers].[Country].Members'], ['[Customers].CurrentMember']]
|
296
|
+
end
|
297
|
+
|
298
|
+
it "should generate new set with all option" do
|
299
|
+
@query.rows('[Customers].[Country].Members').generate('[Customers].CurrentMember', :all)
|
300
|
+
@query.rows.should == [:generate, ['[Customers].[Country].Members'], ['[Customers].CurrentMember'], 'ALL']
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
285
304
|
describe "where" do
|
286
305
|
it "should accept conditions" do
|
287
306
|
@query.where('[Time].[2010].[Q1]', '[Customers].[USA].[CA]').should equal(@query)
|
@@ -605,6 +624,26 @@ describe "Query" do
|
|
605
624
|
SQL
|
606
625
|
end
|
607
626
|
|
627
|
+
it "should return query with generate" do
|
628
|
+
@query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
|
629
|
+
rows('[Customers].[Country].Members').generate('[Customers].CurrentMember').
|
630
|
+
to_mdx.should be_like <<-SQL
|
631
|
+
SELECT {[Measures].[Unit Sales], [Measures].[Store Sales]} ON COLUMNS,
|
632
|
+
GENERATE([Customers].[Country].Members, [Customers].CurrentMember) ON ROWS
|
633
|
+
FROM [Sales]
|
634
|
+
SQL
|
635
|
+
end
|
636
|
+
|
637
|
+
it "should return query with generate all" do
|
638
|
+
@query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
|
639
|
+
rows('[Customers].[Country].Members').generate('[Customers].CurrentMember', :all).
|
640
|
+
to_mdx.should be_like <<-SQL
|
641
|
+
SELECT {[Measures].[Unit Sales], [Measures].[Store Sales]} ON COLUMNS,
|
642
|
+
GENERATE([Customers].[Country].Members, [Customers].CurrentMember, ALL) ON ROWS
|
643
|
+
FROM [Sales]
|
644
|
+
SQL
|
645
|
+
end
|
646
|
+
|
608
647
|
it "should return query including WITH MEMBER clause" do
|
609
648
|
@query.
|
610
649
|
with_member('[Measures].[ProfitPct]').
|
@@ -632,20 +671,25 @@ describe "Query" do
|
|
632
671
|
end
|
633
672
|
|
634
673
|
it "should return query including WITH SET clause" do
|
635
|
-
@query.with_set('
|
674
|
+
@query.with_set('CrossJoinSet').
|
636
675
|
as('[Product].children').crossjoin('[Customers].[Canada]', '[Customers].[USA]').
|
676
|
+
with_set('MemberSet').as('[Product].[All Products]').
|
677
|
+
with_set('FunctionSet').as('[Product].AllMembers').
|
678
|
+
with_set('ItemSet').as('[Product].AllMembers.Item(0)').
|
637
679
|
with_member('[Measures].[Profit]').
|
638
680
|
as('[Measures].[Store Sales] - [Measures].[Store Cost]').
|
639
681
|
columns('[Measures].[Profit]').
|
640
|
-
rows('
|
682
|
+
rows('CrossJoinSet').
|
641
683
|
to_mdx.should be_like <<-SQL
|
642
684
|
WITH
|
643
|
-
SET
|
644
|
-
'
|
685
|
+
SET CrossJoinSet AS 'CROSSJOIN([Product].children, {[Customers].[Canada], [Customers].[USA]})'
|
686
|
+
SET MemberSet AS '{[Product].[All Products]}'
|
687
|
+
SET FunctionSet AS '[Product].AllMembers'
|
688
|
+
SET ItemSet AS '{[Product].AllMembers.Item(0)}'
|
645
689
|
MEMBER [Measures].[Profit] AS
|
646
690
|
'[Measures].[Store Sales] - [Measures].[Store Cost]'
|
647
691
|
SELECT {[Measures].[Profit]} ON COLUMNS,
|
648
|
-
|
692
|
+
CrossJoinSet ON ROWS
|
649
693
|
FROM [Sales]
|
650
694
|
SQL
|
651
695
|
end
|
@@ -717,6 +761,17 @@ describe "Query" do
|
|
717
761
|
}
|
718
762
|
end
|
719
763
|
|
764
|
+
it "should raise error when TokenMgrError is raised" do
|
765
|
+
expect {
|
766
|
+
@query.with_member('[Measures].[Dummy]').as('[Measures].[Store Sales]]').
|
767
|
+
columns('[Measures].[Dummy]').execute
|
768
|
+
}.to raise_error {|e|
|
769
|
+
e.should be_kind_of(Mondrian::OLAP::Error)
|
770
|
+
e.message.should =~ /mondrian\.parser\.TokenMgrError/
|
771
|
+
e.root_cause_message.should =~ /Lexical error/
|
772
|
+
}
|
773
|
+
end
|
774
|
+
|
720
775
|
end
|
721
776
|
|
722
777
|
describe "drill through cell" do
|
@@ -733,7 +788,7 @@ describe "Query" do
|
|
733
788
|
@drill_through.column_types.should == [
|
734
789
|
:INT, :VARCHAR, :INT, :INT, :INT,
|
735
790
|
:VARCHAR, :VARCHAR, :VARCHAR, :VARCHAR, :VARCHAR, :VARCHAR,
|
736
|
-
:VARCHAR, :VARCHAR, :VARCHAR, :
|
791
|
+
:VARCHAR, :VARCHAR, :VARCHAR, :BIGINT,
|
737
792
|
:VARCHAR,
|
738
793
|
:DECIMAL
|
739
794
|
]
|
@@ -752,12 +807,14 @@ describe "Query" do
|
|
752
807
|
end if %w(mysql postgresql).include? MONDRIAN_DRIVER
|
753
808
|
|
754
809
|
it "should return table names" do
|
755
|
-
|
810
|
+
# ignore calculated customer full name column name which is shown differently on each database
|
811
|
+
@drill_through.table_names[0..12].should == [
|
756
812
|
"time", "time", "time", "time", "time",
|
757
813
|
"product_classes", "product_classes", "product_classes", "product_classes", "products", "products",
|
758
|
-
"customers", "customers"
|
759
|
-
|
760
|
-
|
814
|
+
"customers", "customers"
|
815
|
+
]
|
816
|
+
@drill_through.table_names[14..16].should == [
|
817
|
+
"customers", "customers", "sales"
|
761
818
|
]
|
762
819
|
end if %w(mysql postgresql).include? MONDRIAN_DRIVER
|
763
820
|
|
@@ -776,8 +833,7 @@ describe "Query" do
|
|
776
833
|
end
|
777
834
|
|
778
835
|
it "should return correct row value types" do
|
779
|
-
|
780
|
-
case MONDRIAN_DRIVER
|
836
|
+
expected_value_types = case MONDRIAN_DRIVER
|
781
837
|
when "oracle"
|
782
838
|
[
|
783
839
|
BigDecimal, String, BigDecimal, BigDecimal, BigDecimal,
|
@@ -788,21 +844,26 @@ describe "Query" do
|
|
788
844
|
]
|
789
845
|
when 'mssql'
|
790
846
|
[
|
791
|
-
|
847
|
+
Integer, String, Integer, Integer, Integer,
|
792
848
|
String, String, String, String, String, String,
|
793
|
-
|
849
|
+
# last one can be BigDecimal or Integer, probably depends on MS SQL version
|
850
|
+
String, String, String, Numeric,
|
794
851
|
String,
|
795
852
|
BigDecimal
|
796
853
|
]
|
797
854
|
else
|
798
855
|
[
|
799
|
-
|
856
|
+
Integer, String, Integer, Integer, Integer,
|
800
857
|
String, String, String, String, String, String,
|
801
|
-
String, String, String,
|
858
|
+
String, String, String, Integer,
|
802
859
|
String,
|
803
860
|
BigDecimal
|
804
861
|
]
|
805
862
|
end
|
863
|
+
|
864
|
+
@drill_through.rows.first.each_with_index do |value, i|
|
865
|
+
value.should be_a expected_value_types[i]
|
866
|
+
end
|
806
867
|
end
|
807
868
|
|
808
869
|
it "should return only specified max rows" do
|
@@ -816,8 +877,7 @@ describe "Query" do
|
|
816
877
|
@query = @olap.from('Sales')
|
817
878
|
@result = @query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
|
818
879
|
rows('[Product].children').
|
819
|
-
|
820
|
-
where('[Time].[2010].[Q1]').
|
880
|
+
where('[Time].[2010].[Q1]', '[Time].[2010].[Q2]').
|
821
881
|
execute
|
822
882
|
end
|
823
883
|
|
@@ -829,9 +889,9 @@ describe "Query" do
|
|
829
889
|
'[Measures].[Unit Sales]', '[Measures].[Store Sales]'
|
830
890
|
])
|
831
891
|
@drill_through.column_labels.should == [
|
832
|
-
"Month",
|
833
|
-
"City",
|
834
|
-
"Product Family",
|
892
|
+
"Month (Key)",
|
893
|
+
"City (Key)",
|
894
|
+
"Product Family (Key)",
|
835
895
|
"Unit Sales", "Store Sales"
|
836
896
|
]
|
837
897
|
end
|
@@ -847,6 +907,78 @@ describe "Query" do
|
|
847
907
|
@drill_through.rows.all?{|r| r.any?{|c| c}}.should be_true
|
848
908
|
end
|
849
909
|
|
910
|
+
it "should return member name and property values" do
|
911
|
+
@drill_through = @result.drill_through(row: 0, column: 0,
|
912
|
+
return: [
|
913
|
+
"Name([Customers].[Name])",
|
914
|
+
"Property([Customers].[Name], 'Gender')",
|
915
|
+
"Property([Customers].[Name], 'Description')",
|
916
|
+
"Property([Customers].[Name], 'Very long non-existing property name')"
|
917
|
+
]
|
918
|
+
)
|
919
|
+
@drill_through.column_labels.should == [
|
920
|
+
"Name", "Gender", "Description",
|
921
|
+
"Very long non-existing property name"[0, MONDRIAN_DRIVER == 'oracle' ? 30 : 9999]
|
922
|
+
]
|
923
|
+
@drill_through.rows.should == @sql.select_rows(<<-SQL)
|
924
|
+
SELECT
|
925
|
+
customers.fullname,
|
926
|
+
customers.gender,
|
927
|
+
customers.description,
|
928
|
+
'' as non_existing
|
929
|
+
FROM
|
930
|
+
sales,
|
931
|
+
customers,
|
932
|
+
time,
|
933
|
+
products,
|
934
|
+
product_classes
|
935
|
+
WHERE
|
936
|
+
(time.quarter = 'Q1' OR time.quarter = 'Q2') AND
|
937
|
+
time.the_year = 2010 AND
|
938
|
+
product_classes.product_family = 'Drink' AND
|
939
|
+
products.product_class_id = product_classes.id AND
|
940
|
+
sales.product_id = products.id AND
|
941
|
+
sales.time_id = time.id AND
|
942
|
+
customers.id = sales.customer_id
|
943
|
+
ORDER BY
|
944
|
+
customers.fullname,
|
945
|
+
customers.gender,
|
946
|
+
customers.description
|
947
|
+
SQL
|
948
|
+
end
|
949
|
+
|
950
|
+
it "should group by" do
|
951
|
+
@drill_through = @result.drill_through(row: 0, column: 0,
|
952
|
+
return: [
|
953
|
+
"[Product].[Product Family]",
|
954
|
+
"[Measures].[Unit Sales]",
|
955
|
+
"[Measures].[Store Cost]"
|
956
|
+
],
|
957
|
+
group_by: true
|
958
|
+
)
|
959
|
+
@drill_through.column_labels.should == [ "Product Family (Key)", "Unit Sales", "Store Cost" ]
|
960
|
+
@drill_through.rows.should == @sql.select_rows(<<-SQL
|
961
|
+
SELECT
|
962
|
+
product_classes.product_family,
|
963
|
+
SUM(sales.unit_sales) AS unit_sales,
|
964
|
+
SUM(sales.store_cost) AS store_cost
|
965
|
+
FROM
|
966
|
+
sales,
|
967
|
+
time,
|
968
|
+
products,
|
969
|
+
product_classes
|
970
|
+
WHERE
|
971
|
+
(time.quarter = 'Q1' OR time.quarter = 'Q2') AND
|
972
|
+
time.the_year = 2010 AND
|
973
|
+
product_classes.product_family = 'Drink' AND
|
974
|
+
products.product_class_id = product_classes.id AND
|
975
|
+
sales.product_id = products.id AND
|
976
|
+
sales.time_id = time.id
|
977
|
+
GROUP BY
|
978
|
+
product_classes.product_family
|
979
|
+
SQL
|
980
|
+
)
|
981
|
+
end
|
850
982
|
end
|
851
983
|
|
852
984
|
describe "drill through statement" do
|
@@ -895,4 +1027,170 @@ describe "Query" do
|
|
895
1027
|
|
896
1028
|
end
|
897
1029
|
|
1030
|
+
describe "schema cache" do
|
1031
|
+
before(:all) do
|
1032
|
+
product_id = @sql.select_value("SELECT MIN(id) FROM products")
|
1033
|
+
time_id = @sql.select_value("SELECT MIN(id) FROM #{qt :time}")
|
1034
|
+
customer_id = @sql.select_value("SELECT MIN(id) FROM customers")
|
1035
|
+
@condition = "product_id = #{product_id} AND time_id = #{time_id} AND customer_id = #{customer_id}"
|
1036
|
+
# check expected initial value
|
1037
|
+
@first_unit_sales = 1
|
1038
|
+
@sql.select_value("SELECT unit_sales FROM sales WHERE #{@condition}").to_i.should == @first_unit_sales
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
before do
|
1042
|
+
create_olap_connection
|
1043
|
+
@unit_sales = query_unit_sales_value
|
1044
|
+
|
1045
|
+
update_first_unit_sales(@first_unit_sales + 1)
|
1046
|
+
|
1047
|
+
# should still use previous value from cache
|
1048
|
+
create_olap_connection
|
1049
|
+
query_unit_sales_value.should == @unit_sales
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
after do
|
1053
|
+
update_first_unit_sales(@first_unit_sales)
|
1054
|
+
Mondrian::OLAP::Connection.flush_schema_cache
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
def create_olap_connection(options = {})
|
1058
|
+
@olap2.close if @olap2
|
1059
|
+
@olap2 = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS_WITH_CATALOG.merge(options))
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
def update_first_unit_sales(value)
|
1063
|
+
@sql.update "UPDATE sales SET unit_sales = #{value} WHERE #{@condition}"
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
def query_unit_sales_value
|
1067
|
+
@olap2.from('Sales').columns('[Measures].[Unit Sales]').execute.values.first
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
it "should flush schema cache" do
|
1071
|
+
@olap2.flush_schema
|
1072
|
+
create_olap_connection
|
1073
|
+
query_unit_sales_value.should == @unit_sales + 1
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
it "should remove schema by key" do
|
1077
|
+
Mondrian::OLAP::Connection.flush_schema(@olap2.schema_key)
|
1078
|
+
create_olap_connection
|
1079
|
+
query_unit_sales_value.should == @unit_sales + 1
|
1080
|
+
end
|
1081
|
+
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
describe "profiling" do
|
1085
|
+
before(:all) do
|
1086
|
+
if @olap
|
1087
|
+
@olap.flush_schema
|
1088
|
+
@olap.close
|
1089
|
+
end
|
1090
|
+
@olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS_WITH_CATALOG)
|
1091
|
+
@result = @olap.execute "SELECT [Measures].[Unit Sales] ON COLUMNS, [Product].Children ON ROWS FROM [Sales]", profiling: true
|
1092
|
+
@result.profiling_mark_full("MDX query time", 100)
|
1093
|
+
end
|
1094
|
+
|
1095
|
+
it "should return query plan" do
|
1096
|
+
@result.profiling_plan.strip.should == <<-EOS.strip
|
1097
|
+
Axis (COLUMNS):
|
1098
|
+
SetListCalc(name=SetListCalc, class=class mondrian.olap.fun.SetFunDef$SetListCalc, type=SetType<MemberType<member=[Measures].[Unit Sales]>>, resultStyle=MUTABLE_LIST)
|
1099
|
+
2(name=2, class=class mondrian.olap.fun.SetFunDef$SetListCalc$2, type=MemberType<member=[Measures].[Unit Sales]>, resultStyle=VALUE)
|
1100
|
+
Literal(name=Literal, class=class mondrian.calc.impl.ConstantCalc, type=MemberType<member=[Measures].[Unit Sales]>, resultStyle=VALUE_NOT_NULL, value=[Measures].[Unit Sales])
|
1101
|
+
|
1102
|
+
Axis (ROWS):
|
1103
|
+
Children(name=Children, class=class mondrian.olap.fun.BuiltinFunTable$22$1, type=SetType<MemberType<hierarchy=[Product]>>, resultStyle=LIST)
|
1104
|
+
CurrentMemberFixed(hierarchy=[Product], name=CurrentMemberFixed, class=class mondrian.olap.fun.HierarchyCurrentMemberFunDef$FixedCalcImpl, type=MemberType<hierarchy=[Product]>, resultStyle=VALUE)
|
1105
|
+
EOS
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
it "should return SQL timing string" do
|
1109
|
+
@result.profiling_timing_string.strip.should =~
|
1110
|
+
%r{^SqlStatement-Segment.load invoked 1 times for total of \d+ms. \(Avg. \d+ms/invocation\)$}
|
1111
|
+
end
|
1112
|
+
|
1113
|
+
it "should return custom profiling string" do
|
1114
|
+
@result.profiling_timing_string.strip.should =~
|
1115
|
+
%r{^MDX query time invoked 1 times for total of 100ms. \(Avg. 100ms/invocation\)$}
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
it "should return total duration" do
|
1119
|
+
@result.total_duration.should > 0
|
1120
|
+
end
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
describe "error with profiling" do
|
1124
|
+
before(:all) do
|
1125
|
+
@olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS_WITH_CATALOG)
|
1126
|
+
begin
|
1127
|
+
@olap.execute <<-MDX, profiling: true
|
1128
|
+
SELECT [Measures].[Unit Sales] ON COLUMNS,
|
1129
|
+
FILTER([Customers].Children, ([Customers].DefaultMember, [Measures].[Unit Sales]) > 'dummy') ON ROWS
|
1130
|
+
FROM [Sales]
|
1131
|
+
MDX
|
1132
|
+
rescue => e
|
1133
|
+
@error = e
|
1134
|
+
end
|
1135
|
+
end
|
1136
|
+
|
1137
|
+
it "should return query plan" do
|
1138
|
+
@error.profiling_plan.should =~ /^Axis \(COLUMNS\):/
|
1139
|
+
end
|
1140
|
+
|
1141
|
+
it "should return timing string" do
|
1142
|
+
@error.profiling_timing_string.should =~
|
1143
|
+
%r{^FilterFunDef invoked 1 times for total of \d+ms. \(Avg. \d+ms/invocation\)$}
|
1144
|
+
end
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
describe "timeout" do
|
1148
|
+
before(:all) do
|
1149
|
+
@schema = Mondrian::OLAP::Schema.new
|
1150
|
+
@schema.define do
|
1151
|
+
cube 'Sales' do
|
1152
|
+
table 'sales'
|
1153
|
+
dimension 'Customers', foreign_key: 'customer_id' do
|
1154
|
+
hierarchy all_member_name: 'All Customers', primary_key: 'id' do
|
1155
|
+
table 'customers'
|
1156
|
+
level 'Name', column: 'fullname'
|
1157
|
+
end
|
1158
|
+
end
|
1159
|
+
calculated_member 'Sleep 5' do
|
1160
|
+
dimension 'Measures'
|
1161
|
+
formula 'Sleep(5)'
|
1162
|
+
end
|
1163
|
+
calculated_member 'Sleep 0' do
|
1164
|
+
dimension 'Measures'
|
1165
|
+
formula 'Sleep(0)'
|
1166
|
+
end
|
1167
|
+
end
|
1168
|
+
user_defined_function 'Sleep' do
|
1169
|
+
ruby do
|
1170
|
+
parameters :numeric
|
1171
|
+
returns :numeric
|
1172
|
+
def call(n)
|
1173
|
+
sleep n
|
1174
|
+
n
|
1175
|
+
end
|
1176
|
+
end
|
1177
|
+
end
|
1178
|
+
end
|
1179
|
+
@olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge schema: @schema)
|
1180
|
+
end
|
1181
|
+
|
1182
|
+
it "should raise timeout error for long queries" do
|
1183
|
+
expect do
|
1184
|
+
@olap.from('Sales').columns('[Measures].[Sleep 5]').execute(timeout: 0.1)
|
1185
|
+
end.to raise_error do |e|
|
1186
|
+
e.should be_kind_of(Mondrian::OLAP::Error)
|
1187
|
+
e.message.should == 'org.olap4j.OlapException: Mondrian Error:Query timeout of 0 seconds reached'
|
1188
|
+
end
|
1189
|
+
end
|
1190
|
+
|
1191
|
+
it "should not raise timeout error for short queries" do
|
1192
|
+
@olap.from('Sales').columns('[Measures].[Sleep 0]').execute(timeout: 1).values.should == [0]
|
1193
|
+
end
|
1194
|
+
end
|
1195
|
+
|
898
1196
|
end
|