activerecord-oracle_enhanced-adapter 1.7.11 → 1.8.0.beta1

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/Gemfile +20 -11
  4. data/History.md +123 -4
  5. data/RUNNING_TESTS.md +79 -55
  6. data/Rakefile +13 -19
  7. data/VERSION +1 -1
  8. data/activerecord-oracle_enhanced-adapter.gemspec +16 -17
  9. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +1 -1
  10. data/lib/active_record/connection_adapters/oracle_enhanced/column.rb +7 -59
  11. data/lib/active_record/connection_adapters/oracle_enhanced/column_dumper.rb +6 -50
  12. data/lib/active_record/connection_adapters/oracle_enhanced/connection.rb +11 -11
  13. data/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb +117 -117
  14. data/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +30 -23
  15. data/lib/active_record/connection_adapters/oracle_enhanced/database_tasks.rb +10 -10
  16. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb +48 -70
  17. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_quoting.rb +1 -4
  18. data/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb +51 -69
  19. data/lib/active_record/connection_adapters/oracle_enhanced/oci_quoting.rb +4 -4
  20. data/lib/active_record/connection_adapters/oracle_enhanced/procedures.rb +76 -76
  21. data/lib/active_record/connection_adapters/oracle_enhanced/quoting.rb +13 -42
  22. data/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb +60 -64
  23. data/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb +33 -47
  24. data/lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb +146 -159
  25. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +94 -132
  26. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements_ext.rb +3 -3
  27. data/lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb +65 -100
  28. data/lib/active_record/connection_adapters/oracle_enhanced/version.rb +1 -1
  29. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +250 -487
  30. data/lib/active_record/oracle_enhanced/type/boolean.rb +7 -10
  31. data/lib/active_record/oracle_enhanced/type/integer.rb +3 -4
  32. data/lib/active_record/oracle_enhanced/type/national_character_string.rb +1 -1
  33. data/lib/active_record/oracle_enhanced/type/raw.rb +2 -3
  34. data/lib/active_record/oracle_enhanced/type/string.rb +2 -2
  35. data/lib/active_record/oracle_enhanced/type/text.rb +2 -2
  36. data/lib/activerecord-oracle_enhanced-adapter.rb +2 -2
  37. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +57 -131
  38. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +32 -34
  39. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +40 -42
  40. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +83 -85
  41. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +205 -286
  42. data/spec/active_record/connection_adapters/oracle_enhanced_database_tasks_spec.rb +14 -6
  43. data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +3 -5
  44. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +42 -49
  45. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +1 -3
  46. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +68 -71
  47. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +51 -92
  48. data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +221 -327
  49. data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +16 -18
  50. data/spec/spec_helper.rb +59 -57
  51. metadata +10 -10
@@ -5,16 +5,16 @@ module ActiveRecord
5
5
  def _type_cast(value)
6
6
  case value
7
7
  when ActiveModel::Type::Binary::Data
8
- lob_value = value == '' ? ' ' : value
8
+ lob_value = value == "" ? " " : value
9
9
  bind_type = OCI8::BLOB
10
10
  ora_value = bind_type.new(@connection.raw_oci_connection, lob_value)
11
- ora_value.size = 0 if value == ''
11
+ ora_value.size = 0 if value == ""
12
12
  ora_value
13
13
  when ActiveRecord::OracleEnhanced::Type::Text::Data
14
- lob_value = value.to_s == '' ? ' ' : value.to_s
14
+ lob_value = value.to_s == "" ? " " : value.to_s
15
15
  bind_type = OCI8::CLOB
16
16
  ora_value = bind_type.new(@connection.raw_oci_connection, lob_value)
17
- ora_value.size = 0 if value.to_s == ''
17
+ ora_value.size = 0 if value.to_s == ""
18
18
  ora_value
19
19
  else
20
20
  super
@@ -1,4 +1,4 @@
1
- require 'active_support'
1
+ require "active_support"
2
2
 
3
3
  module ActiveRecord #:nodoc:
4
4
  # Custom create, update, delete methods functionality.
@@ -96,99 +96,99 @@ module ActiveRecord #:nodoc:
96
96
 
97
97
  private
98
98
 
99
- # Creates a record with custom create method
100
- # and returns its id.
101
- def _create_record
102
- # check if class has custom create method
103
- if self.class.custom_create_method
104
- # run before/after callbacks defined in model
105
- run_callbacks(:create) do
106
- # timestamp
107
- if self.record_timestamps
108
- current_time = current_time_from_proper_timezone
109
-
110
- all_timestamp_attributes.each do |column|
111
- if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
112
- write_attribute(column.to_s, current_time)
99
+ # Creates a record with custom create method
100
+ # and returns its id.
101
+ def _create_record
102
+ # check if class has custom create method
103
+ if self.class.custom_create_method
104
+ # run before/after callbacks defined in model
105
+ run_callbacks(:create) do
106
+ # timestamp
107
+ if self.record_timestamps
108
+ current_time = current_time_from_proper_timezone
109
+
110
+ all_timestamp_attributes_in_model.each do |column|
111
+ if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
112
+ write_attribute(column.to_s, current_time)
113
+ end
113
114
  end
114
115
  end
116
+ # run
117
+ create_using_custom_method
115
118
  end
116
- # run
117
- create_using_custom_method
119
+ else
120
+ super
118
121
  end
119
- else
120
- super
121
122
  end
122
- end
123
123
 
124
- def create_using_custom_method
125
- log_custom_method("custom create method", "#{self.class.name} Create") do
126
- self.id = instance_eval(&self.class.custom_create_method)
124
+ def create_using_custom_method
125
+ log_custom_method("custom create method", "#{self.class.name} Create") do
126
+ self.id = instance_eval(&self.class.custom_create_method)
127
+ end
128
+ @new_record = false
129
+ # Starting from ActiveRecord 3.0.3 @persisted is used instead of @new_record
130
+ @persisted = true
131
+ id
127
132
  end
128
- @new_record = false
129
- # Starting from ActiveRecord 3.0.3 @persisted is used instead of @new_record
130
- @persisted = true
131
- id
132
- end
133
133
 
134
- # Updates the associated record with custom update method
135
- # Returns the number of affected rows.
136
- def _update_record(attribute_names = @attributes.keys)
137
- # check if class has custom update method
138
- if self.class.custom_update_method
139
- # run before/after callbacks defined in model
140
- run_callbacks(:update) do
141
- # timestamp
142
- if should_record_timestamps?
143
- current_time = current_time_from_proper_timezone
144
-
145
- timestamp_attributes_for_update_in_model.each do |column|
146
- column = column.to_s
147
- next if attribute_changed?(column)
148
- write_attribute(column, current_time)
134
+ # Updates the associated record with custom update method
135
+ # Returns the number of affected rows.
136
+ def _update_record(attribute_names = @attributes.keys)
137
+ # check if class has custom update method
138
+ if self.class.custom_update_method
139
+ # run before/after callbacks defined in model
140
+ run_callbacks(:update) do
141
+ # timestamp
142
+ if should_record_timestamps?
143
+ current_time = current_time_from_proper_timezone
144
+
145
+ timestamp_attributes_for_update_in_model.each do |column|
146
+ column = column.to_s
147
+ next if attribute_changed?(column)
148
+ write_attribute(column, current_time)
149
+ end
150
+ end
151
+ # update just dirty attributes
152
+ if partial_writes?
153
+ # Serialized attributes should always be written in case they've been
154
+ # changed in place.
155
+ update_using_custom_method(changed | (attributes.keys & self.class.columns.select { |column| column.is_a?(Type::Serialized) }))
156
+ else
157
+ update_using_custom_method(attributes.keys)
149
158
  end
150
159
  end
151
- # update just dirty attributes
152
- if partial_writes?
153
- # Serialized attributes should always be written in case they've been
154
- # changed in place.
155
- update_using_custom_method(changed | (attributes.keys & self.class.columns.select {|column| column.is_a?(Type::Serialized)}))
156
- else
157
- update_using_custom_method(attributes.keys)
158
- end
160
+ else
161
+ super
159
162
  end
160
- else
161
- super
162
163
  end
163
- end
164
164
 
165
- def update_using_custom_method(attribute_names)
166
- return 0 if attribute_names.empty?
167
- log_custom_method("custom update method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Update") do
168
- instance_eval(&self.class.custom_update_method)
165
+ def update_using_custom_method(attribute_names)
166
+ return 0 if attribute_names.empty?
167
+ log_custom_method("custom update method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Update") do
168
+ instance_eval(&self.class.custom_update_method)
169
+ end
170
+ 1
169
171
  end
170
- 1
171
- end
172
172
 
173
- # Deletes the record in the database with custom delete method
174
- # and freezes this instance to reflect that no changes should
175
- # be made (since they can't be persisted).
176
- def destroy_using_custom_method
177
- unless new_record? || @destroyed
178
- log_custom_method("custom delete method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Destroy") do
179
- instance_eval(&self.class.custom_delete_method)
173
+ # Deletes the record in the database with custom delete method
174
+ # and freezes this instance to reflect that no changes should
175
+ # be made (since they can't be persisted).
176
+ def destroy_using_custom_method
177
+ unless new_record? || @destroyed
178
+ log_custom_method("custom delete method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Destroy") do
179
+ instance_eval(&self.class.custom_delete_method)
180
+ end
180
181
  end
181
- end
182
182
 
183
- @destroyed = true
184
- freeze
185
- end
183
+ @destroyed = true
184
+ freeze
185
+ end
186
186
 
187
- def log_custom_method(*args)
188
- self.class.connection.send(:log, *args) { yield }
189
- end
187
+ def log_custom_method(*args)
188
+ self.class.connection.send(:log, *args) { yield }
189
+ end
190
190
 
191
- alias_method :update_record, :_update_record if private_method_defined?(:_update_record)
192
- alias_method :create_record, :_create_record if private_method_defined?(:_create_record)
191
+ alias_method :update_record, :_update_record if private_method_defined?(:_update_record)
192
+ alias_method :create_record, :_create_record if private_method_defined?(:_create_record)
193
193
  end
194
194
  end
@@ -76,76 +76,47 @@ module ActiveRecord
76
76
  end
77
77
 
78
78
  def quote_table_name(name) #:nodoc:
79
- name, link = name.to_s.split('@')
80
- @quoted_table_names[name] ||= [name.split('.').map{|n| quote_column_name(n)}.join('.'), quote_database_link(link)].compact.join('@')
79
+ name, link = name.to_s.split("@")
80
+ @quoted_table_names[name] ||= [name.split(".").map { |n| quote_column_name(n) }.join("."), quote_database_link(link)].compact.join("@")
81
81
  end
82
82
 
83
83
  def quote_string(s) #:nodoc:
84
84
  s.gsub(/'/, "''")
85
85
  end
86
86
 
87
- def quote(value, column = nil) #:nodoc:
88
- super
89
- end
90
-
91
87
  def _quote(value) #:nodoc:
92
88
  case value
93
89
  when ActiveRecord::OracleEnhanced::Type::NationalCharacterString::Data then
94
- "N" << "'#{quote_string(value.to_s)}'"
90
+ "N" << "'#{quote_string(value.to_s)}'"
95
91
  when ActiveModel::Type::Binary::Data then
96
- %Q{empty_#{ type_to_sql(column.type.to_sym).downcase rescue 'blob' }()}
92
+ "empty_#{ type_to_sql(column.type.to_sym).downcase rescue 'blob' }()"
97
93
  when ActiveRecord::OracleEnhanced::Type::Text::Data then
98
- %Q{empty_#{ type_to_sql(column.type.to_sym).downcase rescue 'clob' }()}
94
+ "empty_#{ type_to_sql(column.type.to_sym).downcase rescue 'clob' }()"
99
95
  else
100
96
  super
101
97
  end
102
98
  end
103
99
 
104
100
  def quoted_true #:nodoc:
105
- return "'#{self.class.boolean_to_string(true)}'" if emulate_booleans_from_strings
101
+ return "'Y'" if emulate_booleans_from_strings
106
102
  "1".freeze
107
103
  end
108
104
 
109
105
  def unquoted_true #:nodoc:
110
- return "#{self.class.boolean_to_string(true)}" if emulate_booleans_from_strings
106
+ return "Y" if emulate_booleans_from_strings
111
107
  "1".freeze
112
108
  end
113
109
 
114
110
  def quoted_false #:nodoc:
115
- return "'#{self.class.boolean_to_string(false)}'" if emulate_booleans_from_strings
111
+ return "'N'" if emulate_booleans_from_strings
116
112
  "0".freeze
117
113
  end
118
114
 
119
115
  def unquoted_false #:nodoc:
120
- return "#{self.class.boolean_to_string(false)}" if emulate_booleans_from_strings
116
+ return "N" if emulate_booleans_from_strings
121
117
  "0".freeze
122
118
  end
123
119
 
124
- def quote_date_with_to_date(value) #:nodoc:
125
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
126
- `quote_date_with_to_date` will be deprecated in future version of Oracle enhanced adapter.
127
- Also this method should not be called directly. Let Abstract adapter `_quote` method handle it.
128
- MSG
129
- # should support that composite_primary_keys gem will pass date as string
130
- value = quoted_date(value) if value.acts_like?(:date) || value.acts_like?(:time)
131
- "TO_DATE('#{value}','YYYY-MM-DD HH24:MI:SS')"
132
- end
133
-
134
- def quote_timestamp_with_to_timestamp(value) #:nodoc:
135
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
136
- `quote_timestamp_with_to_timestamp` will be deprecated in future version of Oracle enhanced adapter.
137
- Also this method should not be called directly. Let Abstract adapter `_quote` method handle it.
138
- MSG
139
- # add up to 9 digits of fractional seconds to inserted time
140
- value = "#{quoted_date(value)}:#{("%.6f"%value.to_f).split('.')[1]}" if value.acts_like?(:time)
141
- "TO_TIMESTAMP('#{value}','YYYY-MM-DD HH24:MI:SS:FF6')"
142
- end
143
-
144
- # Cast a +value+ to a type that the database understands.
145
- def type_cast(value, column = nil)
146
- super
147
- end
148
-
149
120
  def _type_cast(value)
150
121
  case value
151
122
  when Date, Time
@@ -167,11 +138,11 @@ module ActiveRecord
167
138
  end
168
139
 
169
140
  # if MRI or YARV
170
- if !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby'
171
- require 'active_record/connection_adapters/oracle_enhanced/oci_quoting'
141
+ if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby"
142
+ require "active_record/connection_adapters/oracle_enhanced/oci_quoting"
172
143
  # if JRuby
173
- elsif RUBY_ENGINE == 'jruby'
174
- require 'active_record/connection_adapters/oracle_enhanced/jdbc_quoting'
144
+ elsif RUBY_ENGINE == "jruby"
145
+ require "active_record/connection_adapters/oracle_enhanced/jdbc_quoting"
175
146
  else
176
147
  raise "Unsupported Ruby engine #{RUBY_ENGINE}"
177
148
  end
@@ -4,92 +4,88 @@ module ActiveRecord
4
4
  class SchemaCreation < AbstractAdapter::SchemaCreation
5
5
  private
6
6
 
7
- def visit_ColumnDefinition(o)
8
- case
9
- when o.type.to_sym == :virtual
10
- sql_type = type_to_sql(o.default[:type], o.limit, o.precision, o.scale) if o.default[:type]
11
- return "#{quote_column_name(o.name)} #{sql_type} AS (#{o.default[:as]})"
12
- when [:blob, :clob].include?(sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale).downcase.to_sym)
7
+ def visit_ColumnDefinition(o)
8
+ if [:blob, :clob].include?(sql_type = type_to_sql(o.type, o.options).downcase.to_sym)
13
9
  if (tablespace = default_tablespace_for(sql_type))
14
10
  @lob_tablespaces ||= {}
15
11
  @lob_tablespaces[o.name] = tablespace
16
12
  end
13
+ end
14
+ super
17
15
  end
18
- super
19
- end
20
16
 
21
- def visit_TableDefinition(o)
22
- create_sql = "CREATE#{' GLOBAL TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} "
23
- statements = o.columns.map { |c| accept c }
24
- statements << accept(o.primary_keys) if o.primary_keys
17
+ def visit_TableDefinition(o)
18
+ create_sql = "CREATE#{' GLOBAL TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} "
19
+ statements = o.columns.map { |c| accept c }
20
+ statements << accept(o.primary_keys) if o.primary_keys
25
21
 
26
- if supports_foreign_keys?
27
- statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
28
- end
22
+ if supports_foreign_keys_in_create?
23
+ statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
24
+ end
29
25
 
30
- create_sql << "(#{statements.join(', ')})" if statements.present?
26
+ create_sql << "(#{statements.join(', ')})" if statements.present?
31
27
 
32
- unless o.temporary
33
- @lob_tablespaces.each do |lob_column, tablespace|
34
- create_sql << " LOB (#{quote_column_name(lob_column)}) STORE AS (TABLESPACE #{tablespace}) \n"
35
- end if defined?(@lob_tablespaces)
36
- create_sql << " ORGANIZATION #{o.organization}" if o.organization
37
- if (tablespace = o.tablespace || default_tablespace_for(:table))
38
- create_sql << " TABLESPACE #{tablespace}"
28
+ unless o.temporary
29
+ @lob_tablespaces.each do |lob_column, tablespace|
30
+ create_sql << " LOB (#{quote_column_name(lob_column)}) STORE AS (TABLESPACE #{tablespace}) \n"
31
+ end if defined?(@lob_tablespaces)
32
+ create_sql << " ORGANIZATION #{o.organization}" if o.organization
33
+ if (tablespace = o.tablespace || default_tablespace_for(:table))
34
+ create_sql << " TABLESPACE #{tablespace}"
35
+ end
39
36
  end
37
+ add_table_options!(create_sql, table_options(o))
38
+ create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
39
+ create_sql
40
40
  end
41
- add_table_options!(create_sql, table_options(o))
42
- create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
43
- create_sql
44
- end
45
41
 
46
- def default_tablespace_for(type)
47
- (ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[type] ||
48
- ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[native_database_types[type][:name]]) rescue nil
49
- end
42
+ def default_tablespace_for(type)
43
+ (ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[type] ||
44
+ ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces[native_database_types[type][:name]]) rescue nil
45
+ end
50
46
 
51
- def add_column_options!(sql, options)
52
- type = options[:type] || ((column = options[:column]) && column.type)
53
- type = type && type.to_sym
54
- # handle case of defaults for CLOB columns, which would otherwise get "quoted" incorrectly
55
- if options_include_default?(options)
56
- if type == :text
57
- sql << " DEFAULT #{@conn.quote(options[:default])}"
58
- else
59
- sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}"
47
+ def add_column_options!(sql, options)
48
+ type = options[:type] || ((column = options[:column]) && column.type)
49
+ type = type && type.to_sym
50
+ # handle case of defaults for CLOB columns, which would otherwise get "quoted" incorrectly
51
+ if options_include_default?(options)
52
+ if type == :text
53
+ sql << " DEFAULT #{@conn.quote(options[:default])}"
54
+ else
55
+ sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}"
56
+ end
57
+ end
58
+ # must explicitly add NULL or NOT NULL to allow change_column to work on migrations
59
+ if options[:null] == false
60
+ sql << " NOT NULL"
61
+ elsif options[:null] == true
62
+ sql << " NULL" unless type == :primary_key
63
+ end
64
+ # add AS expression for virtual columns
65
+ if options[:as].present?
66
+ sql << " AS (#{options[:as]})"
67
+ end
68
+ if options[:primary_key] == true
69
+ sql << " PRIMARY KEY"
60
70
  end
61
71
  end
62
- # must explicitly add NULL or NOT NULL to allow change_column to work on migrations
63
- if options[:null] == false
64
- sql << " NOT NULL"
65
- elsif options[:null] == true
66
- sql << " NULL" unless type == :primary_key
67
- end
68
- # add AS expression for virtual columns
69
- if options[:as].present?
70
- sql << " AS (#{options[:as]})"
71
- end
72
- if options[:primary_key] == true
73
- sql << " PRIMARY KEY"
74
- end
75
- end
76
72
 
77
- def action_sql(action, dependency)
78
- if action == 'UPDATE'
79
- raise ArgumentError, <<-MSG.strip_heredoc
73
+ def action_sql(action, dependency)
74
+ if action == "UPDATE"
75
+ raise ArgumentError, <<-MSG.strip_heredoc
80
76
  '#{action}' is not supported by Oracle
81
77
  MSG
82
- end
83
- case dependency
84
- when :nullify then "ON #{action} SET NULL"
85
- when :cascade then "ON #{action} CASCADE"
86
- else
87
- raise ArgumentError, <<-MSG.strip_heredoc
78
+ end
79
+ case dependency
80
+ when :nullify then "ON #{action} SET NULL"
81
+ when :cascade then "ON #{action} CASCADE"
82
+ else
83
+ raise ArgumentError, <<-MSG.strip_heredoc
88
84
  '#{dependency}' is not supported for #{action}
89
85
  Supported values are: :nullify, :cascade
90
86
  MSG
87
+ end
91
88
  end
92
- end
93
89
  end
94
90
  end
95
91
  end