mondrian-olap 1.2.0 → 1.3.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
  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