mondrian-olap 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5b807f5866de6c32810049ddaa15cf62725bdd14db8c92be93828172cc4a0c6
4
- data.tar.gz: 34eb04b5545e340509f8699ce499a447a32effee40749e1b580d6063b7d596cf
3
+ metadata.gz: f016aae4b1d6d9f4138ae3bded251944cf7fcfe8f1bd0579df7f962eb1a52d03
4
+ data.tar.gz: 8b5a4d5c4526f2fd7a239c3dccd57c7d08f5f1d842354fae5bd13f21920e7e73
5
5
  SHA512:
6
- metadata.gz: 503b38add95142332eb40c8885075461b98bb2cbbb2f49446b213754a593b94fd2226a965e2d90383e96b163eeab180a5de999cc3631b347060e5c59323ef83b
7
- data.tar.gz: 7a9e42bda5d2acc62ccda1da5ab63bb63ad51c49828ed08e89a83e5c4e1a2742118be320c925e7771e29e95166b2e73e26420ac5f7132c4ffb3a2e4221c8ce83
6
+ metadata.gz: 7a9b3633a62cef270a163c25c5f1511442938b6705b2385fb2b7e2c2a8f9a1a2041b3f041af0be995970fa86307c4fe1af3c0fb94488c86cc1ec617b93acfe3b
7
+ data.tar.gz: 82ee663a5017af81ed6089f6f162e6d8a98458c1a7afb50e5bf5c3b27ea7422b91e81cc38e099a0a23d7ae99368b8c425083c55b6a6d1f1b75a04566cc664b0a
data/Changelog.md CHANGED
@@ -1,4 +1,24 @@
1
- ### Master
1
+ ### 1.3.0 / 2023-06-02
2
+
3
+ * New features
4
+ * Upgrade to the latest Mondrian version 9.3.0.0 with additional patches
5
+ * Additional Mondrian patches and improvements
6
+ * Support for ClickHouse database
7
+ * Support for MariaDB ColumnStore engine
8
+ * Improve Aggregate performance of large compound slicers when mondrian.rolap.EnableInMemoryRollup=false
9
+ * Improve performance of Mondrian member property value lookup
10
+ * Set dynamic Mondrian connection pool size based on mondrian.rolap.maxSqlThreads property
11
+ * Skip registration of MondrianOlap4jDriver if mondrian.olap4j.registerDriver=false
12
+ * Enable Mondrian supportsMultiValueInExpr for PostgreSQL and Oracle
13
+ * Upgrade to log4j2
14
+ * Support instance parameter for SQL Server connection
15
+ * Allow to specify high_cardinality for dimension
16
+ * Remove deprecated jTDS driver support, use MS JDBC driver instead
17
+ * Remove user defined functions and formatters in JavaScript and CoffeeScript as they are not supported since JVM 8
18
+ * Upgrade tests to use the later ActiveRecord and RSpec versions
19
+ * Tested with JRuby 9.4 and JVM 17
20
+ * Bug fixes
21
+ * Patch for MONDRIAN-2714 (fixed support for MySQL JDBC driver version 8.0.23)
2
22
 
3
23
  ### 1.2.0 / 2021-03-06
4
24
 
data/README.md CHANGED
@@ -215,8 +215,8 @@ See more examples of dimension and member queries in `spec/cube_spec.rb`.
215
215
 
216
216
  ### User defined MDX functions
217
217
 
218
- You can define new MDX functions using JavaScript, CoffeeScript or Ruby language that you can later use
219
- either in calculated member formulas or in MDX queries. Here are examples of user defined functions in Ruby:
218
+ You can define new MDX functions using Ruby that you can later use either in calculated member formulas or in MDX queries.
219
+ Here are examples of user defined functions in Ruby:
220
220
 
221
221
  ```ruby
222
222
  schema = Mondrian::OLAP::Schema.define do
@@ -273,9 +273,9 @@ See more examples of data access roles in `spec/connection_role_spec.rb`.
273
273
  REQUIREMENTS
274
274
  ------------
275
275
 
276
- mondrian-olap gem is compatible with JRuby version 9.2.x, and Java 8 and 11 VM. mondrian-olap works only with JRuby and not with other Ruby implementations as it includes Mondrian OLAP Java libraries.
276
+ mondrian-olap gem is compatible with JRuby versions 9.3.x and 9.4.x, JVM 8, 11, and 17. mondrian-olap works only with JRuby and not with other Ruby implementations as it includes Mondrian OLAP Java libraries.
277
277
 
278
- mondrian-olap supports MySQL, PostgreSQL, Oracle, Microsoft SQL Server, Vertica and Snowflake databases as well as other databases that are supported by Mondrian OLAP engine (using jdbc_driver and jdbc_url connection parameters). When using MySQL or PostgreSQL databases then install jdbc-mysql or jdbc-postgres gem and require "jdbc/mysql" or "jdbc/postgres" to load the corresponding JDBC database driver. When using Oracle then require Oracle JDBC driver (`ojdbc8.jar` for Java 8). When using SQL Server you can choose between the jTDS or Microsoft JDBC drivers. If you use jTDS require "jdbc/jtds". If you use the Microsoft JDBC driver then require the latest `mssql-jdbc-*.jar`. When using Vertica or Snowflake then require corresponding JDBC drivers.
278
+ mondrian-olap supports MySQL, PostgreSQL, Oracle, Microsoft SQL Server, Vertica, Snowflake, and ClickHouse databases as well as other databases that are supported by Mondrian OLAP engine (using jdbc_driver and jdbc_url connection parameters). When using MySQL or PostgreSQL databases then install jdbc-mysql or jdbc-postgres gem and require "jdbc/mysql" or "jdbc/postgres" to load the corresponding JDBC database driver. When using Oracle then require Oracle JDBC driver `ojdbc*.jar`. When using MS SQL Server you then use the Microsoft JDBC driver `mssql-jdbc-*.jar`. When using Vertica, Snowflake, or ClickHouse then require corresponding JDBC drivers.
279
279
 
280
280
  INSTALL
281
281
  -------
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.0
1
+ 1.3.0
Binary file
@@ -289,6 +289,14 @@ module Mondrian
289
289
  true
290
290
  end
291
291
 
292
+ def jdbc_uri
293
+ if respond_to?(method_name = "jdbc_uri_#{@driver}", true)
294
+ send method_name
295
+ else
296
+ raise ArgumentError, 'unknown JDBC driver'
297
+ end
298
+ end
299
+
292
300
  private
293
301
 
294
302
  def connection_string
@@ -306,17 +314,10 @@ module Mondrian
306
314
  string + (@params[:catalog] ? "Catalog=#{catalog_uri}" : "CatalogContent=#{quote_string(catalog_content)}")
307
315
  end
308
316
 
309
- def jdbc_uri
310
- if respond_to?(method_name = "jdbc_uri_#{@driver}", true)
311
- send method_name
312
- else
313
- raise ArgumentError, 'unknown JDBC driver'
314
- end
315
- end
316
-
317
317
  def jdbc_uri_generic(options = {})
318
318
  uri_prefix = options[:uri_prefix] || "jdbc:#{@driver}://"
319
- uri = "#{uri_prefix}#{@params[:host]}#{@params[:port] && ":#{@params[:port]}"}"
319
+ port = @params[:port] || options[:default_port]
320
+ uri = "#{uri_prefix}#{@params[:host]}#{port && ":#{port}"}"
320
321
  uri += "/#{@params[:database]}" if @params[:database] && options[:add_database] != false
321
322
  properties = new_empty_properties
322
323
  properties.merge!(options[:default_properties]) if options[:default_properties].is_a?(Hash)
@@ -343,6 +344,7 @@ module Mondrian
343
344
 
344
345
  alias_method :jdbc_uri_postgresql, :jdbc_uri_generic
345
346
  alias_method :jdbc_uri_vertica, :jdbc_uri_generic
347
+ alias_method :jdbc_uri_mariadb, :jdbc_uri_generic
346
348
 
347
349
  def jdbc_uri_oracle
348
350
  # connection using TNS alias
@@ -360,18 +362,12 @@ module Mondrian
360
362
  end
361
363
  end
362
364
 
363
- def jdbc_uri_mssql
364
- jdbc_uri_generic(
365
- uri_prefix: 'jdbc:jtds:sqlserver://', separator: ';', first_separator: ';',
366
- default_properties: @params.slice(:instance, :domain, :appname)
367
- )
368
- end
369
-
370
365
  JDBC_SQLSERVER_PARAM_PROPERTIES = {
371
366
  database: 'databaseName',
372
367
  integrated_security: 'integratedSecurity',
373
368
  application_name: 'applicationName',
374
- instance_name: 'instanceName'
369
+ instance_name: 'instanceName',
370
+ instance: 'instanceName'
375
371
  }
376
372
 
377
373
  def jdbc_uri_sqlserver
@@ -404,6 +400,15 @@ module Mondrian
404
400
  )
405
401
  end
406
402
 
403
+ def jdbc_uri_clickhouse
404
+ protocol_prefix = if protocol = @params[:protocol]
405
+ raise ArgumentError, "invalid protocol #{protocol}" unless protocol =~ /\A\w+\z/
406
+ ":#{protocol}"
407
+ end
408
+ uri_prefix = "jdbc:ch#{protocol_prefix}://"
409
+ jdbc_uri_generic(uri_prefix: uri_prefix)
410
+ end
411
+
407
412
  def jdbc_uri_jdbc
408
413
  @params[:jdbc_url] or raise ArgumentError, 'missing jdbc_url parameter'
409
414
  end
@@ -411,10 +416,11 @@ module Mondrian
411
416
  JDBC_DRIVER_CLASS = {
412
417
  'postgresql' => 'org.postgresql.Driver',
413
418
  'oracle' => 'oracle.jdbc.OracleDriver',
414
- 'mssql' => 'net.sourceforge.jtds.jdbc.Driver',
415
419
  'sqlserver' => 'com.microsoft.sqlserver.jdbc.SQLServerDriver',
416
420
  'vertica' => 'com.vertica.jdbc.Driver',
417
- 'snowflake' => 'net.snowflake.client.jdbc.SnowflakeDriver'
421
+ 'snowflake' => 'net.snowflake.client.jdbc.SnowflakeDriver',
422
+ 'clickhouse' => 'com.clickhouse.jdbc.ClickHouseDriver',
423
+ 'mariadb' => 'org.mariadb.jdbc.Driver'
418
424
  }
419
425
 
420
426
  def jdbc_driver
@@ -474,6 +480,9 @@ module Mondrian
474
480
  end
475
481
  end
476
482
 
483
+ # Starting from Mondrian 9.2 additional QueryBody plan string is added at the end which will be ignored.
484
+ QUERY_BODY_PLAN_REGEXP = /\AQueryBody:/
485
+
477
486
  class ProfilingHandler
478
487
  java_implements Java::mondrian.spi.ProfileHandler
479
488
  attr_reader :plan
@@ -481,7 +490,11 @@ module Mondrian
481
490
 
482
491
  java_signature 'void explain(String plan, mondrian.olap.QueryTiming timing)'
483
492
  def explain(plan, timing)
484
- @plan = plan
493
+ if @plan
494
+ @plan += "\n" + plan unless plan =~ QUERY_BODY_PLAN_REGEXP
495
+ else
496
+ @plan = plan
497
+ end
485
498
  @timing = timing
486
499
  end
487
500
  end
@@ -56,15 +56,35 @@ module Mondrian
56
56
  end
57
57
 
58
58
  def dimensions
59
- @dimenstions ||= @raw_cube.getDimensions.map{|d| Dimension.new(self, d)}
59
+ @dimenstions ||= @raw_cube.getDimensions.map { |d| dimension_from_raw(d) }
60
60
  end
61
61
 
62
62
  def dimension_names
63
- dimensions.map{|d| d.name}
63
+ dimensions.map(&:name)
64
64
  end
65
65
 
66
66
  def dimension(name)
67
- dimensions.detect{|d| d.name == name}
67
+ if @dimensions
68
+ @dimensions.detect { |d| d.name == name }
69
+ elsif raw_dimension = @raw_cube.getDimensions.detect { |d| d.getName == name }
70
+ dimension_from_raw(raw_dimension)
71
+ end
72
+ end
73
+
74
+ def hierarchies
75
+ @hierarchies ||= @raw_cube.getHierarchies.map { |h| hierarchy_from_raw(h) }
76
+ end
77
+
78
+ def hierarchy_names
79
+ hierarchies.map(&:name)
80
+ end
81
+
82
+ def hierarchy(name)
83
+ if @hierarchies
84
+ @hierarchies.detect { |h| h.name == name }
85
+ elsif raw_hierarchy = @raw_cube.getHierarchies.detect { |h| h.getName == name }
86
+ hierarchy_from_raw(raw_hierarchy)
87
+ end
68
88
  end
69
89
 
70
90
  def query
@@ -85,6 +105,16 @@ module Mondrian
85
105
 
86
106
  def_delegators :@cache_control, :flush_region_cache_with_segments, :flush_region_cache_with_segments
87
107
  def_delegators :@cache_control, :flush_region_cache_with_full_names, :flush_region_cache_with_full_names
108
+
109
+ private
110
+
111
+ def dimension_from_raw(raw_dimension)
112
+ Dimension.new(self, raw_dimension)
113
+ end
114
+
115
+ def hierarchy_from_raw(raw_hierarchy)
116
+ Hierarchy.new(dimension_from_raw(raw_hierarchy.getDimension), raw_hierarchy)
117
+ end
88
118
  end
89
119
 
90
120
  class Dimension
@@ -156,7 +186,7 @@ module Mondrian
156
186
  @raw_hierarchy = raw_hierarchy
157
187
  end
158
188
 
159
- attr_reader :raw_hierarchy
189
+ attr_reader :raw_hierarchy, :dimension
160
190
 
161
191
  def name
162
192
  @name ||= @raw_hierarchy.getName
@@ -170,6 +200,10 @@ module Mondrian
170
200
  @caption ||= @raw_hierarchy.getCaption
171
201
  end
172
202
 
203
+ def dimension_name
204
+ @dimension.name
205
+ end
206
+
173
207
  def levels
174
208
  @levels = @raw_hierarchy.getLevels.map{|l| Level.new(self, l)}
175
209
  end
@@ -257,6 +291,10 @@ module Mondrian
257
291
  @cardinality = @raw_level.getCardinality
258
292
  end
259
293
 
294
+ def cardinality=(value)
295
+ mondrian_level.setApproxRowCount(value || Java::JavaLang::Integer::MIN_VALUE)
296
+ end
297
+
260
298
  def members_count
261
299
  @members_count ||= begin
262
300
  if cardinality >= 0
@@ -275,6 +313,10 @@ module Mondrian
275
313
  end
276
314
  end
277
315
 
316
+ def mondrian_level
317
+ @raw_level.unwrap(Java::MondrianOlap::Level.java_class)
318
+ end
319
+
278
320
  include Annotated
279
321
  def annotations
280
322
  annotations_for(@raw_level)
@@ -39,7 +39,7 @@ module Mondrian
39
39
 
40
40
  def profiling_timing_string
41
41
  if profiling_timing && (timing_string = profiling_timing.toString)
42
- timing_string.gsub("\r\n", "\n")
42
+ timing_string.gsub("\r\n", "\n").sub(Mondrian::OLAP::Result::QUERY_TIMING_CUMULATIVE_REGEXP, '')
43
43
  end
44
44
  end
45
45
 
@@ -76,7 +76,15 @@ module Mondrian
76
76
  f.accessible = true
77
77
  if cell_set = f.value(statement)
78
78
  cell_set.close
79
- @profiling_handler = statement.getProfileHandler
79
+ # Starting from Mondrian 9.2 query plan was not available in case of error, need to get it explicitly.
80
+ if (@profiling_handler = statement.getProfileHandler) && !@profiling_handler.timing
81
+ query = statement.getQuery
82
+ string_writer = Java::java.io.StringWriter.new
83
+ print_writer = Java::java.io.PrintWriter.new(string_writer)
84
+ query.explain(print_writer)
85
+ print_writer.close
86
+ @profiling_handler.explain(string_writer.toString, cell_set.getQueryTiming)
87
+ end
80
88
  end
81
89
  end
82
90
  end
@@ -296,6 +296,7 @@ module Mondrian
296
296
  }
297
297
 
298
298
  def members_to_mdx(members)
299
+ members ||= []
299
300
  # if only one member which does not end with ] or .Item(...)
300
301
  # then assume it is expression which returns set
301
302
  # TODO: maybe always include also single expressions in {...} to avoid some edge cases?
@@ -127,9 +127,11 @@ module Mondrian
127
127
  profiling_timing && profiling_timing.markFull(name, duration)
128
128
  end
129
129
 
130
+ QUERY_TIMING_CUMULATIVE_REGEXP = /\AQuery Timing \(Cumulative\):\n/
131
+
130
132
  def profiling_timing_string
131
133
  if profiling_timing && (timing_string = profiling_timing.toString)
132
- timing_string.gsub("\r\n", "\n")
134
+ timing_string.gsub("\r\n", "\n").sub(QUERY_TIMING_CUMULATIVE_REGEXP, '')
133
135
  end
134
136
  end
135
137
 
@@ -147,13 +149,12 @@ module Mondrian
147
149
  cell_params << Java::JavaLang::Integer.new(axis_position)
148
150
  end
149
151
  raw_cell = @raw_cell_set.getCell(cell_params)
150
- DrillThrough.from_raw_cell(raw_cell, params)
152
+ DrillThrough.from_raw_cell(raw_cell, params.merge(role_name: @connection.role_name))
151
153
  end
152
154
  end
153
155
 
154
156
  class DrillThrough
155
157
  def self.from_raw_cell(raw_cell, params = {})
156
- max_rows = params[:max_rows] || -1
157
158
  # workaround to avoid calling raw_cell.drillThroughInternal private method
158
159
  # which fails when running inside TorqueBox
159
160
  cell_field = raw_cell.java_class.declared_field('cell')
@@ -161,14 +162,18 @@ module Mondrian
161
162
  rolap_cell = cell_field.value(raw_cell)
162
163
 
163
164
  if params[:return] || rolap_cell.canDrillThrough
164
- sql_statement = drill_through_internal(rolap_cell, params)
165
+ sql_statement, return_fields = drill_through_internal(rolap_cell, params)
165
166
  raw_result_set = sql_statement.getWrappedResultSet
166
- new(raw_result_set)
167
+ raw_cube = raw_cell.getCellSet.getMetaData.getCube
168
+ new(raw_result_set, return_fields: return_fields, raw_cube: raw_cube, role_name: params[:role_name])
167
169
  end
168
170
  end
169
171
 
170
- def initialize(raw_result_set)
172
+ def initialize(raw_result_set, options = {})
171
173
  @raw_result_set = raw_result_set
174
+ @return_fields = options[:return_fields]
175
+ @raw_cube = options[:raw_cube]
176
+ @role_name = options[:role_name]
172
177
  end
173
178
 
174
179
  def column_types
@@ -207,7 +212,7 @@ module Mondrian
207
212
  column_types.each_with_index do |column_type, i|
208
213
  row_values << Result.java_to_ruby_value(@raw_result_set.getObject(i + 1), column_type)
209
214
  end
210
- row_values
215
+ can_access_row_values?(row_values) ? row_values : fetch
211
216
  else
212
217
  @raw_result_set.close
213
218
  nil
@@ -226,6 +231,35 @@ module Mondrian
226
231
 
227
232
  private
228
233
 
234
+ def can_access_row_values?(row_values)
235
+ return true unless @role_name
236
+
237
+ member_full_name_columns_indexes.each do |column_indexes|
238
+ segment_names = [@return_fields[column_indexes.first][:member].getHierarchy.getName]
239
+ column_indexes.each { |i| segment_names << row_values[i].to_s }
240
+ segment_list = Java::OrgOlap4jMdx::IdentifierNode.ofNames(*segment_names).getSegmentList
241
+ return false unless @raw_cube.lookupMember(segment_list)
242
+ end
243
+
244
+ true
245
+ end
246
+
247
+ def member_full_name_columns_indexes
248
+ @member_full_name_columns_indexes ||= begin
249
+ fieldset_columns = Hash.new { |h, k| h[k] = Array.new }
250
+ column_labels.each_with_index do |label, i|
251
+ # Find all role restriction columns with a label pattern "_level:<Fieldset ID>:<Level depth>"
252
+ if label =~ /\A_level:(\d+):(\d+)\z/
253
+ # Group by fieldset ID with a compound value of level depth and column index
254
+ fieldset_columns[$1] << [$2.to_i, i]
255
+ end
256
+ end
257
+ # For each fieldset create an array with columns indexes sorted by level depth
258
+ fieldset_columns.each { |k, v| fieldset_columns[k] = v.sort_by(&:first).map(&:last) }
259
+ fieldset_columns.values
260
+ end
261
+ end
262
+
229
263
  def metadata
230
264
  @metadata ||= @raw_result_set.getMetaData
231
265
  end
@@ -238,7 +272,7 @@ module Mondrian
238
272
  result_field.accessible = true
239
273
  result = result_field.value(rolap_cell)
240
274
 
241
- sql = generate_drill_through_sql(rolap_cell, result, params)
275
+ sql, return_fields = generate_drill_through_sql(rolap_cell, result, params)
242
276
 
243
277
  # Choose the appropriate scrollability. If we need to start from an
244
278
  # offset row, it is useful that the cursor is scrollable, but not
@@ -248,10 +282,8 @@ module Mondrian
248
282
  connection = statement.getMondrianConnection
249
283
  result_set_type = Java::JavaSql::ResultSet::TYPE_FORWARD_ONLY
250
284
  result_set_concurrency = Java::JavaSql::ResultSet::CONCUR_READ_ONLY
251
- schema = statement.getSchema
252
- dialect = schema.getDialect
253
285
 
254
- Java::MondrianRolap::RolapUtil.executeQuery(
286
+ sql_statement = Java::MondrianRolap::RolapUtil.executeQuery(
255
287
  connection.getDataSource,
256
288
  sql,
257
289
  nil,
@@ -267,11 +299,12 @@ module Mondrian
267
299
  result_set_concurrency,
268
300
  nil
269
301
  )
302
+ [sql_statement, return_fields]
270
303
  end
271
304
 
272
305
  def self.generate_drill_through_sql(rolap_cell, result, params)
273
306
  nonempty_columns, return_fields = parse_return_fields(result, params)
274
- return_expressions = return_fields.map{|field| field[:member]}
307
+ return_expressions = return_fields.map { |field| field[:member] }
275
308
 
276
309
  sql_non_extended = rolap_cell.getDrillThroughSQL(return_expressions, false)
277
310
  sql_extended = rolap_cell.getDrillThroughSQL(return_expressions, true)
@@ -314,9 +347,11 @@ module Mondrian
314
347
 
315
348
  return_fields.size.times do |i|
316
349
  column_alias = return_fields[i][:column_alias]
350
+ column_expression = return_fields[i][:column_expression]
351
+ quoted_table_name = return_fields[i][:quoted_table_name]
317
352
  new_select_columns <<
318
- if column_expression = return_fields[i][:column_expression]
319
- new_order_by_columns << column_expression
353
+ if column_expression && (!quoted_table_name || extended_from.include?(quoted_table_name))
354
+ new_order_by_columns << column_expression unless return_fields[i][:name].start_with?('_level:')
320
355
  new_group_by_columns << column_expression if group_by && return_fields[i][:type] != :measure
321
356
  "#{column_expression} AS #{column_alias}"
322
357
  else
@@ -337,12 +372,9 @@ module Mondrian
337
372
  outer_join_from_parts = extended_from.split(/,\s*/) - new_from_parts
338
373
  where_parts = extended_where.split(' and ')
339
374
 
340
- # reverse outer_join_from_parts to support dimensions with several table joins
341
- # where join with detailed level table should be constructed first
342
- outer_join_from_parts.reverse.each do |part|
375
+ outer_join_from_parts.each do |part|
343
376
  part_elements = part.split(/\s+/)
344
377
  # first is original table, then optional 'as' and the last is alias
345
- table_name = part_elements.first
346
378
  table_alias = part_elements.last
347
379
  join_conditions = where_parts.select do |where_part|
348
380
  where_part.include?(" = #{table_alias}.")
@@ -368,7 +400,7 @@ module Mondrian
368
400
  sql = "select #{new_select} from #{new_from} where #{new_where}"
369
401
  sql << " group by #{new_group_by}" unless new_group_by.empty?
370
402
  sql << " order by #{new_order_by}" unless new_order_by.empty?
371
- sql
403
+ [sql, return_fields]
372
404
  end
373
405
 
374
406
  def self.parse_return_fields(result, params)
@@ -396,8 +428,14 @@ module Mondrian
396
428
 
397
429
  # Old versions of Oracle had a limit of 30 character identifiers.
398
430
  # Do not limit it for other databases (as e.g. in MySQL aliases can be longer than column names)
399
- max_alias_length = dialect.getMaxColumnNameLength
400
- max_alias_length = nil if max_alias_length && max_alias_length > 30
431
+ max_alias_length = dialect.getMaxColumnNameLength # 0 means that there is no limit
432
+ max_alias_length = nil if max_alias_length && (max_alias_length > 30 || max_alias_length == 0)
433
+ sql_options = {
434
+ dialect: dialect,
435
+ sql_query: sql_query,
436
+ max_alias_length: max_alias_length,
437
+ params: params
438
+ }
401
439
 
402
440
  return_fields.size.times do |i|
403
441
  member_full_name = return_fields[i][:member_full_name]
@@ -418,44 +456,7 @@ module Mondrian
418
456
  raise ArgumentError, "return field #{member_full_name} should be level or measure"
419
457
  end
420
458
 
421
- return_fields[i][:column_expression] = case return_fields[i][:type]
422
- when :name
423
- if level_or_member.respond_to? :getNameExp
424
- level_or_member.getNameExp.getExpression sql_query
425
- end
426
- when :property
427
- if property = level_or_member.getProperties.to_a.detect{|p| p.getName == return_fields[i][:name]}
428
- # property.getExp is a protected method therefore
429
- # use a workaround to get the value from the field
430
- f = property.java_class.declared_field("exp")
431
- f.accessible = true
432
- if column = f.value(property)
433
- column.getExpression sql_query
434
- end
435
- end
436
- else
437
- if level_or_member.respond_to? :getKeyExp
438
- return_fields[i][:type] = :key
439
- level_or_member.getKeyExp.getExpression sql_query
440
- else
441
- return_fields[i][:type] = :measure
442
- column_expression = level_or_member.getMondrianDefExpression.getExpression sql_query
443
- if params[:group_by]
444
- level_or_member.getAggregator.getExpression column_expression
445
- else
446
- column_expression
447
- end
448
- end
449
- end
450
-
451
- column_alias = if return_fields[i][:type] == :key
452
- "#{return_fields[i][:name]} (Key)"
453
- else
454
- return_fields[i][:name]
455
- end
456
- return_fields[i][:column_alias] = dialect.quoteIdentifier(
457
- max_alias_length ? column_alias[0, max_alias_length] : column_alias
458
- )
459
+ add_sql_attributes return_fields[i], sql_options
459
460
  end
460
461
  end
461
462
 
@@ -479,8 +480,76 @@ module Mondrian
479
480
  end
480
481
  end
481
482
 
483
+ if params[:role_name].present?
484
+ add_role_restricition_fields return_fields, sql_options
485
+ end
486
+
482
487
  [nonempty_columns, return_fields]
483
488
  end
489
+
490
+ def self.add_sql_attributes(field, options = {})
491
+ member = field[:member]
492
+ dialect = options[:dialect]
493
+ sql_query = options[:sql_query]
494
+ max_alias_length = options[:max_alias_length]
495
+ params = options[:params]
496
+
497
+ if table_name = (member.try(:getTableName) || member.try(:getMondrianDefExpression).try(:table))
498
+ field[:quoted_table_name] = dialect.quoteIdentifier(table_name)
499
+ end
500
+
501
+ field[:column_expression] =
502
+ case field[:type]
503
+ when :name
504
+ if member.respond_to? :getNameExp
505
+ member.getNameExp.getExpression sql_query
506
+ end
507
+ when :property
508
+ if property = member.getProperties.to_a.detect { |p| p.getName == field[:name] }
509
+ # property.getExp is a protected method therefore
510
+ # use a workaround to get the value from the field
511
+ f = property.java_class.declared_field("exp")
512
+ f.accessible = true
513
+ if column = f.value(property)
514
+ column.getExpression sql_query
515
+ end
516
+ end
517
+ when :name_or_key
518
+ member.getNameExp&.getExpression(sql_query) || member.getKeyExp&.getExpression(sql_query)
519
+ else
520
+ if member.respond_to? :getKeyExp
521
+ field[:type] = :key
522
+ member.getKeyExp.getExpression sql_query
523
+ else
524
+ field[:type] = :measure
525
+ column_expression = member.getMondrianDefExpression.getExpression sql_query
526
+ if params[:group_by]
527
+ member.getAggregator.getExpression column_expression
528
+ else
529
+ column_expression
530
+ end
531
+ end
532
+ end
533
+
534
+ column_alias = field[:type] == :key ? "#{field[:name]} (Key)" : field[:name]
535
+ field[:column_alias] = dialect.quoteIdentifier(
536
+ max_alias_length ? column_alias[0, max_alias_length] : column_alias
537
+ )
538
+ end
539
+
540
+ def self.add_role_restricition_fields(fields, options = {})
541
+ # For each unique level field add a set of fields to be able to build level member full name from database query results
542
+ fields.map { |f| f[:member] }.uniq.each_with_index do |level_or_member, i|
543
+ next if level_or_member.is_a?(Java::MondrianOlap::Member)
544
+ current_level = level_or_member
545
+ loop do
546
+ # Create an additional field name using a pattern "_level:<Fieldset ID>:<Level depth>"
547
+ fields << {member: current_level, type: :name_or_key, name: "_level:#{i}:#{current_level.getDepth}"}
548
+ add_sql_attributes fields.last, options
549
+ break unless (current_level = current_level.getParentLevel) and !current_level.isAll
550
+ end
551
+ end
552
+ end
484
553
  end
485
554
 
486
555
  def self.java_to_ruby_value(value, column_type = nil)
@@ -95,7 +95,9 @@ module Mondrian
95
95
  :type,
96
96
  # The name of the column in the fact table which joins to the leaf level of this dimension.
97
97
  # Required in a private Dimension or a DimensionUsage, but not in a public Dimension.
98
- :foreign_key
98
+ :foreign_key,
99
+ # Flag to mark this dimension as a high cardinality one and avoid caching.
100
+ :high_cardinality
99
101
  data_dictionary_names :foreign_key # values in XML will be uppercased when using Oracle driver
100
102
  elements :annotations, :hierarchy
101
103
  end
@@ -113,7 +115,9 @@ module Mondrian
113
115
  :usage_prefix,
114
116
  # The name of the column in the fact table which joins to the leaf level of this dimension.
115
117
  # Required in a private Dimension or a DimensionUsage, but not in a public Dimension.
116
- :foreign_key
118
+ :foreign_key,
119
+ # Flag to mark this dimensions as a high cardinality one and avoid caching.
120
+ :high_cardinality
117
121
  data_dictionary_names :usage_prefix, :foreign_key # values in XML will be uppercased when using Oracle driver
118
122
  elements :annotations
119
123
 
@@ -347,7 +351,9 @@ module Mondrian
347
351
  # Name of the cube which the dimension belongs to, or unspecified if the dimension is shared
348
352
  :cube_name,
349
353
  # Whether this dimension is visible in the user-interface. Default true.
350
- :visible
354
+ :visible,
355
+ # Flag to mark this dimensions as a high cardinality one and avoid caching.
356
+ :high_cardinality
351
357
  elements :annotations
352
358
  end
353
359