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.
Files changed (54) hide show
  1. checksums.yaml +5 -5
  2. data/Changelog.md +86 -0
  3. data/LICENSE-Mondrian.txt +87 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +43 -40
  6. data/VERSION +1 -1
  7. data/lib/mondrian/jars/commons-collections-3.2.2.jar +0 -0
  8. data/lib/mondrian/jars/commons-dbcp-1.4.jar +0 -0
  9. data/lib/mondrian/jars/commons-io-2.2.jar +0 -0
  10. data/lib/mondrian/jars/commons-lang-2.6.jar +0 -0
  11. data/lib/mondrian/jars/commons-logging-1.2.jar +0 -0
  12. data/lib/mondrian/jars/commons-pool-1.5.7.jar +0 -0
  13. data/lib/mondrian/jars/commons-vfs2-2.2.jar +0 -0
  14. data/lib/mondrian/jars/eigenbase-xom-1.3.5.jar +0 -0
  15. data/lib/mondrian/jars/guava-17.0.jar +0 -0
  16. data/lib/mondrian/jars/log4j-1.2.17.jar +0 -0
  17. data/lib/mondrian/jars/log4j.properties +2 -4
  18. data/lib/mondrian/jars/mondrian-9.1.0.0.jar +0 -0
  19. data/lib/mondrian/jars/olap4j-1.2.0.jar +0 -0
  20. data/lib/mondrian/olap/connection.rb +252 -67
  21. data/lib/mondrian/olap/cube.rb +63 -2
  22. data/lib/mondrian/olap/error.rb +37 -8
  23. data/lib/mondrian/olap/query.rb +41 -21
  24. data/lib/mondrian/olap/result.rb +163 -44
  25. data/lib/mondrian/olap/schema.rb +42 -3
  26. data/lib/mondrian/olap/schema_element.rb +25 -6
  27. data/lib/mondrian/olap/schema_udf.rb +21 -16
  28. data/spec/connection_role_spec.rb +69 -13
  29. data/spec/connection_spec.rb +3 -2
  30. data/spec/cube_cache_control_spec.rb +261 -0
  31. data/spec/cube_spec.rb +32 -4
  32. data/spec/fixtures/MondrianTest.xml +1 -6
  33. data/spec/fixtures/MondrianTestOracle.xml +1 -6
  34. data/spec/mondrian_spec.rb +71 -1
  35. data/spec/query_spec.rb +323 -25
  36. data/spec/rake_tasks.rb +253 -159
  37. data/spec/schema_definition_spec.rb +314 -61
  38. data/spec/spec_helper.rb +115 -45
  39. data/spec/support/data/customers.csv +10902 -0
  40. data/spec/support/data/product_classes.csv +101 -0
  41. data/spec/support/data/products.csv +101 -0
  42. data/spec/support/data/sales.csv +101 -0
  43. data/spec/support/data/time.csv +731 -0
  44. metadata +126 -124
  45. data/LICENSE-Mondrian.html +0 -259
  46. data/lib/mondrian/jars/commons-collections-3.2.jar +0 -0
  47. data/lib/mondrian/jars/commons-dbcp-1.2.1.jar +0 -0
  48. data/lib/mondrian/jars/commons-logging-1.1.1.jar +0 -0
  49. data/lib/mondrian/jars/commons-pool-1.2.jar +0 -0
  50. data/lib/mondrian/jars/commons-vfs-1.0.jar +0 -0
  51. data/lib/mondrian/jars/eigenbase-xom-1.3.1.jar +0 -0
  52. data/lib/mondrian/jars/log4j-1.2.14.jar +0 -0
  53. data/lib/mondrian/jars/mondrian.jar +0 -0
  54. data/lib/mondrian/jars/olap4j-1.0.1.539.jar +0 -0
@@ -50,10 +50,12 @@ module Mondrian
50
50
  public
51
51
 
52
52
  attributes :name, :description, :measures_caption
53
- elements :annotations, :dimension, :cube, :virtual_cube, :role, :user_defined_function
53
+ elements :annotations, :parameter, :dimension, :cube, :virtual_cube, :role, :user_defined_function
54
54
 
55
55
  class Cube < SchemaElement
56
56
  attributes :name, :description, :caption,
57
+ # Whether this cube is visible in the user-interface. Default true.
58
+ :visible,
57
59
  # The name of the measure that would be taken as the default measure of the cube.
58
60
  :default_measure,
59
61
  # Should the Fact table data for this Cube be cached by Mondrian or not.
@@ -84,6 +86,8 @@ module Mondrian
84
86
 
85
87
  class Dimension < SchemaElement
86
88
  attributes :name, :description, :caption,
89
+ # Whether this dimension is visible in the user-interface. Default true.
90
+ :visible,
87
91
  # The dimension's type may be one of "Standard" or "Time".
88
92
  # A time dimension will allow the use of the MDX time functions (WTD, YTD, QTD, etc.).
89
93
  # Use a standard dimension if the dimension is not a time-related dimension.
@@ -111,6 +115,7 @@ module Mondrian
111
115
  # Required in a private Dimension or a DimensionUsage, but not in a public Dimension.
112
116
  :foreign_key
113
117
  data_dictionary_names :usage_prefix, :foreign_key # values in XML will be uppercased when using Oracle driver
118
+ elements :annotations
114
119
 
115
120
  def initialize(name = nil, attributes = {}, parent = nil)
116
121
  super
@@ -121,6 +126,8 @@ module Mondrian
121
126
 
122
127
  class Hierarchy < SchemaElement
123
128
  attributes :name, :description, :caption,
129
+ # Whether this hierarchy is visible in the user-interface. Default true.
130
+ :visible,
124
131
  # Whether this hierarchy has an 'all' member.
125
132
  :has_all,
126
133
  # Name of the 'all' member. If this attribute is not specified,
@@ -137,6 +144,8 @@ module Mondrian
137
144
  # The name of the table which contains primary_key.
138
145
  # If the hierarchy has only one table, defaults to that; it is required.
139
146
  :primary_key_table,
147
+ #
148
+ :default_member,
140
149
  # Should be set to the level (if such a level exists) at which depth it is known
141
150
  # that all members have entirely unique rows, allowing SQL GROUP BY clauses to be completely eliminated from the query.
142
151
  :unique_key_level_name
@@ -160,6 +169,8 @@ module Mondrian
160
169
 
161
170
  class Level < SchemaElement
162
171
  attributes :name, :description, :caption,
172
+ # Whether this level is visible in the user-interface. Default true.
173
+ :visible,
163
174
  # The name of the table that the column comes from.
164
175
  # If this hierarchy is based upon just one table, defaults to the name of that table;
165
176
  # otherwise, it is required.
@@ -187,6 +198,12 @@ module Mondrian
187
198
  # for example, "DATE '2006-06-01'".
188
199
  # Default value: 'String'
189
200
  :type,
201
+ # Indicates the Java type that Mondrian uses to store this level's key column.
202
+ # It also determines the JDBC method that Mondrian will call to retrieve the column;
203
+ # for example, if the Java type is 'int', Mondrian will call 'ResultSet.getInt(int)'.
204
+ # Usually this attribute is not needed, because Mondrian can choose a sensible type based on the type of the database column.
205
+ # Allowable values are: 'int', 'long', 'Object', 'String'.
206
+ :internal_type,
190
207
  # Whether members are unique across all parents.
191
208
  # For example, zipcodes are unique across all states.
192
209
  # The first level's members are always unique.
@@ -288,8 +305,12 @@ module Mondrian
288
305
 
289
306
  class CalculatedMember < SchemaElement
290
307
  attributes :name, :description, :caption,
291
- # Name of the dimension which this member belongs to.
308
+ # Name of the dimension which this member belongs to. Cannot be used if :hieararchy is specified.
292
309
  :dimension,
310
+ # Full unique name of the hierarchy that this member belongs to.
311
+ :hierarchy,
312
+ # Fully-qualified name of the parent member. If not specified, the member will be at the lowest level (besides the 'all' level) in the hierarchy.
313
+ :parent,
293
314
  # Format string with which to format cells of this measure. For more details, see the mondrian.util.Format class.
294
315
  :format_string,
295
316
  # Whether this member is visible in the user-interface. Default true.
@@ -312,6 +333,8 @@ module Mondrian
312
333
 
313
334
  class VirtualCube < SchemaElement
314
335
  attributes :name, :description, :caption,
336
+ # Whether this cube is visible in the user-interface. Default true.
337
+ :visible,
315
338
  # The name of the measure that would be taken as the default measure of the cube.
316
339
  :default_measure,
317
340
  # Whether element is enabled - if true, then the VirtualCube is realized otherwise it is ignored.
@@ -322,7 +345,10 @@ module Mondrian
322
345
  class VirtualCubeDimension < SchemaElement
323
346
  attributes :name,
324
347
  # Name of the cube which the dimension belongs to, or unspecified if the dimension is shared
325
- :cube_name
348
+ :cube_name,
349
+ # Whether this dimension is visible in the user-interface. Default true.
350
+ :visible
351
+ elements :annotations
326
352
  end
327
353
 
328
354
  class VirtualCubeMeasure < SchemaElement
@@ -471,8 +497,21 @@ module Mondrian
471
497
  end
472
498
 
473
499
  class Annotation < SchemaElement
500
+ attributes :name
474
501
  content :text
475
502
  end
503
+
504
+ class Parameter < SchemaElement
505
+ attributes :name, :description,
506
+ # Indicates the type of this parameter: String, Numeric, Integer, Boolean, Date, Time, Timestamp, or Member.
507
+ :type,
508
+ # If false, statement cannot change the value of this parameter; the parameter becomes effectively constant
509
+ # (provided that its default value expression always returns the same value). Default is true.
510
+ :modifiable,
511
+ # Expression for the default value of this parameter.
512
+ :default_value
513
+ end
514
+
476
515
  end
477
516
  end
478
517
  end
@@ -67,14 +67,30 @@ module Mondrian
67
67
  attr_reader pluralize(name).to_sym
68
68
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
69
69
  def #{name}(name=nil, attributes = {}, &block)
70
- @#{pluralize(name)} << Schema::#{camel_case(name)}.new(name, attributes, self, &block)
70
+ new_element = Schema::#{camel_case(name)}.new(name, attributes, self, &block)
71
+ @#{pluralize(name)} << new_element
72
+ new_element
71
73
  end
72
74
  RUBY
75
+ if name == :annotations
76
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
77
+ def annotations_hash
78
+ hash = {}
79
+ @annotationss.each do |annotations|
80
+ annotations.annotations.each do |annotation|
81
+ hash[annotation.name] = annotation.content
82
+ end
83
+ end
84
+ hash
85
+ end
86
+ RUBY
87
+ end
73
88
  end
74
89
  end
75
90
 
76
- def self.content(type=nil)
91
+ def self.content(type = nil)
77
92
  return @content if type.nil?
93
+ attr_reader :content
78
94
  @content = type
79
95
  end
80
96
 
@@ -86,7 +102,7 @@ module Mondrian
86
102
  @xml_fragments << string
87
103
  end
88
104
 
89
- def to_xml(options={})
105
+ def to_xml(options = {})
90
106
  options[:upcase_data_dictionary] = @upcase_data_dictionary unless @upcase_data_dictionary.nil?
91
107
  Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
92
108
  add_to_xml(xml, options)
@@ -124,8 +140,8 @@ module Mondrian
124
140
 
125
141
  def xmlized_attributes(options)
126
142
  # data dictionary values should be in uppercase if schema defined with :upcase_data_dictionary => true
127
- # or by default when using Oracle or LucidDB driver (can be overridden by :upcase_data_dictionary => false)
128
- upcase_attributes = if options[:upcase_data_dictionary].nil? && %w(oracle luciddb).include?(options[:driver]) ||
143
+ # or by default when using Oracle or Snowflake driver (can be overridden by :upcase_data_dictionary => false)
144
+ upcase_attributes = if options[:upcase_data_dictionary].nil? && %w(oracle snowflake).include?(options[:driver]) ||
129
145
  options[:upcase_data_dictionary]
130
146
  self.class.data_dictionary_names
131
147
  else
@@ -133,7 +149,10 @@ module Mondrian
133
149
  end
134
150
  hash = {}
135
151
  @attributes.each do |attr, value|
136
- value = value.upcase if upcase_attributes.include?(attr)
152
+ # Support value calculation in parallel threads.
153
+ # value could be either Thread or a future object from concurrent-ruby
154
+ value = value.value if value.respond_to?(:value)
155
+ value = value.upcase if upcase_attributes.include?(attr) && value.is_a?(String)
137
156
  hash[
138
157
  # camelcase attribute name
139
158
  attr.to_s.gsub(/_([^_]+)/){|m| $1.capitalize}
@@ -17,7 +17,7 @@ module Mondrian
17
17
 
18
18
  def coffeescript_function(arguments_string, text)
19
19
  # construct function to ensure that last expression is returned
20
- coffee_text = "#{arguments_string} ->\n" << text.gsub(/^/,' ')
20
+ coffee_text = "#{arguments_string} ->\n" << text.gsub(/^/, ' ')
21
21
  javascript_text = CoffeeScript.compile(coffee_text, :bare => true)
22
22
  # remove function definition first and last lines
23
23
  javascript_text = javascript_text.strip.lines.to_a[1..-2].join
@@ -67,7 +67,7 @@ module Mondrian
67
67
  end
68
68
 
69
69
  def ruby_formatter_java_class_name(name)
70
- "rubyobj.#{self.class.name.gsub('::','.')}.#{ruby_formatter_name_to_class_name(name)}"
70
+ "rubyobj.#{self.class.name.gsub('::', '.')}.#{ruby_formatter_name_to_class_name(name)}"
71
71
  end
72
72
 
73
73
  end
@@ -180,19 +180,21 @@ JS
180
180
  add_method_signature("getSyntax", [Java::mondrian.olap.Syntax])
181
181
 
182
182
  UDF_SCALAR_TYPES = {
183
- "Numeric" => Java::mondrian.olap.type.NumericType,
184
- "String" => Java::mondrian.olap.type.StringType,
185
- "Boolean" => Java::mondrian.olap.type.BooleanType,
186
- "DateTime" => Java::mondrian.olap.type.DateTimeType,
187
- "Decimal" => Java::mondrian.olap.type.DecimalType,
188
- "Scalar" => Java::mondrian.olap.type.ScalarType
183
+ 'Numeric' => Java::mondrian.olap.type.NumericType,
184
+ 'String' => Java::mondrian.olap.type.StringType,
185
+ 'Boolean' => Java::mondrian.olap.type.BooleanType,
186
+ 'DateTime' => Java::mondrian.olap.type.DateTimeType,
187
+ 'Decimal' => Java::mondrian.olap.type.DecimalType,
188
+ 'Scalar' => Java::mondrian.olap.type.ScalarType
189
189
  }
190
190
  UDF_OTHER_TYPES = {
191
- "Member" => Java::mondrian.olap.type.MemberType::Unknown,
192
- "Set" => Java::mondrian.olap.type.SetType.new(Java::mondrian.olap.type.MemberType::Unknown),
193
- "Hierarchy" => Java::mondrian.olap.type.HierarchyType.new(nil, nil),
194
- "Level" => Java::mondrian.olap.type.LevelType::Unknown
191
+ 'Member' => Java::mondrian.olap.type.MemberType::Unknown,
192
+ 'Tuple' => Java::mondrian.olap.type.TupleType.new([].to_java(Java::mondrian.olap.type.Type)),
193
+ 'Hierarchy' => Java::mondrian.olap.type.HierarchyType.new(nil, nil),
194
+ 'Level' => Java::mondrian.olap.type.LevelType::Unknown
195
195
  }
196
+ UDF_OTHER_TYPES['Set'] = UDF_OTHER_TYPES['MemberSet'] = Java::mondrian.olap.type.SetType.new(UDF_OTHER_TYPES['Member'])
197
+ UDF_OTHER_TYPES['TupleSet'] = Java::mondrian.olap.type.SetType.new(UDF_OTHER_TYPES['Tuple'])
196
198
 
197
199
  def getParameterTypes
198
200
  @parameterTypes ||= self.class.parameters.map{|p| get_java_type(p)}
@@ -214,7 +216,7 @@ JS
214
216
 
215
217
  def execute(evaluator, arguments)
216
218
  values = []
217
- self.class.parameters.each_with_index do |p,i|
219
+ self.class.parameters.each_with_index do |p, i|
218
220
  value = UDF_SCALAR_TYPES[p] ? arguments[i].evaluateScalar(evaluator) : arguments[i].evaluate(evaluator)
219
221
  values << value
220
222
  end
@@ -239,9 +241,12 @@ JS
239
241
  end
240
242
 
241
243
  def self.stringified_type(type)
242
- type = stringify(type)
243
- raise ArgumentError, "invalid user defined function type #{type.inspect}" unless UDF_SCALAR_TYPES[type] || UDF_OTHER_TYPES[type]
244
- type
244
+ type_as_string = stringify(type)
245
+ if UDF_SCALAR_TYPES[type_as_string] || UDF_OTHER_TYPES[type_as_string]
246
+ type_as_string
247
+ else
248
+ raise ArgumentError, "Invalid user defined function type #{type.inspect}"
249
+ end
245
250
  end
246
251
 
247
252
  def self.stringify(arg)
@@ -3,16 +3,22 @@ require "spec_helper"
3
3
  describe "Connection role" do
4
4
 
5
5
  describe "create connection" do
6
- before(:each) do
7
- @role_name = role_name = 'California manager'
8
- @role_name2 = role_name2 = 'Dummy, with comma'
6
+ before(:all) do
7
+ @all_roles = [
8
+ @role_name = role_name = 'California manager',
9
+ @role_name2 = role_name2 = 'Dummy, with comma',
10
+ @simple_role_name = simple_role_name = 'USA manager',
11
+ @union_role_name = union_role_name = 'Union California manager',
12
+ @intermediate_union_role_name = intermediate_union_role_name = "Intermediate #{union_role_name}"
13
+ ]
14
+
9
15
  @schema = Mondrian::OLAP::Schema.define do
10
16
  cube 'Sales' do
11
17
  table 'sales'
12
18
  dimension 'Gender', :foreign_key => 'customer_id' do
13
19
  hierarchy :has_all => true, :primary_key => 'id' do
14
20
  table 'customers'
15
- level 'Gender', :column => 'gender', :unique_members => true
21
+ level 'Gender', :column => 'gender', :unique_members => true, :hide_member_if => 'IfBlankName'
16
22
  end
17
23
  end
18
24
  dimension 'Customers', :foreign_key => 'customer_id' do
@@ -49,6 +55,26 @@ describe "Connection role" do
49
55
  end
50
56
  role role_name2
51
57
 
58
+ role simple_role_name do
59
+ schema_grant :access => 'none' do
60
+ cube_grant :cube => 'Sales', :access => 'all' do
61
+ hierarchy_grant :hierarchy => '[Customers]', :access => 'custom', :bottom_level => '[Customers].[State Province]' do
62
+ member_grant :member => '[Customers].[USA]', :access => 'all'
63
+ end
64
+ end
65
+ end
66
+ end
67
+ role intermediate_union_role_name do
68
+ union do
69
+ role_usage role_name: simple_role_name
70
+ end
71
+ end
72
+ role union_role_name do
73
+ union do
74
+ role_usage role_name: intermediate_union_role_name
75
+ end
76
+ end
77
+
52
78
  # to test that Role elements are generated before UserDefinedFunction
53
79
  user_defined_function 'Factorial' do
54
80
  ruby do
@@ -60,15 +86,22 @@ describe "Connection role" do
60
86
  end
61
87
  end
62
88
  end
89
+ end
90
+
91
+ before(:each) do
63
92
  @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge :schema => @schema)
64
93
  end
65
94
 
95
+ after(:each) do
96
+ @olap.role_name = nil if @olap
97
+ end
98
+
66
99
  it "should connect" do
67
100
  @olap.should be_connected
68
101
  end
69
102
 
70
103
  it "should get available role names" do
71
- @olap.available_role_names.should == [@role_name, @role_name2]
104
+ @olap.available_role_names.sort.should == @all_roles.sort
72
105
  end
73
106
 
74
107
  it "should not get role name if not set" do
@@ -113,17 +146,40 @@ describe "Connection role" do
113
146
  # end
114
147
 
115
148
  it "should not get non-visible member when role name set in connection parameters" do
116
- @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge :schema => @schema,
117
- :role => @role_name)
118
- @cube = @olap.cube('Sales')
119
- @cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
149
+ olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge schema: @schema, role: @role_name)
150
+ cube = olap.cube('Sales')
151
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
120
152
  end
121
153
 
122
154
  it "should not get non-visible member when several role names set in connection parameters" do
123
- @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge :schema => @schema,
124
- :roles => [@role_name, @role_name2])
125
- @cube = @olap.cube('Sales')
126
- @cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
155
+ olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge schema: @schema, roles: [@role_name, @role_name2])
156
+ cube = olap.cube('Sales')
157
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
158
+ end
159
+
160
+ it "should see members from ragged dimensions when using single role" do
161
+ # Workaround for a Mondrian bug which does not allow access to ragged dimensions when using single role.
162
+ # This syntax will create a union role with one role.
163
+ @olap.role_names = [@role_name]
164
+ cube = @olap.cube('Sales')
165
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
166
+ cube.member('[Gender].[All Genders]').should_not be_nil
167
+ end
168
+
169
+ it "should see members from ragged dimensions when using multiple roles" do
170
+ @olap.role_names = [@role_name, @role_name2]
171
+ cube = @olap.cube('Sales')
172
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
173
+ cube.member('[Gender].[All Genders]').should_not be_nil
174
+ end
175
+
176
+ # Test patch for UnionRoleImpl getBottomLevelDepth method
177
+ it "should see member as drillable when using union of union role" do
178
+ @olap.role_names = [@union_role_name]
179
+ cube = @olap.cube('Sales')
180
+ cube.member('[Customers].[All Customers]').should be_drillable
181
+ cube.member('[Customers].[All Customers].[USA]').should be_drillable
182
+ cube.member('[Customers].[All Customers].[USA].[CA]').should_not be_drillable
127
183
  end
128
184
 
129
185
  end
@@ -45,13 +45,14 @@ describe "Connection" do
45
45
  schema_field = @olap.raw_schema.getClass.getDeclaredField("schema")
46
46
  schema_field.setAccessible(true)
47
47
  private_schema = schema_field.get(@olap.raw_schema)
48
- private_schema.getDialect.java_class.name.should == case MONDRIAN_DRIVER
48
+ private_schema.getDialect.java_class.name.should == case MONDRIAN_DRIVER.split('_').last
49
49
  when 'mysql' then 'mondrian.spi.impl.MySqlDialect'
50
50
  when 'postgresql' then 'mondrian.spi.impl.PostgreSqlDialect'
51
51
  when 'oracle' then 'mondrian.spi.impl.OracleDialect'
52
- when 'luciddb' then 'mondrian.spi.impl.LucidDbDialect'
53
52
  when 'mssql' then 'mondrian.spi.impl.MicrosoftSqlServerDialect'
54
53
  when 'sqlserver' then 'mondrian.spi.impl.MicrosoftSqlServerDialect'
54
+ when 'vertica' then 'mondrian.spi.impl.VerticaDialect'
55
+ when 'snowflake' then 'mondrian.spi.impl.SnowflakeDialect'
55
56
  end
56
57
  end
57
58
 
@@ -0,0 +1,261 @@
1
+ require "spec_helper"
2
+
3
+ describe "Cube" do
4
+ before(:all) do
5
+ @schema = Mondrian::OLAP::Schema.define do
6
+ measures_caption 'Measures caption'
7
+
8
+ cube 'Sales' do
9
+ description 'Sales description'
10
+ caption 'Sales caption'
11
+ annotations :foo => 'bar'
12
+ table 'sales'
13
+ visible true
14
+ dimension 'Gender', :foreign_key => 'customer_id' do
15
+ description 'Gender description'
16
+ caption 'Gender caption'
17
+ visible true
18
+ hierarchy :has_all => true, :primary_key => 'id' do
19
+ description 'Gender hierarchy description'
20
+ caption 'Gender hierarchy caption'
21
+ all_member_name 'All Genders'
22
+ all_member_caption 'All Genders caption'
23
+ table 'customers'
24
+ visible true
25
+ level 'Gender', :column => 'gender', :unique_members => true,
26
+ :description => 'Gender level description', :caption => 'Gender level caption' do
27
+ visible true
28
+ # Dimension values SQL generated by caption_expression fails on PostgreSQL and MS SQL
29
+ if %w(mysql oracle).include?(MONDRIAN_DRIVER)
30
+ caption_expression do
31
+ sql "'dummy'"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ dimension 'Customers', :foreign_key => 'customer_id', :annotations => {:foo => 'bar'} do
38
+ hierarchy :has_all => true, :all_member_name => 'All Customers', :primary_key => 'id', :annotations => {:foo => 'bar'} do
39
+ table 'customers'
40
+ level 'Country', :column => 'country', :unique_members => true, :annotations => {:foo => 'bar'}
41
+ level 'State Province', :column => 'state_province', :unique_members => true
42
+ level 'City', :column => 'city', :unique_members => false
43
+ level 'Name', :column => 'fullname', :unique_members => true
44
+ end
45
+ end
46
+ calculated_member 'Non-USA', :annotations => {:foo => 'bar'} do
47
+ dimension 'Customers'
48
+ formula '[Customers].[All Customers] - [Customers].[USA]'
49
+ end
50
+ dimension 'Time', :foreign_key => 'time_id', :type => 'TimeDimension' do
51
+ hierarchy :has_all => false, :primary_key => 'id' do
52
+ table 'time'
53
+ level 'Year', :column => 'the_year', :type => 'Numeric', :unique_members => true, :level_type => 'TimeYears'
54
+ level 'Quarter', :column => 'quarter', :unique_members => false, :level_type => 'TimeQuarters'
55
+ level 'Month', :column => 'month_of_year', :type => 'Numeric', :unique_members => false, :level_type => 'TimeMonths'
56
+ end
57
+ hierarchy 'Weekly', :has_all => false, :primary_key => 'id' do
58
+ table 'time'
59
+ level 'Year', :column => 'the_year', :type => 'Numeric', :unique_members => true, :level_type => 'TimeYears'
60
+ level 'Week', :column => 'weak_of_year', :type => 'Numeric', :unique_members => false, :level_type => 'TimeWeeks'
61
+ end
62
+ end
63
+ calculated_member 'Last week' do
64
+ hierarchy '[Time.Weekly]'
65
+ formula 'Tail([Time.Weekly].[Week].Members).Item(0)'
66
+ end
67
+ measure 'Unit Sales', :column => 'unit_sales', :aggregator => 'sum', :annotations => {:foo => 'bar'}
68
+ measure 'Store Sales', :column => 'store_sales', :aggregator => 'sum'
69
+ measure 'Store Cost', :column => 'store_cost', :aggregator => 'sum', :visible => false
70
+ end
71
+ end
72
+ @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge :schema => @schema)
73
+ end
74
+
75
+ # Do not execute tests on analytical databases with slow individual inserts
76
+ describe 'cache', unless: %w(vertica snowflake).include?(MONDRIAN_DRIVER) do
77
+ def qt(name)
78
+ @connection.quote_table_name(name.to_s)
79
+ end
80
+
81
+ before(:all) do
82
+ @connection = ActiveRecord::Base.connection
83
+ @cube = @olap.cube('Sales')
84
+ @query = <<-SQL
85
+ SELECT {[Measures].[Store Cost], [Measures].[Store Sales]} ON COLUMNS
86
+ FROM [Sales]
87
+ WHERE ([Time].[2010].[Q1], [Customers].[USA].[CA])
88
+ SQL
89
+
90
+ case MONDRIAN_DRIVER
91
+ when 'mysql', 'jdbc_mysql', 'postgresql', 'oracle'
92
+ @connection.execute 'CREATE TABLE sales_copy AS SELECT * FROM sales'
93
+ when 'mssql', 'sqlserver'
94
+ # Use raw_connection.execute to avoid detecting this query as a SELECT query
95
+ # for which executeQuery JDBC method will fail
96
+ @connection.raw_connection.execute_update 'SELECT * INTO sales_copy FROM sales'
97
+ end
98
+ end
99
+
100
+ after(:each) do
101
+ @connection.execute 'TRUNCATE TABLE sales'
102
+ @connection.execute 'INSERT INTO sales SELECT * FROM sales_copy'
103
+
104
+ @olap.flush_schema_cache
105
+ @olap.close
106
+ @olap.connect
107
+ end
108
+
109
+ after(:all) do
110
+ @connection.execute 'DROP TABLE sales_copy'
111
+ end
112
+
113
+ it 'should clear cache for deleted data at lower level with segments' do
114
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
115
+ @connection.execute <<-SQL
116
+ DELETE FROM sales
117
+ WHERE time_id IN (SELECT id
118
+ FROM #{qt :time}
119
+ WHERE the_year = 2010
120
+ AND quarter = 'Q1')
121
+ AND customer_id IN (SELECT id
122
+ FROM customers
123
+ WHERE country = 'USA'
124
+ AND state_province = 'CA'
125
+ AND city = 'Berkeley')
126
+ SQL
127
+ @cube.flush_region_cache_with_segments(%w(Time 2010 Q1), %w(Customers USA CA))
128
+ @olap.execute(@query).values.should == [6756.4296, 11156.28]
129
+ end
130
+
131
+ it 'should clear cache for deleted data at same level with segments' do
132
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
133
+ @connection.execute <<-SQL
134
+ DELETE FROM sales
135
+ WHERE time_id IN (SELECT id
136
+ FROM #{qt :time}
137
+ WHERE the_year = 2010
138
+ AND quarter = 'Q1')
139
+ AND customer_id IN (SELECT id
140
+ FROM customers
141
+ WHERE country = 'USA'
142
+ AND state_province = 'CA')
143
+ SQL
144
+ @cube.flush_region_cache_with_segments(%w(Time 2010 Q1), %w(Customers USA CA))
145
+ @olap.execute(@query).values.should == [nil, nil]
146
+ end
147
+
148
+ it 'should clear cache for update data at lower level with segments' do
149
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
150
+ @connection.execute <<-SQL
151
+ UPDATE sales SET
152
+ store_sales = store_sales + 1,
153
+ store_cost = store_cost + 1
154
+ WHERE time_id IN (SELECT id
155
+ FROM #{qt :time}
156
+ WHERE the_year = 2010
157
+ AND quarter = 'Q1')
158
+ AND customer_id IN (SELECT id
159
+ FROM customers
160
+ WHERE country = 'USA'
161
+ AND state_province = 'CA'
162
+ AND city = 'Berkeley')
163
+ SQL
164
+ @cube.flush_region_cache_with_segments(%w(Time 2010 Q1), %w(Customers USA CA))
165
+ @olap.execute(@query).values.should == [6891.553, 11391.4]
166
+ end
167
+
168
+ it 'should clear cache for update data at same level with segments' do
169
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
170
+ @connection.execute <<-SQL
171
+ UPDATE sales SET
172
+ store_sales = store_sales + 1,
173
+ store_cost = store_cost + 1
174
+ WHERE time_id IN (SELECT id
175
+ FROM #{qt :time}
176
+ WHERE the_year = 2010
177
+ AND quarter = 'Q1')
178
+ AND customer_id IN (SELECT id
179
+ FROM customers
180
+ WHERE country = 'USA'
181
+ AND state_province = 'CA')
182
+ SQL
183
+ @cube.flush_region_cache_with_segments(%w(Time 2010 Q1), %w(Customers USA CA))
184
+ @olap.execute(@query).values.should == [6935.553, 11435.4]
185
+ end
186
+
187
+ it 'should clear cache for deleted data at lower level with members' do
188
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
189
+ @connection.execute <<-SQL
190
+ DELETE FROM sales
191
+ WHERE time_id IN (SELECT id
192
+ FROM #{qt :time}
193
+ WHERE the_year = 2010
194
+ AND quarter = 'Q1')
195
+ AND customer_id IN (SELECT id
196
+ FROM customers
197
+ WHERE country = 'USA'
198
+ AND state_province = 'CA'
199
+ AND city = 'Berkeley')
200
+ SQL
201
+ @cube.flush_region_cache_with_full_names('[Time].[2010].[Q1]', '[Customers].[USA].[CA]')
202
+ @olap.execute(@query).values.should == [6756.4296, 11156.28]
203
+ end
204
+
205
+ it 'should clear cache for deleted data at same level with members' do
206
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
207
+ @connection.execute <<-SQL
208
+ DELETE FROM sales
209
+ WHERE time_id IN (SELECT id
210
+ FROM #{qt :time}
211
+ WHERE the_year = 2010
212
+ AND quarter = 'Q1')
213
+ AND customer_id IN (SELECT id
214
+ FROM customers
215
+ WHERE country = 'USA'
216
+ AND state_province = 'CA')
217
+ SQL
218
+ @cube.flush_region_cache_with_full_names('[Time].[2010].[Q1]', '[Customers].[USA].[CA]')
219
+ @olap.execute(@query).values.should == [nil, nil]
220
+ end
221
+
222
+ it 'should clear cache for update data at lower level with members' do
223
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
224
+ @connection.execute <<-SQL
225
+ UPDATE sales SET
226
+ store_sales = store_sales + 1,
227
+ store_cost = store_cost + 1
228
+ WHERE time_id IN (SELECT id
229
+ FROM #{qt :time}
230
+ WHERE the_year = 2010
231
+ AND quarter = 'Q1')
232
+ AND customer_id IN (SELECT id
233
+ FROM customers
234
+ WHERE country = 'USA'
235
+ AND state_province = 'CA'
236
+ AND city = 'Berkeley')
237
+ SQL
238
+ @cube.flush_region_cache_with_full_names('[Time].[2010].[Q1]', '[Customers].[USA].[CA]')
239
+ @olap.execute(@query).values.should == [6891.553, 11391.4]
240
+ end
241
+
242
+ it 'should clear cache for update data at same level with members' do
243
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
244
+ @connection.execute <<-SQL
245
+ UPDATE sales SET
246
+ store_sales = store_sales + 1,
247
+ store_cost = store_cost + 1
248
+ WHERE time_id IN (SELECT id
249
+ FROM #{qt :time}
250
+ WHERE the_year = 2010
251
+ AND quarter = 'Q1')
252
+ AND customer_id IN (SELECT id
253
+ FROM customers
254
+ WHERE country = 'USA'
255
+ AND state_province = 'CA')
256
+ SQL
257
+ @cube.flush_region_cache_with_full_names('[Time].[2010].[Q1]', '[Customers].[USA].[CA]')
258
+ @olap.execute(@query).values.should == [6935.553, 11435.4]
259
+ end
260
+ end
261
+ end