mondrian-olap 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 62331aaa1c73a54c6adde925d5ee226ed05e73c6
4
- data.tar.gz: 9c1f2557b90a4b1f9cb26da6358eaa36a1d33d79
3
+ metadata.gz: ebf052cd103202ea4f2a37a914d13379776349bf
4
+ data.tar.gz: af98519e88cc3f8a98c8fdf9538847115b499f7a
5
5
  SHA512:
6
- metadata.gz: dedc6bee9ea5e9fb01f00a6de70c6d35edaa5ff6c30b80e7553db2cf30099b153056ac1979286bc4114c866c3d6e71d63723d7ce17026ee8d92e86ecf36d24cd
7
- data.tar.gz: 8b39386fd21332b74baf56c47cabba12667f0d8231f99f24d8a3fd690de9a556d4e65048549ca59e53f26340b49a212b3e365d9e516ef4b90935fd42ab409015
6
+ metadata.gz: 801ab9633c7b0e4efeb83ddc12d7ffebfe1966c6e65065c02692b0aa662efb5552c4fda1acc83a407bb82eaacaf76223ed03d6a4c863a0c7f8673bf32413a55e
7
+ data.tar.gz: 0969711c0ffe2f39b4616b016718294b9b759333890e56f8ba3191f2b64472da645426ddc3e97e7b22b84afae7cb9e0599254f28be14db0875247f3968f37c4a
@@ -1,3 +1,12 @@
1
+ ### 0.8.0 / 2016-10-26
2
+
3
+ * New features
4
+ * upgraded to the latest Mondrian version 3.12.0.6
5
+ * added flush_region_cache_with_segments and flush_region_cache_with_full_names methods for partial clearing of the cache
6
+ * added Name() and Property() extensions for drill_through return fields
7
+ * Bug fixes
8
+ * fixed retrieving of drill through results with Clob values
9
+
1
10
  ### 0.7.0 / 2015-12-12
2
11
 
3
12
  * New features
@@ -1,6 +1,6 @@
1
1
  (The MIT License)
2
2
 
3
- Copyright (c) 2010-2015 Raimonds Simanovskis
3
+ Copyright (c) 2010-2016 Raimonds Simanovskis
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining
6
6
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -12,7 +12,7 @@ One of the most popular open-source OLAP engines is [Mondrian](http://mondrian.p
12
12
 
13
13
  mondrian-olap is JRuby gem which includes Mondrian OLAP engine and provides Ruby DSL for creating OLAP schemas on top of relational database schemas and provides MDX query language and query builder Ruby methods for making analytical queries.
14
14
 
15
- mondrian-olap is used in [eazyBI data analysis and reporting web application](https://eazybi.com). [Private eazyBI](https://eazybi.com/help/private-eazybi) can be used to create easy-to-use web based reports and dashboards on top of mondrian-olap based backend database. There is also [mondrian-olap demo Rails application for trying MDX queries](https://github.com/rsim/mondrian_demo).
15
+ mondrian-olap is used in [eazyBI data analysis and reporting web application](https://eazybi.com). [Private eazyBI](https://eazybi.com/help/private-eazybi) can be used to create easy-to-use web based reports and dashboards on top of mondrian-olap based backend database. There is also [mondrian-olap demo Rails application for trying MDX queries](https://github.com/rsim/mondrian_demo). The [mondrian-rest](https://github.com/jazzido/mondrian-rest) uses mondrian-olap to implement a REST API interface for a Mondrian schema.
16
16
 
17
17
  USAGE
18
18
  -----
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.0
1
+ 0.8.0
@@ -7,7 +7,8 @@ module Mondrian
7
7
  connection
8
8
  end
9
9
 
10
- attr_reader :raw_connection, :raw_catalog, :raw_schema, :raw_schema_reader
10
+ attr_reader :raw_connection, :raw_catalog, :raw_schema,
11
+ :raw_schema_reader, :raw_cache_control
11
12
 
12
13
  def initialize(params={})
13
14
  @params = params
@@ -55,6 +56,7 @@ module Mondrian
55
56
  # currently it is assumed that there is just one schema per connection catalog
56
57
  @raw_schema = @raw_catalog.getSchemas.first
57
58
  @raw_schema_reader = @raw_connection.getMondrianConnection.getSchemaReader
59
+ @raw_cache_control = @raw_connection.getMondrianConnection.getCacheControl(nil)
58
60
  @connected = true
59
61
  true
60
62
  end
@@ -16,6 +16,8 @@ module Mondrian
16
16
  end
17
17
 
18
18
  class Cube
19
+ extend Forwardable
20
+
19
21
  def self.get(connection, name)
20
22
  if raw_cube = connection.raw_schema.getCubes.get(name)
21
23
  Cube.new(connection, raw_cube)
@@ -25,6 +27,7 @@ module Mondrian
25
27
  def initialize(connection, raw_cube)
26
28
  @connection = connection
27
29
  @raw_cube = raw_cube
30
+ @cache_control = CacheControl.new(@connection, self)
28
31
  end
29
32
 
30
33
  attr_reader :raw_cube
@@ -77,6 +80,9 @@ module Mondrian
77
80
  raw_member = @raw_cube.lookupMember(segment_list)
78
81
  raw_member && Member.new(raw_member)
79
82
  end
83
+
84
+ def_delegators :@cache_control, :flush_region_cache_with_segments, :flush_region_cache_with_segments
85
+ def_delegators :@cache_control, :flush_region_cache_with_full_names, :flush_region_cache_with_full_names
80
86
  end
81
87
 
82
88
  class Dimension
@@ -374,6 +380,10 @@ module Mondrian
374
380
  end
375
381
  end
376
382
 
383
+ def mondrian_member
384
+ @raw_member.unwrap(Java::MondrianOlap::Member.java_class)
385
+ end
386
+
377
387
  include Annotated
378
388
  def annotations
379
389
  annotations_for(@raw_member)
@@ -400,7 +410,35 @@ module Mondrian
400
410
  end
401
411
  end
402
412
  end
413
+ end
414
+
415
+ class CacheControl
416
+ def initialize(connection, cube)
417
+ @connection = connection
418
+ @cube = cube
419
+ @mondrian_cube = @cube.raw_cube.unwrap(Java::MondrianOlap::Cube.java_class)
420
+ @cache_control = @connection.raw_cache_control
421
+ end
403
422
 
423
+ def flush_region_cache_with_segments(*segment_names)
424
+ members = segment_names.map { |name| @cube.member_by_segments(*name).mondrian_member }
425
+ flush(members)
426
+ end
427
+
428
+ def flush_region_cache_with_full_names(*full_names)
429
+ members = full_names.map { |name| @cube.member(*name).mondrian_member }
430
+ flush(members)
431
+ end
432
+
433
+ private
434
+
435
+ def flush(members)
436
+ regions = members.map do |member|
437
+ @cache_control.create_member_region(member, true)
438
+ end
439
+ regions << @cache_control.create_measures_region(@mondrian_cube)
440
+ @cache_control.flush(@cache_control.create_crossjoin_region(*regions))
441
+ end
404
442
  end
405
443
  end
406
444
  end
@@ -242,7 +242,8 @@ module Mondrian
242
242
  end
243
243
 
244
244
  def self.generate_drill_through_sql(rolap_cell, result, params)
245
- return_field_names, return_expressions, nonempty_columns = parse_return_fields(result, params)
245
+ nonempty_columns, return_fields = parse_return_fields(result, params)
246
+ return_expressions = return_fields.map{|field| field[:member]}
246
247
 
247
248
  sql_non_extended = rolap_cell.getDrillThroughSQL(return_expressions, false)
248
249
  sql_extended = rolap_cell.getDrillThroughSQL(return_expressions, true)
@@ -273,25 +274,30 @@ module Mondrian
273
274
  raise ArgumentError, "cannot parse drill through SQL: #{sql_extended}"
274
275
  end
275
276
 
276
- return_column_positions = {}
277
-
278
- if return_field_names && !return_field_names.empty?
279
- new_select = extended_select.split(/,\s*/).map do |part|
280
- column_name, column_alias = part.split(' as ')
281
- field_name = column_alias[1..-2].gsub(' (Key)', '')
282
- position = return_field_names.index(field_name) || 9999
283
- return_column_positions[column_name] = position
284
- [part, position]
285
- end.sort_by(&:last).map(&:first).join(', ')
286
-
287
- new_order_by = extended_order_by.split(/,\s*/).map do |part|
288
- column_name, asc_desc = part.split(/\s+/)
289
- position = return_column_positions[column_name] || 9999
290
- [part, position]
291
- end.sort_by(&:last).map(&:first).join(', ')
277
+ if return_fields.present?
278
+ new_select_columns = []
279
+ new_order_by_columns = []
280
+ new_group_by_columns = []
281
+ group_by = params[:group_by]
282
+
283
+ return_fields.size.times do |i|
284
+ column_alias = return_fields[i][:column_alias]
285
+ new_select_columns << if column_expression = return_fields[i][:column_expression]
286
+ new_order_by_columns << column_expression
287
+ new_group_by_columns << column_expression if group_by && return_fields[i][:type] != :measure
288
+ "#{column_expression} AS #{column_alias}"
289
+ else
290
+ "'' AS #{column_alias}"
291
+ end
292
+ end
293
+
294
+ new_select = new_select_columns.join(', ')
295
+ new_order_by = new_order_by_columns.join(', ')
296
+ new_group_by = new_group_by_columns.join(', ')
292
297
  else
293
298
  new_select = extended_select
294
299
  new_order_by = extended_order_by
300
+ new_group_by = ''
295
301
  end
296
302
 
297
303
  new_from_parts = non_extended_from.split(/,\s*/)
@@ -327,40 +333,89 @@ module Mondrian
327
333
  end
328
334
 
329
335
  sql = "select #{new_select} from #{new_from} where #{new_where}"
336
+ sql << " group by #{new_group_by}" unless new_group_by.empty?
330
337
  sql << " order by #{new_order_by}" unless new_order_by.empty?
331
338
  sql
332
339
  end
333
340
 
334
341
  def self.parse_return_fields(result, params)
335
- return_field_names = []
336
- return_expressions = []
337
342
  nonempty_columns = []
343
+ return_fields = []
338
344
 
339
345
  if params[:return] || params[:nonempty]
340
346
  rolap_cube = result.getCube
341
347
  schema_reader = rolap_cube.getSchemaReader
348
+ dialect = result.getCube.getSchema.getDialect
349
+ sql_query = Java::mondrian.rolap.sql.SqlQuery.new(dialect)
350
+
351
+ if fields = params[:return]
352
+ fields = fields.split(/,\s*/) if fields.is_a? String
353
+ fields.each do |field|
354
+ return_fields << case field
355
+ when /\AName\((.*)\)\z/i then
356
+ { member_full_name: $1, type: :name }
357
+ when /\AProperty\((.*)\s*,\s*'(.*)'\)\z/i then
358
+ { member_full_name: $1, type: :property, name: $2 }
359
+ else
360
+ { member_full_name: field }
361
+ end
362
+ end
342
363
 
343
- if return_fields = params[:return]
344
- return_fields = return_fields.split(/,\s*/) if return_fields.is_a?(String)
345
- return_expressions = return_fields.map do |return_field|
364
+ return_fields.size.times do | i |
365
+ member_full_name = return_fields[i][:member_full_name]
346
366
  begin
347
- segment_list = Java::MondrianOlap::Util.parseIdentifier(return_field)
348
- return_field_names << segment_list.to_a.last.name
367
+ segment_list = Java::MondrianOlap::Util.parseIdentifier(member_full_name)
349
368
  rescue Java::JavaLang::IllegalArgumentException
350
- raise ArgumentError, "invalid return field #{return_field}"
369
+ raise ArgumentError, "invalid return field #{member_full_name}"
351
370
  end
352
371
 
372
+ # if this is property field then the name is initilized already
373
+ return_fields[i][:name] ||= segment_list.to_a.last.name
353
374
  level_or_member = schema_reader.lookupCompound rolap_cube, segment_list, false, 0
375
+ return_fields[i][:member] = level_or_member
376
+
377
+ if level_or_member.is_a? Java::MondrianOlap::Member
378
+ raise ArgumentError, "cannot use calculated member #{member_full_name} as return field" if level_or_member.isCalculated
379
+ elsif !level_or_member.is_a? Java::MondrianOlap::Level
380
+ raise ArgumentError, "return field #{member_full_name} should be level or measure"
381
+ end
354
382
 
355
- case level_or_member
356
- when Java::MondrianOlap::Level
357
- level_or_member
358
- when Java::MondrianOlap::Member
359
- raise ArgumentError, "cannot use calculated member #{return_field} as return field" if level_or_member.isCalculated
360
- level_or_member
383
+ return_fields[i][:column_expression] = case return_fields[i][:type]
384
+ when :name
385
+ if level_or_member.respond_to? :getNameExp
386
+ level_or_member.getNameExp.getExpression sql_query
387
+ end
388
+ when :property
389
+ if property = level_or_member.getProperties.to_a.detect{|p| p.getName == return_fields[i][:name]}
390
+ # property.getExp is a protected method therefore
391
+ # use a workaround to get the value from the field
392
+ f = property.java_class.declared_field("exp")
393
+ f.accessible = true
394
+ if column = f.value(property)
395
+ column.getExpression sql_query
396
+ end
397
+ end
398
+ else
399
+ if level_or_member.respond_to? :getKeyExp
400
+ return_fields[i][:type] = :key
401
+ level_or_member.getKeyExp.getExpression sql_query
402
+ else
403
+ return_fields[i][:type] = :measure
404
+ column_expression = level_or_member.getMondrianDefExpression.getExpression sql_query
405
+ if params[:group_by]
406
+ level_or_member.getAggregator.getExpression column_expression
407
+ else
408
+ column_expression
409
+ end
410
+ end
411
+ end
412
+
413
+ column_alias = if return_fields[i][:type] == :key
414
+ "#{return_fields[i][:name]} (Key)"
361
415
  else
362
- raise ArgumentError, "return field #{return_field} should be level or measure"
416
+ return_fields[i][:name]
363
417
  end
418
+ return_fields[i][:column_alias] = dialect.quoteIdentifier(column_alias)
364
419
  end
365
420
  end
366
421
 
@@ -370,23 +425,22 @@ module Mondrian
370
425
  begin
371
426
  segment_list = Java::MondrianOlap::Util.parseIdentifier(nonempty_field)
372
427
  rescue Java::JavaLang::IllegalArgumentException
373
- raise ArgumentError, "invalid return field #{return_field}"
428
+ raise ArgumentError, "invalid return field #{nonempty_field}"
374
429
  end
375
430
  member = schema_reader.lookupCompound rolap_cube, segment_list, false, 0
376
431
  if member.is_a? Java::MondrianOlap::Member
377
- raise ArgumentError, "cannot use calculated member #{return_field} as nonempty field" if member.isCalculated
432
+ raise ArgumentError, "cannot use calculated member #{nonempty_field} as nonempty field" if member.isCalculated
378
433
  sql_query = member.getStarMeasure.getSqlQuery
379
434
  member.getStarMeasure.generateExprString(sql_query)
380
435
  else
381
- raise ArgumentError, "nonempty field #{return_field} should be measure"
436
+ raise ArgumentError, "nonempty field #{nonempty_field} should be measure"
382
437
  end
383
438
  end
384
439
  end
385
440
  end
386
441
 
387
- [return_field_names, return_expressions, nonempty_columns]
442
+ [nonempty_columns, return_fields]
388
443
  end
389
-
390
444
  end
391
445
 
392
446
  def self.java_to_ruby_value(value, column_type = nil)
@@ -395,6 +449,8 @@ module Mondrian
395
449
  value
396
450
  when Java::JavaMath::BigDecimal
397
451
  BigDecimal(value.to_s)
452
+ when Java::JavaSql::Clob
453
+ clob_to_string(value)
398
454
  else
399
455
  value
400
456
  end
@@ -402,6 +458,23 @@ module Mondrian
402
458
 
403
459
  private
404
460
 
461
+ def self.clob_to_string(value)
462
+ if reader = value.getCharacterStream
463
+ buffered_reader = Java::JavaIo::BufferedReader.new(reader)
464
+ result = []
465
+ while str = buffered_reader.readLine
466
+ result << str
467
+ end
468
+ result.join("\n")
469
+ end
470
+ ensure
471
+ if buffered_reader
472
+ buffered_reader.close
473
+ elsif reader
474
+ reader.close
475
+ end
476
+ end
477
+
405
478
  def axes
406
479
  @axes ||= @raw_cell_set.getAxes
407
480
  end
@@ -0,0 +1,267 @@
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
+ describe 'cache', unless: MONDRIAN_DRIVER == 'luciddb' do
76
+ before(:all) do
77
+ @connection = ActiveRecord::Base.connection
78
+ @cube = @olap.cube('Sales')
79
+ @query = <<-SQL
80
+ SELECT {[Measures].[Store Cost], [Measures].[Store Sales]} ON COLUMNS
81
+ FROM [Sales]
82
+ WHERE ([Time].[2010].[Q1], [Customers].[USA].[CA])
83
+ SQL
84
+
85
+ case MONDRIAN_DRIVER
86
+ when 'mysql', 'jdbc_mysql', 'postgresql', 'oracle'
87
+ @connection.execute 'CREATE TABLE sales_copy AS SELECT * FROM sales'
88
+ when 'mssql', 'sqlserver'
89
+ # Use raw_connection.execute to avoid detecting this query as a SELECT query
90
+ # for which executeQuery JDBC method will fail
91
+ @connection.raw_connection.execute 'SELECT * INTO sales_copy FROM sales'
92
+ end
93
+ end
94
+
95
+ after(:each) do
96
+ case MONDRIAN_DRIVER
97
+ when 'mysql', 'jdbc_mysql', 'postgresql', 'oracle'
98
+ @connection.execute 'TRUNCATE TABLE sales'
99
+ @connection.execute 'INSERT INTO sales SELECT * FROM sales_copy'
100
+ when 'mssql', 'sqlserver'
101
+ @connection.execute 'TRUNCATE TABLE sales'
102
+ @connection.execute 'INSERT INTO sales SELECT * FROM sales_copy'
103
+ end
104
+
105
+ @olap.flush_schema_cache
106
+ @olap.close
107
+ @olap.connect
108
+ end
109
+
110
+ after(:all) do
111
+ case MONDRIAN_DRIVER
112
+ when 'mysql', 'jdbc_mysql', 'postgresql', 'oracle'
113
+ @connection.execute 'DROP TABLE sales_copy'
114
+ when 'mssql', 'sqlserver'
115
+ @connection.execute 'DROP TABLE sales_copy'
116
+ end
117
+ end
118
+
119
+ it 'should clear cache for deleted data at lower level with segments' do
120
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
121
+ @connection.execute <<-SQL
122
+ DELETE FROM sales
123
+ WHERE time_id IN (SELECT id
124
+ FROM TIME
125
+ WHERE the_year = 2010
126
+ AND quarter = 'Q1')
127
+ AND customer_id IN (SELECT id
128
+ FROM customers
129
+ WHERE country = 'USA'
130
+ AND state_province = 'CA'
131
+ AND city = 'Berkeley')
132
+ SQL
133
+ @cube.flush_region_cache_with_segments(%w(Time 2010 Q1), %w(Customers USA CA))
134
+ @olap.execute(@query).values.should == [6756.4296, 11156.28]
135
+ end
136
+
137
+ it 'should clear cache for deleted data at same level with segments' do
138
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
139
+ @connection.execute <<-SQL
140
+ DELETE FROM sales
141
+ WHERE time_id IN (SELECT id
142
+ FROM TIME
143
+ WHERE the_year = 2010
144
+ AND quarter = 'Q1')
145
+ AND customer_id IN (SELECT id
146
+ FROM customers
147
+ WHERE country = 'USA'
148
+ AND state_province = 'CA')
149
+ SQL
150
+ @cube.flush_region_cache_with_segments(%w(Time 2010 Q1), %w(Customers USA CA))
151
+ @olap.execute(@query).values.should == [nil, nil]
152
+ end
153
+
154
+ it 'should clear cache for update data at lower level with segments' do
155
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
156
+ @connection.execute <<-SQL
157
+ UPDATE sales SET
158
+ store_sales = store_sales + 1,
159
+ store_cost = store_cost + 1
160
+ WHERE time_id IN (SELECT id
161
+ FROM TIME
162
+ WHERE the_year = 2010
163
+ AND quarter = 'Q1')
164
+ AND customer_id IN (SELECT id
165
+ FROM customers
166
+ WHERE country = 'USA'
167
+ AND state_province = 'CA'
168
+ AND city = 'Berkeley')
169
+ SQL
170
+ @cube.flush_region_cache_with_segments(%w(Time 2010 Q1), %w(Customers USA CA))
171
+ @olap.execute(@query).values.should == [6891.553, 11391.4]
172
+ end
173
+
174
+ it 'should clear cache for update data at same level with segments' do
175
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
176
+ @connection.execute <<-SQL
177
+ UPDATE sales SET
178
+ store_sales = store_sales + 1,
179
+ store_cost = store_cost + 1
180
+ WHERE time_id IN (SELECT id
181
+ FROM TIME
182
+ WHERE the_year = 2010
183
+ AND quarter = 'Q1')
184
+ AND customer_id IN (SELECT id
185
+ FROM customers
186
+ WHERE country = 'USA'
187
+ AND state_province = 'CA')
188
+ SQL
189
+ @cube.flush_region_cache_with_segments(%w(Time 2010 Q1), %w(Customers USA CA))
190
+ @olap.execute(@query).values.should == [6935.553, 11435.4]
191
+ end
192
+
193
+ it 'should clear cache for deleted data at lower level with members' do
194
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
195
+ @connection.execute <<-SQL
196
+ DELETE FROM sales
197
+ WHERE time_id IN (SELECT id
198
+ FROM TIME
199
+ WHERE the_year = 2010
200
+ AND quarter = 'Q1')
201
+ AND customer_id IN (SELECT id
202
+ FROM customers
203
+ WHERE country = 'USA'
204
+ AND state_province = 'CA'
205
+ AND city = 'Berkeley')
206
+ SQL
207
+ @cube.flush_region_cache_with_full_names('[Time].[2010].[Q1]', '[Customers].[USA].[CA]')
208
+ @olap.execute(@query).values.should == [6756.4296, 11156.28]
209
+ end
210
+
211
+ it 'should clear cache for deleted data at same level with members' do
212
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
213
+ @connection.execute <<-SQL
214
+ DELETE FROM sales
215
+ WHERE time_id IN (SELECT id
216
+ FROM TIME
217
+ WHERE the_year = 2010
218
+ AND quarter = 'Q1')
219
+ AND customer_id IN (SELECT id
220
+ FROM customers
221
+ WHERE country = 'USA'
222
+ AND state_province = 'CA')
223
+ SQL
224
+ @cube.flush_region_cache_with_full_names('[Time].[2010].[Q1]', '[Customers].[USA].[CA]')
225
+ @olap.execute(@query).values.should == [nil, nil]
226
+ end
227
+
228
+ it 'should clear cache for update data at lower level with members' do
229
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
230
+ @connection.execute <<-SQL
231
+ UPDATE sales SET
232
+ store_sales = store_sales + 1,
233
+ store_cost = store_cost + 1
234
+ WHERE time_id IN (SELECT id
235
+ FROM TIME
236
+ WHERE the_year = 2010
237
+ AND quarter = 'Q1')
238
+ AND customer_id IN (SELECT id
239
+ FROM customers
240
+ WHERE country = 'USA'
241
+ AND state_province = 'CA'
242
+ AND city = 'Berkeley')
243
+ SQL
244
+ @cube.flush_region_cache_with_full_names('[Time].[2010].[Q1]', '[Customers].[USA].[CA]')
245
+ @olap.execute(@query).values.should == [6891.553, 11391.4]
246
+ end
247
+
248
+ it 'should clear cache for update data at same level with members' do
249
+ @olap.execute(@query).values.should == [6890.553, 11390.4]
250
+ @connection.execute <<-SQL
251
+ UPDATE sales SET
252
+ store_sales = store_sales + 1,
253
+ store_cost = store_cost + 1
254
+ WHERE time_id IN (SELECT id
255
+ FROM TIME
256
+ WHERE the_year = 2010
257
+ AND quarter = 'Q1')
258
+ AND customer_id IN (SELECT id
259
+ FROM customers
260
+ WHERE country = 'USA'
261
+ AND state_province = 'CA')
262
+ SQL
263
+ @cube.flush_region_cache_with_full_names('[Time].[2010].[Q1]', '[Customers].[USA].[CA]')
264
+ @olap.execute(@query).values.should == [6935.553, 11435.4]
265
+ end
266
+ end
267
+ end
@@ -433,7 +433,5 @@ describe "Cube" do
433
433
  it "should get member empty annotations" do
434
434
  @cube.member('[Customers].[USA]').annotations.should == {}
435
435
  end
436
-
437
436
  end
438
-
439
437
  end
@@ -87,6 +87,7 @@ fullname
87
87
  </SQL>
88
88
  </OrdinalExpression>
89
89
  <Property name="Gender" column="gender"/>
90
+ <Property name="Description" column="description"/>
90
91
  </Level>
91
92
  </Hierarchy>
92
93
  </Dimension>
@@ -87,6 +87,7 @@ FULLNAME
87
87
  </SQL>
88
88
  </OrdinalExpression>
89
89
  <Property name="Gender" column="GENDER"/>
90
+ <Property name="Description" column="DESCRIPTION"/>
90
91
  </Level>
91
92
  </Hierarchy>
92
93
  </Dimension>
@@ -817,8 +817,7 @@ describe "Query" do
817
817
  end
818
818
 
819
819
  it "should return correct row value types" do
820
- @drill_through.rows.first.map(&:class).should ==
821
- case MONDRIAN_DRIVER
820
+ expected_value_types = case MONDRIAN_DRIVER
822
821
  when "oracle"
823
822
  [
824
823
  BigDecimal, String, BigDecimal, BigDecimal, BigDecimal,
@@ -831,7 +830,8 @@ describe "Query" do
831
830
  [
832
831
  Fixnum, String, Fixnum, Fixnum, Fixnum,
833
832
  String, String, String, String, String, String,
834
- String, String, String, BigDecimal,
833
+ # last one can be BigDecimal or Fixnum, probably depends on MS SQL version
834
+ String, String, String, Numeric,
835
835
  String,
836
836
  BigDecimal
837
837
  ]
@@ -844,6 +844,10 @@ describe "Query" do
844
844
  BigDecimal
845
845
  ]
846
846
  end
847
+
848
+ @drill_through.rows.first.each_with_index do |value, i|
849
+ value.should be_a expected_value_types[i]
850
+ end
847
851
  end
848
852
 
849
853
  it "should return only specified max rows" do
@@ -857,8 +861,7 @@ describe "Query" do
857
861
  @query = @olap.from('Sales')
858
862
  @result = @query.columns('[Measures].[Unit Sales]', '[Measures].[Store Sales]').
859
863
  rows('[Product].children').
860
- # where('[Time].[2010].[Q1]', '[Customers].[USA].[CA]').
861
- where('[Time].[2010].[Q1]').
864
+ where('[Time].[2010].[Q1]', '[Time].[2010].[Q2]').
862
865
  execute
863
866
  end
864
867
 
@@ -870,9 +873,9 @@ describe "Query" do
870
873
  '[Measures].[Unit Sales]', '[Measures].[Store Sales]'
871
874
  ])
872
875
  @drill_through.column_labels.should == [
873
- "Month",
874
- "City",
875
- "Product Family",
876
+ "Month (Key)",
877
+ "City (Key)",
878
+ "Product Family (Key)",
876
879
  "Unit Sales", "Store Sales"
877
880
  ]
878
881
  end
@@ -888,6 +891,74 @@ describe "Query" do
888
891
  @drill_through.rows.all?{|r| r.any?{|c| c}}.should be_true
889
892
  end
890
893
 
894
+ it "should return member name and property values" do
895
+ @drill_through = @result.drill_through(row: 0, column: 0,
896
+ return: [
897
+ "Name([Customers].[Name])",
898
+ "Property([Customers].[Name], 'Gender')",
899
+ "Property([Customers].[Name], 'Description')"
900
+ ]
901
+ )
902
+ @drill_through.column_labels.should == [ "Name", "Gender", "Description" ]
903
+ @drill_through.rows.should == @sql.select_rows(<<-SQL
904
+ SELECT
905
+ customers.fullname,
906
+ customers.gender,
907
+ customers.description
908
+ FROM
909
+ sales,
910
+ customers,
911
+ time,
912
+ products,
913
+ product_classes
914
+ WHERE
915
+ (time.quarter = 'Q1' OR time.quarter = 'Q2') AND
916
+ time.the_year = 2010 AND
917
+ product_classes.product_family = 'Drink' AND
918
+ products.product_class_id = product_classes.id AND
919
+ sales.product_id = products.id AND
920
+ sales.time_id = time.id AND
921
+ customers.id = sales.customer_id
922
+ ORDER BY
923
+ customers.fullname,
924
+ customers.gender,
925
+ customers.description
926
+ SQL
927
+ )
928
+ end
929
+
930
+ it "should group by" do
931
+ @drill_through = @result.drill_through(row: 0, column: 0,
932
+ return: [
933
+ "[Product].[Product Family]",
934
+ "[Measures].[Unit Sales]",
935
+ "[Measures].[Store Cost]"
936
+ ],
937
+ group_by: true
938
+ )
939
+ @drill_through.column_labels.should == [ "Product Family (Key)", "Unit Sales", "Store Cost" ]
940
+ @drill_through.rows.should == @sql.select_rows(<<-SQL
941
+ SELECT
942
+ product_classes.product_family,
943
+ SUM(sales.unit_sales) AS unit_sales,
944
+ SUM(sales.store_cost) AS store_cost
945
+ FROM
946
+ sales,
947
+ time,
948
+ products,
949
+ product_classes
950
+ WHERE
951
+ (time.quarter = 'Q1' OR time.quarter = 'Q2') AND
952
+ time.the_year = 2010 AND
953
+ product_classes.product_family = 'Drink' AND
954
+ products.product_class_id = product_classes.id AND
955
+ sales.product_id = products.id AND
956
+ sales.time_id = time.id
957
+ GROUP BY
958
+ product_classes.product_family
959
+ SQL
960
+ )
961
+ end
891
962
  end
892
963
 
893
964
  describe "drill through statement" do
@@ -40,6 +40,14 @@ namespace :db do
40
40
  t.string :lname, :limit => 30
41
41
  t.string :fullname, :limit => 60
42
42
  t.string :gender, :limit => 30
43
+ # Mondrian does not support properties with Oracle CLOB type
44
+ # as it tries to GROUP BY all columns when loading a dimension table
45
+ if MONDRIAN_DRIVER == 'oracle'
46
+ t.string :description, :limit => 4000
47
+ else
48
+ t.text :description
49
+ end
50
+
43
51
  end
44
52
 
45
53
  create_table :sales, :force => true, :id => false do |t|
@@ -207,7 +215,8 @@ namespace :db do
207
215
  :fname => "First#{i}",
208
216
  :lname => "Last#{i}",
209
217
  :fullname => "First#{i} Last#{i}",
210
- :gender => i % 2 == 0 ? "M" : "F"
218
+ :gender => i % 2 == 0 ? "M" : "F",
219
+ :description => 100.times.map{"1234567890"}.join("\n")
211
220
  )
212
221
  end
213
222
  end
@@ -119,6 +119,7 @@ when 'mssql'
119
119
  url << ";instance=#{DATABASE_INSTANCE}" if DATABASE_INSTANCE
120
120
  AR_CONNECTION_PARAMS = {
121
121
  :adapter => 'jdbc',
122
+ :dialect => 'Microsoft SQL Server',
122
123
  :driver => JDBC_DRIVER,
123
124
  :url => url,
124
125
  :username => CONNECTION_PARAMS[:username],
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mondrian-olap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raimonds Simanovskis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-12-12 00:00:00.000000000 Z
11
+ date: 2016-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -56,14 +56,14 @@ dependencies:
56
56
  name: rspec
57
57
  version_requirements: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ">="
59
+ - - '='
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: '2.14'
62
62
  requirement: !ruby/object:Gem::Requirement
63
63
  requirements:
64
- - - ">="
64
+ - - '='
65
65
  - !ruby/object:Gem::Version
66
- version: '0'
66
+ version: '2.14'
67
67
  prerelease: false
68
68
  type: :development
69
69
  - !ruby/object:Gem::Dependency
@@ -168,14 +168,14 @@ dependencies:
168
168
  name: activerecord-oracle_enhanced-adapter
169
169
  version_requirements: !ruby/object:Gem::Requirement
170
170
  requirements:
171
- - - ">="
171
+ - - "~>"
172
172
  - !ruby/object:Gem::Version
173
- version: '0'
173
+ version: 1.5.6
174
174
  requirement: !ruby/object:Gem::Requirement
175
175
  requirements:
176
- - - ">="
176
+ - - "~>"
177
177
  - !ruby/object:Gem::Version
178
- version: '0'
178
+ version: 1.5.6
179
179
  prerelease: false
180
180
  type: :development
181
181
  - !ruby/object:Gem::Dependency
@@ -250,7 +250,7 @@ files:
250
250
  - lib/mondrian/jars/javacup-10k.jar
251
251
  - lib/mondrian/jars/log4j-1.2.17.jar
252
252
  - lib/mondrian/jars/log4j.properties
253
- - lib/mondrian/jars/mondrian-3.11.0.0-353.jar
253
+ - lib/mondrian/jars/mondrian-3.12.0.6-237.jar
254
254
  - lib/mondrian/jars/olap4j-1.2.0.jar
255
255
  - lib/mondrian/olap.rb
256
256
  - lib/mondrian/olap/connection.rb
@@ -264,6 +264,7 @@ files:
264
264
  - lib/mondrian/olap/version.rb
265
265
  - spec/connection_role_spec.rb
266
266
  - spec/connection_spec.rb
267
+ - spec/cube_cache_control_spec.rb
267
268
  - spec/cube_spec.rb
268
269
  - spec/fixtures/MondrianTest.xml
269
270
  - spec/fixtures/MondrianTestOracle.xml
@@ -300,6 +301,7 @@ summary: JRuby API for Mondrian OLAP Java library
300
301
  test_files:
301
302
  - spec/connection_role_spec.rb
302
303
  - spec/connection_spec.rb
304
+ - spec/cube_cache_control_spec.rb
303
305
  - spec/cube_spec.rb
304
306
  - spec/fixtures/MondrianTest.xml
305
307
  - spec/fixtures/MondrianTestOracle.xml