unclebilly-activerecord-oracle_enhanced-adapter 1.2.4

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 (38) hide show
  1. data/History.txt +165 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +32 -0
  4. data/README.rdoc +75 -0
  5. data/Rakefile +49 -0
  6. data/VERSION +1 -0
  7. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
  8. data/lib/active_record/connection_adapters/oracle_enhanced.rake +51 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1723 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +121 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +64 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +39 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +369 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +396 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +164 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb +126 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +177 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +214 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +224 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +11 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced_virtual_column.rb +35 -0
  24. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +610 -0
  25. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_structure_dumper_spec.rb +266 -0
  26. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +206 -0
  27. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +40 -0
  28. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +107 -0
  29. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +984 -0
  30. data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +67 -0
  31. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +93 -0
  32. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +25 -0
  33. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +370 -0
  34. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +268 -0
  35. data/spec/active_record/connection_adapters/oracle_enhanced_schema_spec.rb +761 -0
  36. data/spec/spec.opts +6 -0
  37. data/spec/spec_helper.rb +130 -0
  38. metadata +149 -0
@@ -0,0 +1,126 @@
1
+ module ActiveRecord #:nodoc:
2
+ module ConnectionAdapters #:nodoc:
3
+ module OracleEnhancedReservedWords #:nodoc:
4
+
5
+ RESERVED_WORDS = {
6
+ "ACCESS" => true,
7
+ "ADD" => true,
8
+ "ALL" => true,
9
+ "ALTER" => true,
10
+ "AND" => true,
11
+ "ANY" => true,
12
+ "AS" => true,
13
+ "ASC" => true,
14
+ "AUDIT" => true,
15
+ "BETWEEN" => true,
16
+ "BY" => true,
17
+ "CHAR" => true,
18
+ "CHECK" => true,
19
+ "CLUSTER" => true,
20
+ "COLUMN" => true,
21
+ "COMMENT" => true,
22
+ "COMPRESS" => true,
23
+ "CONNECT" => true,
24
+ "CREATE" => true,
25
+ "CURRENT" => true,
26
+ "DATE" => true,
27
+ "DECIMAL" => true,
28
+ "DEFAULT" => true,
29
+ "DELETE" => true,
30
+ "DESC" => true,
31
+ "DISTINCT" => true,
32
+ "DROP" => true,
33
+ "ELSE" => true,
34
+ "EXCLUSIVE" => true,
35
+ "EXISTS" => true,
36
+ "FILE" => true,
37
+ "FLOAT" => true,
38
+ "FOR" => true,
39
+ "FROM" => true,
40
+ "GRANT" => true,
41
+ "GROUP" => true,
42
+ "HAVING" => true,
43
+ "IDENTIFIED" => true,
44
+ "IMMEDIATE" => true,
45
+ "IN" => true,
46
+ "INCREMENT" => true,
47
+ "INDEX" => true,
48
+ "INITIAL" => true,
49
+ "INSERT" => true,
50
+ "INTEGER" => true,
51
+ "INTERSECT" => true,
52
+ "INTO" => true,
53
+ "IS" => true,
54
+ "LEVEL" => true,
55
+ "LIKE" => true,
56
+ "LOCK" => true,
57
+ "LONG" => true,
58
+ "MAXEXTENTS" => true,
59
+ "MINUS" => true,
60
+ "MLSLABEL" => true,
61
+ "MODE" => true,
62
+ "MODIFY" => true,
63
+ "NOAUDIT" => true,
64
+ "NOCOMPRESS" => true,
65
+ "NOT" => true,
66
+ "NOWAIT" => true,
67
+ "NULL" => true,
68
+ "NUMBER" => true,
69
+ "OF" => true,
70
+ "OFFLINE" => true,
71
+ "ON" => true,
72
+ "ONLINE" => true,
73
+ "OPTION" => true,
74
+ "OR" => true,
75
+ "ORDER" => true,
76
+ "PCTFREE" => true,
77
+ "PRIOR" => true,
78
+ "PRIVILEGES" => true,
79
+ "PUBLIC" => true,
80
+ "RAW" => true,
81
+ "RENAME" => true,
82
+ "RESOURCE" => true,
83
+ "REVOKE" => true,
84
+ "ROW" => true,
85
+ "ROWID" => true,
86
+ "ROWNUM" => true,
87
+ "ROWS" => true,
88
+ "SELECT" => true,
89
+ "SESSION" => true,
90
+ "SET" => true,
91
+ "SHARE" => true,
92
+ "SIZE" => true,
93
+ "SMALLINT" => true,
94
+ "START" => true,
95
+ "SUCCESSFUL" => true,
96
+ "SYNONYM" => true,
97
+ "SYSDATE" => true,
98
+ "TABLE" => true,
99
+ "THEN" => true,
100
+ "TO" => true,
101
+ "TRIGGER" => true,
102
+ "UID" => true,
103
+ "UNION" => true,
104
+ "UNIQUE" => true,
105
+ "UPDATE" => true,
106
+ "USER" => true,
107
+ "VALIDATE" => true,
108
+ "VALUES" => true,
109
+ "VARCHAR" => true,
110
+ "VARCHAR2" => true,
111
+ "VIEW" => true,
112
+ "WHENEVER" => true,
113
+ "WHERE" => true,
114
+ "WITH" => true
115
+ }
116
+
117
+ def quote_oracle_reserved_words(name)
118
+ RESERVED_WORDS[name.to_s.upcase].nil? ? name : "\"#{name}\""
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
125
+ include ActiveRecord::ConnectionAdapters::OracleEnhancedReservedWords
126
+ end
@@ -0,0 +1,177 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class OracleEnhancedForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
4
+ end
5
+
6
+ class OracleEnhancedSynonymDefinition < Struct.new(:name, :table_owner, :table_name, :db_link) #:nodoc:
7
+ end
8
+
9
+ class OracleEnhancedIndexDefinition < Struct.new(:table, :name, :unique, :tablespace, :columns)
10
+ end
11
+
12
+ class OracleEnhancedColumnDefinition < ColumnDefinition
13
+ def to_sql
14
+ return super unless type==:virtual
15
+ #virtual column syntax
16
+ "#{base.quote_column_name(name)} AS (#{default})"
17
+ end
18
+ alias to_s :to_sql
19
+ end
20
+
21
+ module OracleEnhancedSchemaDefinitions #:nodoc:
22
+ def self.included(base)
23
+ base::TableDefinition.class_eval do
24
+ include OracleEnhancedTableDefinition
25
+ end
26
+
27
+ # Available starting from ActiveRecord 2.1
28
+ base::Table.class_eval do
29
+ include OracleEnhancedTable
30
+ end if defined?(base::Table)
31
+ end
32
+ end
33
+
34
+ module OracleEnhancedTableDefinition
35
+ class ForeignKey < Struct.new(:base, :to_table, :options) #:nodoc:
36
+ def to_sql
37
+ base.foreign_key_definition(to_table, options)
38
+ end
39
+ alias to_s :to_sql
40
+ end
41
+
42
+ def self.included(base) #:nodoc:
43
+ base.class_eval do
44
+ alias_method_chain :references, :foreign_keys
45
+ alias_method_chain :to_sql, :foreign_keys
46
+ end
47
+ end
48
+
49
+ # Adds a :foreign_key option to TableDefinition.references.
50
+ # If :foreign_key is true, a foreign key constraint is added to the table.
51
+ # You can also specify a hash, which is passed as foreign key options.
52
+ #
53
+ # ===== Examples
54
+ # ====== Add goat_id column and a foreign key to the goats table.
55
+ # t.references(:goat, :foreign_key => true)
56
+ # ====== Add goat_id column and a cascading foreign key to the goats table.
57
+ # t.references(:goat, :foreign_key => {:dependent => :delete})
58
+ #
59
+ # Note: No foreign key is created if :polymorphic => true is used.
60
+ # Note: If no name is specified, the database driver creates one for you!
61
+ def references_with_foreign_keys(*args)
62
+ options = args.extract_options!
63
+ fk_options = options.delete(:foreign_key)
64
+
65
+ if fk_options && !options[:polymorphic]
66
+ fk_options = {} if fk_options == true
67
+ args.each { |to_table| foreign_key(to_table, fk_options) }
68
+ end
69
+
70
+ references_without_foreign_keys(*(args << options))
71
+ end
72
+
73
+ # Defines a foreign key for the table. +to_table+ can be a single Symbol, or
74
+ # an Array of Symbols. See SchemaStatements#add_foreign_key
75
+ #
76
+ # ===== Examples
77
+ # ====== Creating a simple foreign key
78
+ # t.foreign_key(:people)
79
+ # ====== Defining the column
80
+ # t.foreign_key(:people, :column => :sender_id)
81
+ # ====== Creating a named foreign key
82
+ # t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key')
83
+ # ====== Defining the column of the +to_table+.
84
+ # t.foreign_key(:people, :column => :sender_id, :primary_key => :person_id)
85
+ def foreign_key(to_table, options = {})
86
+ if @base.respond_to?(:supports_foreign_keys?) && @base.supports_foreign_keys?
87
+ to_table = to_table.to_s.pluralize if ActiveRecord::Base.pluralize_table_names
88
+ foreign_keys << ForeignKey.new(@base, to_table, options)
89
+ else
90
+ raise ArgumentError, "this ActiveRecord adapter is not supporting foreign_key definition"
91
+ end
92
+ end
93
+
94
+ def to_sql_with_foreign_keys #:nodoc:
95
+ sql = to_sql_without_foreign_keys
96
+ sql << ', ' << (foreign_keys * ', ') unless foreign_keys.blank?
97
+ sql
98
+ end
99
+
100
+ private
101
+ def foreign_keys
102
+ @foreign_keys ||= []
103
+ end
104
+ end
105
+
106
+ module OracleEnhancedTable
107
+ def self.included(base) #:nodoc:
108
+ base.class_eval do
109
+ alias_method_chain :references, :foreign_keys
110
+ end
111
+ end
112
+
113
+ # Adds a new foreign key to the table. +to_table+ can be a single Symbol, or
114
+ # an Array of Symbols. See SchemaStatements#add_foreign_key
115
+ #
116
+ # ===== Examples
117
+ # ====== Creating a simple foreign key
118
+ # t.foreign_key(:people)
119
+ # ====== Defining the column
120
+ # t.foreign_key(:people, :column => :sender_id)
121
+ # ====== Creating a named foreign key
122
+ # t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key')
123
+ # ====== Defining the column of the +to_table+.
124
+ # t.foreign_key(:people, :column => :sender_id, :primary_key => :person_id)
125
+ def foreign_key(to_table, options = {})
126
+ if @base.respond_to?(:supports_foreign_keys?) && @base.supports_foreign_keys?
127
+ to_table = to_table.to_s.pluralize if ActiveRecord::Base.pluralize_table_names
128
+ @base.add_foreign_key(@table_name, to_table, options)
129
+ else
130
+ raise ArgumentError, "this ActiveRecord adapter is not supporting foreign_key definition"
131
+ end
132
+ end
133
+
134
+ # Remove the given foreign key from the table.
135
+ #
136
+ # ===== Examples
137
+ # ====== Remove the suppliers_company_id_fk in the suppliers table.
138
+ # t.remove_foreign_key :companies
139
+ # ====== Remove the foreign key named accounts_branch_id_fk in the accounts table.
140
+ # remove_foreign_key :column => :branch_id
141
+ # ====== Remove the foreign key named party_foreign_key in the accounts table.
142
+ # remove_index :name => :party_foreign_key
143
+ def remove_foreign_key(options = {})
144
+ @base.remove_foreign_key(@table_name, options)
145
+ end
146
+
147
+ # Adds a :foreign_key option to TableDefinition.references.
148
+ # If :foreign_key is true, a foreign key constraint is added to the table.
149
+ # You can also specify a hash, which is passed as foreign key options.
150
+ #
151
+ # ===== Examples
152
+ # ====== Add goat_id column and a foreign key to the goats table.
153
+ # t.references(:goat, :foreign_key => true)
154
+ # ====== Add goat_id column and a cascading foreign key to the goats table.
155
+ # t.references(:goat, :foreign_key => {:dependent => :delete})
156
+ #
157
+ # Note: No foreign key is created if :polymorphic => true is used.
158
+ def references_with_foreign_keys(*args)
159
+ options = args.extract_options!
160
+ polymorphic = options[:polymorphic]
161
+ fk_options = options.delete(:foreign_key)
162
+
163
+ references_without_foreign_keys(*(args << options))
164
+ # references_without_foreign_keys adds {:type => :integer}
165
+ args.extract_options!
166
+ if fk_options && !polymorphic
167
+ fk_options = {} if fk_options == true
168
+ args.each { |to_table| foreign_key(to_table, fk_options) }
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ ActiveRecord::ConnectionAdapters.class_eval do
176
+ include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaDefinitions
177
+ end
@@ -0,0 +1,214 @@
1
+ module ActiveRecord #:nodoc:
2
+ module ConnectionAdapters #:nodoc:
3
+ module OracleEnhancedSchemaDumper #:nodoc:
4
+
5
+ def self.included(base) #:nodoc:
6
+ base.class_eval do
7
+ private
8
+ alias_method_chain :tables, :oracle_enhanced
9
+ alias_method_chain :indexes, :oracle_enhanced
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def tables_with_oracle_enhanced(stream)
16
+ sorted_tables = @connection.tables.sort
17
+ sorted_tables.each do |tbl|
18
+ # add table prefix or suffix for schema_migrations
19
+ next if [ActiveRecord::Migrator.proper_table_name('schema_migrations'), ignore_tables].flatten.any? do |ignored|
20
+ case ignored
21
+ when String; tbl == ignored
22
+ when Regexp; tbl =~ ignored
23
+ else
24
+ raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
25
+ end
26
+ end
27
+ # change table name inspect method
28
+ tbl.extend TableInspect
29
+ oracle_enhanced_table(tbl, stream)
30
+ # add primary key trigger if table has it
31
+ primary_key_trigger(tbl, stream)
32
+ end
33
+ sorted_tables.each do |tbl|
34
+ # add foreign keys if table has them
35
+ foreign_keys(tbl, stream)
36
+ end
37
+ # add synonyms in local schema
38
+ synonyms(stream)
39
+ end
40
+
41
+ def primary_key_trigger(table_name, stream)
42
+ if @connection.respond_to?(:has_primary_key_trigger?) && @connection.has_primary_key_trigger?(table_name)
43
+ pk, pk_seq = @connection.pk_and_sequence_for(table_name)
44
+ stream.print " add_primary_key_trigger #{table_name.inspect}"
45
+ stream.print ", :primary_key => \"#{pk}\"" if pk != 'id'
46
+ stream.print "\n\n"
47
+ end
48
+ end
49
+
50
+ def foreign_keys(table_name, stream)
51
+ if @connection.respond_to?(:foreign_keys) && (foreign_keys = @connection.foreign_keys(table_name)).any?
52
+ add_foreign_key_statements = foreign_keys.map do |foreign_key|
53
+ statement_parts = [ ('add_foreign_key ' + foreign_key.from_table.inspect) ]
54
+ statement_parts << foreign_key.to_table.inspect
55
+ statement_parts << (':name => ' + foreign_key.options[:name].inspect)
56
+
57
+ if foreign_key.options[:column] != "#{foreign_key.to_table.singularize}_id"
58
+ statement_parts << (':column => ' + foreign_key.options[:column].inspect)
59
+ end
60
+ if foreign_key.options[:primary_key] != 'id'
61
+ statement_parts << (':primary_key => ' + foreign_key.options[:primary_key].inspect)
62
+ end
63
+ unless foreign_key.options[:dependent].blank?
64
+ statement_parts << (':dependent => ' + foreign_key.options[:dependent].inspect)
65
+ end
66
+
67
+ ' ' + statement_parts.join(', ')
68
+ end
69
+
70
+ stream.puts add_foreign_key_statements.sort.join("\n")
71
+ stream.puts
72
+ end
73
+ end
74
+
75
+ def synonyms(stream)
76
+ if @connection.respond_to?(:synonyms)
77
+ syns = @connection.synonyms
78
+ syns.each do |syn|
79
+ table_name = syn.table_name
80
+ table_name = "#{syn.table_owner}.#{table_name}" if syn.table_owner
81
+ table_name = "#{table_name}@#{syn.db_link}" if syn.db_link
82
+ stream.print " add_synonym #{syn.name.inspect}, #{table_name.inspect}, :force => true"
83
+ stream.puts
84
+ end
85
+ stream.puts unless syns.empty?
86
+ end
87
+ end
88
+
89
+ def indexes_with_oracle_enhanced(table, stream)
90
+ if (indexes = @connection.indexes(table)).any?
91
+ add_index_statements = indexes.map do |index|
92
+ # use table.inspect as it will remove prefix and suffix
93
+ statment_parts = [ ('add_index ' + table.inspect) ]
94
+ statment_parts << index.columns.inspect
95
+ statment_parts << (':name => ' + index.name.inspect)
96
+ statment_parts << ':unique => true' if index.unique
97
+ statment_parts << ':tablespace => ' + index.tablespace.inspect
98
+
99
+ ' ' + statment_parts.join(', ')
100
+ end
101
+
102
+ stream.puts add_index_statements.sort.join("\n")
103
+ stream.puts
104
+ end
105
+ end
106
+
107
+ def oracle_enhanced_table(table, stream)
108
+ columns = @connection.columns(table)
109
+ begin
110
+ tbl = StringIO.new
111
+
112
+ # first dump primary key column
113
+ if @connection.respond_to?(:pk_and_sequence_for)
114
+ pk, pk_seq = @connection.pk_and_sequence_for(table)
115
+ elsif @connection.respond_to?(:primary_key)
116
+ pk = @connection.primary_key(table)
117
+ end
118
+
119
+ tbl.print " create_table #{table.inspect}"
120
+
121
+ # addition to make temporary option work
122
+ tbl.print ", :temporary => true" if @connection.temporary?(table)
123
+
124
+ if columns.detect { |c| c.name == pk }
125
+ if pk != 'id'
126
+ tbl.print %Q(, :primary_key => "#{pk}")
127
+ end
128
+ else
129
+ tbl.print ", :id => false"
130
+ end
131
+ tbl.print ", :force => true"
132
+ tbl.puts " do |t|"
133
+
134
+ # then dump all non-primary key columns
135
+ column_specs = columns.map do |column|
136
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
137
+ next if column.name == pk
138
+ spec = {}
139
+ spec[:name] = column.name.inspect
140
+ spec[:type] = column.virtual? ? 'virtual' : column.type.to_s
141
+ spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal
142
+ spec[:precision] = column.precision.inspect if !column.precision.nil?
143
+ spec[:scale] = column.scale.inspect if !column.scale.nil?
144
+ spec[:null] = 'false' if !column.null
145
+ spec[:default] = column.default.inspect if column.virtual?
146
+ spec[:default] ||= default_string(column.default) if column.has_default?
147
+ (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
148
+ spec
149
+ end.compact
150
+
151
+ # find all migration keys used in this table
152
+ keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map(&:keys).flatten
153
+
154
+ # figure out the lengths for each column based on above keys
155
+ lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
156
+
157
+ # the string we're going to sprintf our values against, with standardized column widths
158
+ format_string = lengths.map{ |len| "%-#{len}s" }
159
+
160
+ # find the max length for the 'type' column, which is special
161
+ type_length = column_specs.map{ |column| column[:type].length }.max
162
+
163
+ # add column type definition to our format string
164
+ format_string.unshift " t.%-#{type_length}s "
165
+
166
+ format_string *= ''
167
+
168
+ column_specs.each do |colspec|
169
+ values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
170
+ values.unshift colspec[:type]
171
+ tbl.print((format_string % values).gsub(/,\s*$/, ''))
172
+ tbl.puts
173
+ end
174
+
175
+ tbl.puts " end"
176
+ tbl.puts
177
+
178
+ indexes(table, tbl)
179
+
180
+ tbl.rewind
181
+ stream.print tbl.read
182
+ rescue => e
183
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
184
+ stream.puts "# #{e.message}"
185
+ stream.puts
186
+ end
187
+
188
+ stream
189
+ end
190
+
191
+
192
+ # remove table name prefix and suffix when doing #inspect (which is used in tables method)
193
+ module TableInspect #:nodoc:
194
+ def inspect
195
+ remove_prefix_and_suffix(self)
196
+ end
197
+
198
+ private
199
+ def remove_prefix_and_suffix(table_name)
200
+ if table_name =~ /\A#{ActiveRecord::Base.table_name_prefix}(.*)#{ActiveRecord::Base.table_name_suffix}\Z/
201
+ "\"#{$1}\""
202
+ else
203
+ "\"#{table_name}\""
204
+ end
205
+ end
206
+ end
207
+
208
+ end
209
+ end
210
+ end
211
+
212
+ ActiveRecord::SchemaDumper.class_eval do
213
+ include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaDumper
214
+ end