mondrian-olap 0.5.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|