mondrian-olap 0.8.0 → 1.1.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.
@@ -79,7 +79,7 @@ module Mondrian
79
79
  self
80
80
  end
81
81
 
82
- def filter(condition, options={})
82
+ def filter(condition, options = {})
83
83
  raise ArgumentError, "cannot use filter method before axis or with_set method" unless @current_set
84
84
  @current_set.replace [:filter, @current_set.clone, condition]
85
85
  @current_set << options[:as] if options[:as]
@@ -138,7 +138,7 @@ module Mondrian
138
138
  end
139
139
  end
140
140
 
141
- def hierarchize(order=nil, all=nil)
141
+ def hierarchize(order = nil, all = nil)
142
142
  raise ArgumentError, "cannot use hierarchize method before axis or with_set method" unless @current_set
143
143
  order = order && order.to_s.upcase
144
144
  raise ArgumentError, "invalid hierarchize order #{order.inspect}" unless order.nil? || order == 'POST'
@@ -152,7 +152,7 @@ module Mondrian
152
152
  self
153
153
  end
154
154
 
155
- def hierarchize_all(order=nil)
155
+ def hierarchize_all(order = nil)
156
156
  hierarchize(order, :all)
157
157
  end
158
158
 
@@ -236,19 +236,15 @@ module Mondrian
236
236
  end
237
237
 
238
238
  def execute(parameters = {})
239
- Error.wrap_native_exception do
240
- @connection.execute to_mdx, parameters
241
- end
239
+ @connection.execute to_mdx, parameters
242
240
  end
243
241
 
244
242
  def execute_drill_through(options = {})
245
- Error.wrap_native_exception do
246
- drill_through_mdx = "DRILLTHROUGH "
247
- drill_through_mdx << "MAXROWS #{options[:max_rows]} " if options[:max_rows]
248
- drill_through_mdx << to_mdx
249
- drill_through_mdx << " RETURN #{Array(options[:return]).join(',')}" if options[:return]
250
- @connection.execute_drill_through drill_through_mdx
251
- end
243
+ drill_through_mdx = "DRILLTHROUGH "
244
+ drill_through_mdx << "MAXROWS #{options[:max_rows]} " if options[:max_rows]
245
+ drill_through_mdx << to_mdx
246
+ drill_through_mdx << " RETURN #{Array(options[:return]).join(',')}" if options[:return]
247
+ @connection.execute_drill_through drill_through_mdx
252
248
  end
253
249
 
254
250
  private
@@ -300,10 +296,10 @@ module Mondrian
300
296
  }
301
297
 
302
298
  def members_to_mdx(members)
303
- # if only one member which does not end with ]
299
+ # if only one member which does not end with ] or .Item(...)
304
300
  # then assume it is expression which returns set
305
301
  # TODO: maybe always include also single expressions in {...} to avoid some edge cases?
306
- if members.length == 1 && members[0][-1,1] != ']'
302
+ if members.length == 1 && members[0] !~ /(\]|\.Item\(\d+\))\z/i
307
303
  members[0]
308
304
  elsif members[0].is_a?(Symbol)
309
305
  case members[0]
@@ -380,8 +376,9 @@ module Mondrian
380
376
  end
381
377
 
382
378
  def extract_dimension_name(full_name)
383
- if full_name =~ /\A[^\[]*\[([^\]]+)\]/
384
- $1
379
+ # "[Foo [Bar]]].[Baz]" => "Foo [Bar]"
380
+ if full_name
381
+ full_name.gsub(/\A\[|\]\z/, '').split('].[').first.try(:gsub, ']]', ']')
385
382
  end
386
383
  end
387
384
  end
@@ -3,12 +3,14 @@ require 'bigdecimal'
3
3
  module Mondrian
4
4
  module OLAP
5
5
  class Result
6
- def initialize(connection, raw_cell_set)
6
+ def initialize(connection, raw_cell_set, options = {})
7
7
  @connection = connection
8
8
  @raw_cell_set = raw_cell_set
9
+ @profiling_handler = options[:profiling_handler]
10
+ @total_duration = options[:total_duration]
9
11
  end
10
12
 
11
- attr_reader :raw_cell_set
13
+ attr_reader :raw_cell_set, :profiling_handler, :total_duration
12
14
 
13
15
  def axes_count
14
16
  axes.length
@@ -105,6 +107,32 @@ module Mondrian
105
107
  end
106
108
  end
107
109
 
110
+ def profiling_plan
111
+ if profiling_handler
112
+ @raw_cell_set.close
113
+ if plan = profiling_handler.plan
114
+ plan.gsub("\r\n", "\n")
115
+ end
116
+ end
117
+ end
118
+
119
+ def profiling_timing
120
+ if profiling_handler
121
+ @raw_cell_set.close
122
+ profiling_handler.timing
123
+ end
124
+ end
125
+
126
+ def profiling_mark_full(name, duration)
127
+ profiling_timing && profiling_timing.markFull(name, duration)
128
+ end
129
+
130
+ def profiling_timing_string
131
+ if profiling_timing && (timing_string = profiling_timing.toString)
132
+ timing_string.gsub("\r\n", "\n")
133
+ end
134
+ end
135
+
108
136
  # Specify drill through cell position, for example, as
109
137
  # :row => 0, :cell => 1
110
138
  # Specify max returned rows with :max_rows parameter
@@ -177,7 +205,7 @@ module Mondrian
177
205
  if @raw_result_set.next
178
206
  row_values = []
179
207
  column_types.each_with_index do |column_type, i|
180
- row_values << Result.java_to_ruby_value(@raw_result_set.getObject(i+1), column_type)
208
+ row_values << Result.java_to_ruby_value(@raw_result_set.getObject(i + 1), column_type)
181
209
  end
182
210
  row_values
183
211
  else
@@ -248,24 +276,28 @@ module Mondrian
248
276
  sql_non_extended = rolap_cell.getDrillThroughSQL(return_expressions, false)
249
277
  sql_extended = rolap_cell.getDrillThroughSQL(return_expressions, true)
250
278
 
251
- if sql_non_extended =~ /\Aselect (.*) from (.*) where (.*) order by (.*)\Z/
279
+ if sql_non_extended =~ /\Aselect (.*) from (.*) where (.*) order by (.*)\Z/m
280
+ non_extended_from = $2
281
+ non_extended_where = $3
282
+ # the latest Mondrian version sometimes returns sql_non_extended without order by
283
+ elsif sql_non_extended =~ /\Aselect (.*) from (.*) where (.*)\Z/m
252
284
  non_extended_from = $2
253
285
  non_extended_where = $3
254
286
  # if drill through total measure with just all members selection
255
- elsif sql_non_extended =~ /\Aselect (.*) from (.*)\Z/
287
+ elsif sql_non_extended =~ /\Aselect (.*) from (.*)\Z/m
256
288
  non_extended_from = $2
257
289
  non_extended_where = "1 = 1" # dummy true condition
258
290
  else
259
291
  raise ArgumentError, "cannot parse drill through SQL: #{sql_non_extended}"
260
292
  end
261
293
 
262
- if sql_extended =~ /\Aselect (.*) from (.*) where (.*) order by (.*)\Z/
294
+ if sql_extended =~ /\Aselect (.*) from (.*) where (.*) order by (.*)\Z/m
263
295
  extended_select = $1
264
296
  extended_from = $2
265
297
  extended_where = $3
266
298
  extended_order_by = $4
267
299
  # if only measures are selected then there will be no order by
268
- elsif sql_extended =~ /\Aselect (.*) from (.*) where (.*)\Z/
300
+ elsif sql_extended =~ /\Aselect (.*) from (.*) where (.*)\Z/m
269
301
  extended_select = $1
270
302
  extended_from = $2
271
303
  extended_where = $3
@@ -282,13 +314,14 @@ module Mondrian
282
314
 
283
315
  return_fields.size.times do |i|
284
316
  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
317
+ new_select_columns <<
318
+ if column_expression = return_fields[i][:column_expression]
319
+ new_order_by_columns << column_expression
320
+ new_group_by_columns << column_expression if group_by && return_fields[i][:type] != :measure
321
+ "#{column_expression} AS #{column_alias}"
322
+ else
323
+ "'' AS #{column_alias}"
324
+ end
292
325
  end
293
326
 
294
327
  new_select = new_select_columns.join(', ')
@@ -361,7 +394,7 @@ module Mondrian
361
394
  end
362
395
  end
363
396
 
364
- return_fields.size.times do | i |
397
+ return_fields.size.times do |i|
365
398
  member_full_name = return_fields[i][:member_full_name]
366
399
  begin
367
400
  segment_list = Java::MondrianOlap::Util.parseIdentifier(member_full_name)
@@ -381,34 +414,34 @@ module Mondrian
381
414
  end
382
415
 
383
416
  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
417
+ when :name
418
+ if level_or_member.respond_to? :getNameExp
419
+ level_or_member.getNameExp.getExpression sql_query
420
+ end
421
+ when :property
422
+ if property = level_or_member.getProperties.to_a.detect{|p| p.getName == return_fields[i][:name]}
423
+ # property.getExp is a protected method therefore
424
+ # use a workaround to get the value from the field
425
+ f = property.java_class.declared_field("exp")
426
+ f.accessible = true
427
+ if column = f.value(property)
428
+ column.getExpression sql_query
397
429
  end
430
+ end
431
+ else
432
+ if level_or_member.respond_to? :getKeyExp
433
+ return_fields[i][:type] = :key
434
+ level_or_member.getKeyExp.getExpression sql_query
398
435
  else
399
- if level_or_member.respond_to? :getKeyExp
400
- return_fields[i][:type] = :key
401
- level_or_member.getKeyExp.getExpression sql_query
436
+ return_fields[i][:type] = :measure
437
+ column_expression = level_or_member.getMondrianDefExpression.getExpression sql_query
438
+ if params[:group_by]
439
+ level_or_member.getAggregator.getExpression column_expression
402
440
  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
441
+ column_expression
410
442
  end
411
443
  end
444
+ end
412
445
 
413
446
  column_alias = if return_fields[i][:type] == :key
414
447
  "#{return_fields[i][:name]} (Key)"
@@ -479,7 +512,7 @@ module Mondrian
479
512
  @axes ||= @raw_cell_set.getAxes
480
513
  end
481
514
 
482
- def axis_positions(map_method, join_with=false)
515
+ def axis_positions(map_method, join_with = false)
483
516
  axes.map do |axis|
484
517
  axis.getPositions.map do |position|
485
518
  names = position.getMembers.map do |member|
@@ -508,7 +541,7 @@ module Mondrian
508
541
  :chapters => 4
509
542
  }.freeze
510
543
 
511
- def recursive_values(value_method, axes_sequence, current_index, cell_params=[])
544
+ def recursive_values(value_method, axes_sequence, current_index, cell_params = [])
512
545
  if axis_number = axes_sequence[current_index]
513
546
  axis_number = AXIS_SYMBOL_TO_NUMBER[axis_number] if axis_number.is_a?(Symbol)
514
547
  positions_size = axes[axis_number].getPositions.size
@@ -497,6 +497,7 @@ module Mondrian
497
497
  end
498
498
 
499
499
  class Annotation < SchemaElement
500
+ attributes :name
500
501
  content :text
501
502
  end
502
503
 
@@ -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)
@@ -125,7 +141,7 @@ module Mondrian
125
141
  def xmlized_attributes(options)
126
142
  # data dictionary values should be in uppercase if schema defined with :upcase_data_dictionary => true
127
143
  # 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]) ||
144
+ upcase_attributes = if options[:upcase_data_dictionary].nil? && %w(oracle luciddb snowflake).include?(options[:driver]) ||
129
145
  options[:upcase_data_dictionary]
130
146
  self.class.data_dictionary_names
131
147
  else
@@ -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)
@@ -4,15 +4,21 @@ describe "Connection role" do
4
4
 
5
5
  describe "create connection" do
6
6
  before(:each) do
7
- @role_name = role_name = 'California manager'
8
- @role_name2 = role_name2 = 'Dummy, with comma'
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
@@ -63,12 +89,16 @@ describe "Connection role" do
63
89
  @olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge :schema => @schema)
64
90
  end
65
91
 
92
+ after(:each) do
93
+ @olap.role_name = nil if @olap
94
+ end
95
+
66
96
  it "should connect" do
67
97
  @olap.should be_connected
68
98
  end
69
99
 
70
100
  it "should get available role names" do
71
- @olap.available_role_names.should == [@role_name, @role_name2]
101
+ @olap.available_role_names.sort.should == @all_roles.sort
72
102
  end
73
103
 
74
104
  it "should not get role name if not set" do
@@ -113,17 +143,40 @@ describe "Connection role" do
113
143
  # end
114
144
 
115
145
  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
146
+ olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge schema: @schema, role: @role_name)
147
+ cube = olap.cube('Sales')
148
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
120
149
  end
121
150
 
122
151
  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
152
+ olap = Mondrian::OLAP::Connection.create(CONNECTION_PARAMS.merge schema: @schema, roles: [@role_name, @role_name2])
153
+ cube = olap.cube('Sales')
154
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
155
+ end
156
+
157
+ it "should see members from ragged dimensions when using single role" do
158
+ # Workaround for a Mondrian bug which does not allow access to ragged dimensions when using single role.
159
+ # This syntax will create a union role with one role.
160
+ @olap.role_names = [@role_name]
161
+ cube = @olap.cube('Sales')
162
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
163
+ cube.member('[Gender].[All Genders]').should_not be_nil
164
+ end
165
+
166
+ it "should see members from ragged dimensions when using multiple roles" do
167
+ @olap.role_names = [@role_name, @role_name2]
168
+ cube = @olap.cube('Sales')
169
+ cube.member('[Customers].[USA].[CA].[Los Angeles]').should be_nil
170
+ cube.member('[Gender].[All Genders]').should_not be_nil
171
+ end
172
+
173
+ # Test patch for UnionRoleImpl getBottomLevelDepth method
174
+ it "should see member as drillable when using union of union role" do
175
+ @olap.role_names = [@union_role_name]
176
+ cube = @olap.cube('Sales')
177
+ cube.member('[Customers].[All Customers]').should be_drillable
178
+ cube.member('[Customers].[All Customers].[USA]').should be_drillable
179
+ cube.member('[Customers].[All Customers].[USA].[CA]').should_not be_drillable
127
180
  end
128
181
 
129
182
  end