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.
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