activerecord-advantage-adapter 0.1.1 → 0.1.2

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4f45753b6d821481bef646fe79203244d7b69c726b9fe82229e8b312b946c65b
4
+ data.tar.gz: 737097d811c4f196d6a8a4136d17d9b218f6a7294ebf550f78cd22d88ad6edfa
5
+ SHA512:
6
+ metadata.gz: 01f7b781263c979760673854136779bf9a118ab37490ec1b1ed4d77a4e6f98aa0fdc9c78f5cb09cc87eb2a13acbc1f25f2479639f987c73a16ac120a7b03b430
7
+ data.tar.gz: 054a2d014d38699df8f6b5baa585e8c787632e3b53120ffe2ef98fc3816d8bbe368750eff8c4d461c79dee302db7f2647a293797bc2cd2ea60039353a3954e9a
@@ -0,0 +1,49 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ pkg_version = "0.1.2"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "activerecord-advantage-adapter"
8
+ spec.version = pkg_version
9
+ spec.authors = ["Edgar Sherman", "Jon Adams"]
10
+ spec.email = ["advantage@sybase.com", "t12nslookup@googlemail.com"]
11
+
12
+ spec.summary = %q{ActiveRecord driver for Advantage}
13
+ spec.description = %q{ActiveRecord driver for the Advantage Database connector}
14
+ spec.homepage = "http://devzone.advantagedatabase.com"
15
+ spec.license = "Apache-2.0"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
21
+
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ # Changed to the github project, as this is the actively maintained source, now.
24
+ spec.metadata["source_code_uri"] = "https://github.com/t12nslookup/activerecord-advantage-adapter/"
25
+ spec.metadata["changelog_uri"] = "https://github.com/t12nslookup/activerecord-advantage-adapter/blob/master/CHANGELOG.md"
26
+ else
27
+ raise "RubyGems 2.0 or newer is required to protect against " \
28
+ "public gem pushes."
29
+ end
30
+
31
+ # Specify which files should be added to the gem when it is released.
32
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
33
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
34
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
35
+ end
36
+ spec.files = Dir["{test,lib}/**/*",
37
+ "LICENSE",
38
+ "README",
39
+ "activerecord-advantage-adapter.gemspec"]
40
+ spec.require_paths = ["lib"]
41
+
42
+ spec.add_development_dependency "bundler", "~> 1.17"
43
+ spec.add_development_dependency "rake", "~> 10.0"
44
+ spec.add_development_dependency "rspec", "~> 3.0"
45
+
46
+ spec.add_runtime_dependency "advantage", "~> 0.1", ">= 0.1.2"
47
+ # spec.add_runtime_dependency 'activerecord', '>= 3.2.0'
48
+
49
+ end
@@ -1,532 +1,558 @@
1
- #====================================================
2
- #
3
- # Copyright 2008-2010 iAnywhere Solutions, Inc.
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- #
10
- # http://www.apache.org/licenses/LICENSE-2.0
11
- #
12
- # Unless required by applicable law or agreed to in writing, software
13
- # distributed under the License is distributed on an "AS IS" BASIS,
14
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- #
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
- #
19
- #
20
- #
21
- #====================================================
22
-
23
- require 'active_record/connection_adapters/abstract_adapter'
24
- require 'arel/visitors/advantage.rb'
25
-
26
- # Singleton class to hold a valid instance of the AdvantageInterface across all connections
27
- class ADS
28
- include Singleton
29
- attr_accessor :api
30
-
31
- def initialize
32
- require 'advantage' unless defined? Advantage
33
- @api = Advantage::AdvantageInterface.new()
34
- raise LoadError, "Could not load ACE library" if Advantage::API.ads_initialize_interface(@api) == 0
35
- raise LoadError, "Could not initialize ACE library" if @api.ads_init() == 0
36
- end
37
- end
38
-
39
- module ActiveRecord
40
- class Base
41
- DEFAULT_CONFIG = { :username => 'adssys', :password => nil }
42
- # Main connection function to Advantage
43
- # Connection Adapter takes four parameters:
44
- # * :database (required, no default). Corresponds to "Data Source=" in connection string
45
- # * :username (optional, default to 'adssys'). Correspons to "User ID=" in connection string
46
- # * :password (optional, deafult to '')
47
- # * :options (optional, defaults to ''). Corresponds to any additional options in connection string
48
-
49
- def self.advantage_connection(config)
50
-
51
- config = DEFAULT_CONFIG.merge(config)
52
-
53
- raise ArgumentError, "No data source was given. Please add a :database option." unless config.has_key?(:database)
54
-
55
- connection_string = "data source=#{config[:database]};User ID=#{config[:username]};"
56
- connection_string += "Password=#{config[:password]};" unless config[:options].nil?
57
- connection_string += "#{config[:options]};" unless config[:options].nil?
58
- connection_string += "DateFormat=YYYY-MM-DD;"
59
-
60
- db = ADS.instance.api.ads_new_connection()
61
-
62
- ConnectionAdapters::AdvantageAdapter.new(db, logger, connection_string)
63
- end
64
- end
65
-
66
- module ConnectionAdapters
67
- class AdvantageException < StandardError
68
- attr_reader :errno
69
- attr_reader :sql
70
-
71
- def initialize(message, errno, sql)
72
- super(message)
73
- @errno = errno
74
- @sql = sql
75
- end
76
- end
77
-
78
- class AdvantageColumn < Column
79
- private
80
- # Overridden to handle Advantage integer, varchar, binary, and timestamp types
81
- def simplified_type(field_type)
82
- return :boolean if field_type =~ /logical/i
83
- return :string if field_type =~ /varchar/i
84
- return :binary if field_type =~ /long binary/i
85
- return :datetime if field_type =~ /timestamp/i
86
- return :integer if field_type =~ /short|integer/i
87
- return :integer if field_type =~ /autoinc/i
88
- super
89
- end
90
-
91
- #EJS Need?
92
- =begin
93
- def extract_limit(sql_type)
94
- case sql_type
95
- when /^tinyint/i
96
- 1
97
- when /^smallint/i
98
- 2
99
- when /^integer/i
100
- 4
101
- when /^bigint/i
102
- 8
103
- else super
104
- end
105
- end
106
- =end
107
-
108
- protected
109
- end
110
-
111
- class AdvantageAdapter < AbstractAdapter
112
- def initialize( connection, logger, connection_string = "") #:nodoc:
113
- super(connection, logger)
114
- @auto_commit = true
115
- @affected_rows = 0
116
- @connection_string = connection_string
117
- @visitor = Arel::Visitors::Advantage.new self
118
- connect!
119
- end
120
-
121
- def adapter_name #:nodoc:
122
- 'Advantage'
123
- end
124
-
125
- def supports_migrations? #:nodoc:
126
- true
127
- end
128
-
129
- def requires_reloading? #:nodoc:
130
- true
131
- end
132
-
133
- def active? #:nodoc:
134
- ADS.instance.api.ads_execute_immediate(@connection, "SELECT 1 FROM SYSTEM.IOTA") == 1
135
- rescue
136
- false
137
- end
138
-
139
- def disconnect! #:nodoc:
140
- result = ADS.instance.api.ads_disconnect( @connection )
141
- super
142
- end
143
-
144
- def reconnect! #:nodoc:
145
- disconnect!
146
- connect!
147
- end
148
-
149
- def supports_count_distinct? #:nodoc:
150
- true
151
- end
152
-
153
- def supports_autoincrement? #:nodoc:
154
- true
155
- end
156
-
157
- # Used from StackOverflow question 1000688
158
- # Stip alone will return NIL if the string is not altered. In that case,
159
- # still return the string.
160
- def strip_or_self(str) #:nodoc:
161
- str.strip! || str if str
162
- end
163
-
164
- # Maps native ActiveRecord/Ruby types into ADS types
165
- def native_database_types #:nodoc:
166
- {
167
- :primary_key => 'AUTOINC PRIMARY KEY CONSTRAINT NOT NULL',
168
- :string => { :name => "varchar", :limit => 255 },
169
- :text => { :name => "memo" },
170
- :integer => { :name => "integer" },
171
- :float => { :name => "float" },
172
- :decimal => { :name => "numeric" },
173
- :datetime => { :name => "timestamp" },
174
- :timestamp => { :name => "timestamp" },
175
- :time => { :name => "time" },
176
- :date => { :name => "date" },
177
- :binary => { :name => "blob" },
178
- :boolean => { :name => "logical"}
179
- }
180
- end
181
-
182
- # Applies quotations around column names in generated queries
183
- def quote_column_name(name) #:nodoc:
184
- %Q("#{name}")
185
- end
186
-
187
- def quote(value, column = nil) #:nodoc:
188
- super(value, column)
189
- end
190
-
191
- def quoted_true #:nodoc:
192
- '1'
193
- end
194
-
195
- def quoted_false #:nodoc:
196
- '0'
197
- end
198
-
199
- # The database execution function
200
- def execute(sql, name = nil, binds = []) #:nodoc:
201
- if name == :skip_logging
202
- query(sql, binds)
203
- else
204
- log(sql, name, binds) { query(sql, binds) }
205
- end
206
- end
207
-
208
- # Translate the exception if possible
209
- def translate_exception(exception, message) #:nodoc:
210
- return super unless exception.respond_to?(:errno)
211
- case exception.errno
212
- when 2121
213
- if exception.sql !~ /^SELECT/i then
214
- raise ActiveRecord::ActiveRecordError.new(message)
215
- else
216
- super
217
- end
218
- when 7076
219
- raise InvalidForeignKey.new(message, exception)
220
- when 7057
221
- raise RecordNotUnique.new(message, exception)
222
- else
223
- super
224
- end
225
- super
226
- end
227
-
228
- # The database update function.
229
- def update_sql(sql, name = nil) #:nodoc:
230
- execute( sql, name )
231
- return @affected_rows
232
- end
233
-
234
- # The database delete function.
235
- def delete_sql(sql, name = nil) #:nodoc:
236
- execute( sql, name )
237
- return @affected_rows
238
- end
239
-
240
- # The database insert function.
241
- # ActiveRecord requires that insert_sql returns the primary key of the row just inserted. In most cases, this can be accomplished
242
- # by immediatly querying the @@identity property. If the @@identity property is 0, then passed id_value is used
243
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
244
- execute(sql, name)
245
- identity = last_inserted_id(nil)
246
- retval = id_value if retval == 0
247
- return retval
248
- end
249
-
250
- # The Database insert function as part of the rails changes
251
- def exec_insert(sql, name = nil, binds = []) #:nodoc:
252
- log(sql, "insert", binds) { query(sql, binds) }
253
- end
254
-
255
- # The Database update function as part of the rails changes
256
- def exec_update(sql, name = nil, binds = []) #:nodoc:
257
- log(sql, "update", binds) { query(sql, binds) }
258
- end
259
-
260
- # The Database delete function as part of the rails changes
261
- def exec_delete(sql, name = nil, binds = []) #:nodoc:
262
- log(sql, "delete", binds) { query(sql, binds) }
263
- end
264
-
265
- # Retrieve the last AutoInc ID
266
- def last_inserted_id(result) #:nodoc:
267
- rs = ADS.instance.api.ads_execute_direct(@connection, 'SELECT LASTAUTOINC( CONNECTION ) FROM SYSTEM.IOTA')
268
- raise ActiveRecord::StatementInvalid.new("#{ADS.instance.api.ads_error(@connection)}:#{sql}") if rs.nil?
269
- ADS.instance.api.ads_fetch_next(rs)
270
- retval, identity = ADS.instance.api.ads_get_column(rs, 0)
271
- ADS.instance.api.ads_free_stmt(rs)
272
- identity
273
- end
274
-
275
- # Returns a query as an array of arrays
276
- def select_rows(sql, name = nil)
277
- rs = ADS.instance.api.ads_execute_direct(@connection, sql)
278
- raise ActiveRecord::StatementInvalid.new("#{ADS.instance.api.ads_error(@connection)}:#{sql}") if rs.nil?
279
- record = []
280
- while ADS.instance.api.ads_fetch_next(rs) == 1
281
- max_cols = ADS.instance.api.ads_num_cols(rs)
282
- result = Array.new(max_cols)
283
- max_cols.times do |cols|
284
- result[cols] = ADS.instance.api.ads_get_column(rs, cols)[1]
285
- end
286
- record << result
287
- end
288
- ADS.instance.api.ads_free_stmt(rs)
289
- return record
290
- end
291
-
292
- # Begin a transaction
293
- def begin_db_transaction #:nodoc:
294
- ADS.instance.api.AdsBeginTransaction(@connection)
295
- @auto_commit = false;
296
- end
297
-
298
- # Commit the transaction
299
- def commit_db_transaction #:nodoc:
300
- ADS.instance.api.ads_commit(@connection)
301
- @auto_commit = true;
302
- end
303
-
304
- # Rollback the transaction
305
- def rollback_db_transaction #:nodoc:
306
- ADS.instance.api.ads_rollback(@connection)
307
- @auto_commit = true;
308
- end
309
-
310
- def add_lock!(sql, options) #:nodoc:
311
- sql
312
- end
313
-
314
- # Advantage does not support sizing of integers based on the sytax INTEGER(size).
315
- def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
316
- if native = native_database_types[type]
317
- if type == :integer
318
- column_type_sql = 'integer'
319
- elsif type == :string and !limit.nil?
320
- "varchar (#{limit})"
321
- else
322
- super(type, limit, precision, scale)
323
- end
324
- else
325
- super(type, limit, precision, scale)
326
- end
327
- end
328
-
329
- # Retrieve a list of Tables
330
- def tables(name = nil) #:nodoc:
331
- sql = "EXECUTE PROCEDURE sp_GetTables( NULL, NULL, NULL, 'TABLE' );"
332
- select(sql, name).map { |row| strip_or_self(row["TABLE_NAME"]) }
333
- end
334
-
335
- # Return a list of columns
336
- def columns(table_name, name = nil) #:nodoc:
337
- table_structure(table_name).map do |field|
338
- AdvantageColumn.new(strip_or_self(field['COLUMN_NAME']), field['COLUMN_DEF'], strip_or_self(field['TYPE_NAME']), field['NULLABLE'])
339
- end
340
- end
341
-
342
- # Return a list of indexes
343
- # EJS - Is there a way to get info without DD?
344
- def indexes(table_name, name = nil) #:nodoc:
345
- sql = "SELECT name, INDEX_OPTIONS & 1 AS [unique], index_expression FROM SYSTEM.INDEXES WHERE parent = '#{table_name}'"
346
- select(sql, name).map do |row|
347
- index = IndexDefinition.new(table_name, row['name'])
348
- index.unique = row['unique'] == 1
349
- index.columns = row['index_expression']
350
- index
351
- end
352
- end
353
-
354
- # Return the primary key
355
- def primary_key(table_name) #:nodoc:
356
- sql = "SELECT COLUMN_NAME FROM (EXECUTE PROCEDURE sp_GetBestRowIdentifier( NULL, NULL, '#{table_name}', NULL, FALSE)) as gbri"
357
- rs = select(sql)
358
- if !rs.nil? and !rs[0].nil?
359
- strip_or_self(rs[0]['COLUMN_NAME'])
360
- else
361
- nil
362
- end
363
- end
364
-
365
- # Drop an index
366
- def remove_index(table_name, options={}) #:nodoc:
367
- execute "DROP INDEX #{quote_table_name(table_name)}.#{quote_column_name(index_name(table_name, options))}"
368
- end
369
-
370
- # Rename a table
371
- #EJS - can be done without dd?
372
- def rename_table(name, new_name) #:nodoc:
373
- execute "EXECUTE PROCEDURE sp_RenameDDObject(#{quote_table_name(name)} , #{quote_table_name(new_name)}, 1 /* ADS_DD_TABLE_OBJECT */, 0 /* Rename File */)"
374
- end
375
-
376
- # Helper function to retrieve the columns current type
377
- def get_column_type(table_name, column_name) #:nodoc:
378
- sql = <<-SQL
379
- SELECT
380
- CASE
381
- WHEN type_name = 'VARCHAR' or type_name = 'CHAR' or type_name = 'CICHAR' or
382
- type_name = 'NVARCHAR' or type_name = 'NCHAR' or type_name = 'VARBINARY'
383
- THEN CAST(TRIM(type_name) + '(' + TRIM(CAST(column_size AS SQL_CHAR)) + ')' AS SQL_CHAR)
384
- WHEN type_name = 'NUMERIC' or type_name = 'DOUBLE' or type_name = 'CURDOUBLE'
385
- THEN CAST(TRIM(type_name) + '(' + TRIM(CAST(column_size AS SQL_CHAR)) + ',' + TRIM(CAST(decimal_digits AS SQL_CHAR)) + ')' AS SQL_CHAR)
386
- ELSE
387
- TRIM(type_name COLLATE ads_default_cs)
388
- END AS "domain"
389
- from (EXECUTE PROCEDURE sp_GetColumns( NULL, NULL, '#{table_name}', NULL)) as spgc
390
- WHERE COLUMN_NAME = '#{column_name}'
391
- SQL
392
- rs = select(sql)
393
- if !rs.nil? and !rs[0].nil?
394
- rs[0]['domain']
395
- end
396
- end
397
-
398
- # Change a columns defaults.
399
- def change_column_default(table_name, column_name, default) #:nodoc:
400
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{get_column_type(table_name, column_name)} DEFAULT #{quote(default)}"
401
- end
402
-
403
- # Change a columns nullability
404
- def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
405
- unless null || default.nil?
406
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
407
- end
408
- execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{get_column_type(table_name, column_name)} CONSTRAINT #{null ? '' : 'NOT'} NULL")
409
- end
410
-
411
- # Alter a column
412
- def change_column(table_name, column_name, type, options = {}) #:nodoc:
413
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, type_options[:limit], type_options[:precision], type_options[:scale])}"
414
- add_column_options!(add_column_sql, options)
415
- execute(add_column_sql)
416
- end
417
-
418
- # Add column options
419
- def add_column_options!(sql, options) #:nodoc:
420
- sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
421
- # must explicitly check for :null to allow change_column to work on migrations
422
- if options[:null] == false
423
- sql << " CONSTRAINT NOT NULL"
424
- end
425
- end
426
-
427
- # Rename a column
428
- def rename_column(table_name, column_name, new_column_name) #:nodoc:
429
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{type_to_sql(type, type_options[:limit], type_options[:precision], type_options[:scale])}"
430
- end
431
-
432
- # Drop a column from a table
433
- def remove_column(table_name, column_name) #:nodoc:
434
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
435
- end
436
-
437
- protected
438
-
439
- # Execute a query
440
- def select(sql, name = nil, binds = []) #:nodoc:
441
- return execute(sql, name, binds)
442
- end
443
-
444
- # Queries the structure of a table including the columns names, defaults, type, and nullability
445
- # ActiveRecord uses the type to parse scale and precision information out of the types. As a result,
446
- # chars, varchars, binary, nchars, nvarchars must all be returned in the form <i>type</i>(<i>width</i>)
447
- # numeric and decimal must be returned in the form <i>type</i>(<i>width</i>, <i>scale</i>)
448
- # Nullability is returned as 0 (no nulls allowed) or 1 (nulls allowed)
449
- # Alos, ActiveRecord expects an autoincrement column to have default value of NULL
450
- def table_structure(table_name)
451
- sql = "SELECT COLUMN_NAME, IIF(COLUMN_DEF = 'NULL', null, COLUMN_DEF) as COLUMN_DEF, TYPE_NAME, NULLABLE from (EXECUTE PROCEDURE sp_GetColumns( NULL, NULL, '#{table_name}', NULL )) spgc where table_cat <> 'system';"
452
- structure = execute(sql, :skip_logging)
453
- raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure == false
454
- structure
455
- end
456
-
457
- # Required to prevent DEFAULT NULL being added to primary keys
458
- def options_include_default?(options)
459
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
460
- end
461
-
462
- private
463
-
464
- # Connect
465
- def connect! #:nodoc:
466
- result = ADS.instance.api.ads_connect(@connection, @connection_string)
467
- if result != 1 then
468
- error = ADS.instance.api.ads_error(@connection)
469
- raise ActiveRecord::ActiveRecordError.new("#{error}: Cannot Establish Connection")
470
- end
471
- end
472
-
473
- # Execute a query
474
- def query(sql, binds = []) #:nodoc:
475
- return if sql.nil?
476
-
477
- if binds.empty?
478
- rs = ADS.instance.api.ads_execute_direct(@connection, sql)
479
- else
480
- stmt = ADS.instance.api.ads_prepare(@connection, sql)
481
- # bind each of the parameters
482
- # col: Parameter array. Col[0] -> Parameter info, Col[1] -> Parameter value
483
- binds.each_with_index { |col, index|
484
- result, param = ADS.instance.api.ads_describe_bind_param(stmt, index)
485
- if result == 1
486
- # For date/time/timestamp fix up the format to remove the timzone
487
- if (col[0].type === :datetime or col[0].type === :timestamp or col[0] === :time) and !col[1].nil?
488
- param.set_value(col[1].to_s(:db))
489
- else
490
- param.set_value(col[1])
491
- end
492
- ADS.instance.api.ads_bind_param(stmt, index, param)
493
- else
494
- result, errstr = ADS.instance.api.ads_error(@connection)
495
- raise AdvantageException.new(errstr, result, sql)
496
- end
497
- } #binds.each_with_index
498
- result = ADS.instance.api.ads_execute(stmt)
499
- if result == 1
500
- rs = stmt
501
- else
502
- result, errstr = ADS.instance.api.ads_error(@connection)
503
- raise AdvantageException.new(errstr, result, sql)
504
- end
505
- end
506
- if rs.nil?
507
- result, errstr = ADS.instance.api.ads_error(@connection)
508
- raise AdvantageException.new(errstr, result, sql)
509
- end
510
-
511
- record = []
512
- if( ADS.instance.api.ads_num_cols(rs) > 0 )
513
- while ADS.instance.api.ads_fetch_next(rs) == 1
514
- max_cols = ADS.instance.api.ads_num_cols(rs)
515
- result = Hash.new()
516
- max_cols.times do |cols|
517
- result[ADS.instance.api.ads_get_column_info(rs, cols)[2]] = ADS.instance.api.ads_get_column(rs, cols)[1]
518
- end
519
- record << result
520
- end
521
- @affected_rows = 0
522
- else
523
- @affected_rows = ADS.instance.api.ads_affected_rows(rs)
524
- end
525
- ADS.instance.api.ads_free_stmt(rs)
526
-
527
- return record
528
- end
529
- end
530
- end
531
- end
532
-
1
+ #====================================================
2
+ #
3
+ # Copyright 2008-2010 iAnywhere Solutions, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ #
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+ #
20
+ #
21
+ #====================================================
22
+
23
+ require "active_record/connection_adapters/abstract_adapter"
24
+ require "arel/visitors/advantage.rb"
25
+
26
+ # Singleton class to hold a valid instance of the AdvantageInterface across all connections
27
+ class ADS
28
+ include Singleton
29
+ attr_accessor :api
30
+
31
+ def initialize
32
+ require "advantage" unless defined? Advantage
33
+ @api = Advantage::AdvantageInterface.new()
34
+ raise LoadError, "Could not load ACE library" if Advantage::API.ads_initialize_interface(@api) == 0
35
+ raise LoadError, "Could not initialize ACE library" if @api.ads_init() == 0
36
+ end
37
+ end
38
+
39
+ module ActiveRecord
40
+ class Base
41
+ DEFAULT_CONFIG = { :username => "adssys", :password => nil }
42
+ # Main connection function to Advantage
43
+ # Connection Adapter takes four parameters:
44
+ # * :database (required, no default). Corresponds to "Data Source=" in connection string
45
+ # * :username (optional, default to 'adssys'). Correspons to "User ID=" in connection string
46
+ # * :password (optional, deafult to '')
47
+ # * :options (optional, defaults to ''). Corresponds to any additional options in connection string
48
+
49
+ def self.advantage_connection(config)
50
+ config = DEFAULT_CONFIG.merge(config)
51
+
52
+ raise ArgumentError, "No data source was given. Please add a :database option." unless config.has_key?(:database)
53
+
54
+ connection_string = "data source=#{config[:database]};User ID=#{config[:username]};"
55
+ connection_string += "Password=#{config[:password]};" unless config[:password].nil?
56
+ connection_string += "#{config[:options]};" unless config[:options].nil?
57
+ connection_string += "DateFormat=YYYY-MM-DD;"
58
+
59
+ db = ADS.instance.api.ads_new_connection()
60
+
61
+ ConnectionAdapters::AdvantageAdapter.new(db, logger, connection_string)
62
+ end
63
+ end
64
+
65
+ module ConnectionAdapters
66
+ class AdvantageException < StandardError
67
+ attr_reader :errno
68
+ attr_reader :sql
69
+
70
+ def initialize(message, errno, sql)
71
+ super(message)
72
+ @errno = errno
73
+ @sql = sql
74
+ end
75
+ end
76
+
77
+ class AdvantageColumn < Column
78
+ private
79
+
80
+ # Overridden to handle Advantage integer, varchar, binary, and timestamp types
81
+ def simplified_type(field_type)
82
+ case field_type
83
+ when /logical/i
84
+ :boolean
85
+ when /varchar/i, /char/i, /memo/i
86
+ :string
87
+ when /long binary/i
88
+ :binary
89
+ when /timestamp/i
90
+ :datetime
91
+ when /short|integer/i, /autoinc/i
92
+ :integer
93
+ else
94
+ super
95
+ end
96
+ end
97
+
98
+ # JAD Is this helpful?
99
+ def initialize_type_map(m = type_map)
100
+ m.alias_type %r(memo)i, "char"
101
+ m.alias_type %r(long binary)i, "binary"
102
+ m.alias_type %r(integer)i, "int"
103
+ m.alias_type %r(short)i, "int"
104
+ m.alias_type %r(autoinc)i, "int"
105
+ super
106
+ end
107
+ end
108
+
109
+ class AdvantageAdapter < AbstractAdapter
110
+ def initialize(connection, logger, connection_string = "") #:nodoc:
111
+ super(connection, logger)
112
+ @prepared_statements = false
113
+ @auto_commit = true
114
+ @affected_rows = 0
115
+ @connection_string = connection_string
116
+ @visitor = Arel::Visitors::Advantage.new self
117
+ connect!
118
+ end
119
+
120
+ def adapter_name #:nodoc:
121
+ "Advantage"
122
+ end
123
+
124
+ def supports_migrations? #:nodoc:
125
+ true
126
+ end
127
+
128
+ def requires_reloading? #:nodoc:
129
+ true
130
+ end
131
+
132
+ def active? #:nodoc:
133
+ ADS.instance.api.ads_execute_immediate(@connection, "SELECT 1 FROM SYSTEM.IOTA") == 1
134
+ rescue
135
+ false
136
+ end
137
+
138
+ def disconnect! #:nodoc:
139
+ result = ADS.instance.api.ads_disconnect(@connection)
140
+ super
141
+ end
142
+
143
+ def reconnect! #:nodoc:
144
+ disconnect!
145
+ connect!
146
+ end
147
+
148
+ def supports_count_distinct? #:nodoc:
149
+ true
150
+ end
151
+
152
+ def supports_autoincrement? #:nodoc:
153
+ true
154
+ end
155
+
156
+ # Used from StackOverflow question 1000688
157
+ # Strip alone will return NIL if the string is not altered. In that case,
158
+ # still return the string.
159
+ def strip_or_self(str) #:nodoc:
160
+ str.strip! || str if str
161
+ end
162
+
163
+ # Maps native ActiveRecord/Ruby types into ADS types
164
+ def native_database_types #:nodoc:
165
+ {
166
+ :primary_key => "AUTOINC PRIMARY KEY CONSTRAINT NOT NULL",
167
+ :string => { :name => "varchar", :limit => 255 },
168
+ :text => { :name => "memo" },
169
+ :integer => { :name => "integer" },
170
+ :float => { :name => "float" },
171
+ :decimal => { :name => "numeric" },
172
+ :datetime => { :name => "timestamp" },
173
+ :timestamp => { :name => "timestamp" },
174
+ :time => { :name => "time" },
175
+ :date => { :name => "date" },
176
+ :binary => { :name => "blob" },
177
+ :boolean => { :name => "logical" },
178
+ }
179
+ end
180
+
181
+ # Applies quotations around column names in generated queries
182
+ def quote_column_name(name) #:nodoc:
183
+ %Q("#{name}")
184
+ end
185
+
186
+ def quoted_true #:nodoc:
187
+ "1"
188
+ end
189
+
190
+ def quoted_false #:nodoc:
191
+ "0"
192
+ end
193
+
194
+ # Translate the exception if possible
195
+ def translate_exception(exception, message) #:nodoc:
196
+ return super unless exception.respond_to?(:errno)
197
+ case exception.errno
198
+ when 2121
199
+ if exception.sql !~ /^SELECT/i
200
+ raise ActiveRecord::ActiveRecordError.new(message)
201
+ else
202
+ super
203
+ end
204
+ when 7076
205
+ raise InvalidForeignKey.new(message, exception)
206
+ when 7057
207
+ raise RecordNotUnique.new(message, exception)
208
+ else
209
+ super
210
+ end
211
+ super
212
+ end
213
+
214
+ # The database update function.
215
+ def update_sql(sql, name = nil) #:nodoc:
216
+ execute(sql, name)
217
+ return @affected_rows
218
+ end
219
+
220
+ # The database delete function.
221
+ def delete_sql(sql, name = nil) #:nodoc:
222
+ execute(sql, name)
223
+ return @affected_rows
224
+ end
225
+
226
+ # The database insert function.
227
+ # ActiveRecord requires that insert_sql returns the primary key of the row just inserted. In most cases, this can be accomplished
228
+ # by immediatly querying the @@identity property. If the @@identity property is 0, then passed id_value is used
229
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
230
+ execute(sql, name)
231
+ identity = last_inserted_id(nil)
232
+ retval = id_value if retval == 0
233
+ return retval
234
+ end
235
+
236
+ # The Database insert function as part of the rails changes
237
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) #:nodoc:
238
+ log(sql, "insert", binds) { exec_query(sql, binds) }
239
+ end
240
+
241
+ # The Database update function as part of the rails changes
242
+ def exec_update(sql, name = nil, binds = []) #:nodoc:
243
+ log(sql, "update", binds) { exec_query(sql, binds) }
244
+ end
245
+
246
+ # The Database delete function as part of the rails changes
247
+ def exec_delete(sql, name = nil, binds = []) #:nodoc:
248
+ log(sql, "delete", binds) { exec_query(sql, binds) }
249
+ end
250
+
251
+ def exec_query(sql, name = "SQL", binds = [])
252
+ cols, record = execute(sql, name)
253
+ ActiveRecord::Result.new(cols, record)
254
+ end
255
+
256
+ # Retrieve the last AutoInc ID
257
+ def last_inserted_id(result) #:nodoc:
258
+ rs = ADS.instance.api.ads_execute_direct(@connection, "SELECT LASTAUTOINC( CONNECTION ) FROM SYSTEM.IOTA")
259
+ raise ActiveRecord::StatementInvalid.new("#{ADS.instance.api.ads_error(@connection)}:#{sql}") if rs.nil?
260
+ ADS.instance.api.ads_fetch_next(rs)
261
+ retval, identity = ADS.instance.api.ads_get_column(rs, 0)
262
+ ADS.instance.api.ads_free_stmt(rs)
263
+ identity
264
+ end
265
+
266
+ # Returns a query as an array of arrays
267
+ def select_rows(sql, name = nil)
268
+ exec_query(sql, name).rows
269
+ end
270
+
271
+ # Begin a transaction
272
+ def begin_db_transaction #:nodoc:
273
+ ADS.instance.api.AdsBeginTransaction(@connection)
274
+ @auto_commit = false
275
+ end
276
+
277
+ # Commit the transaction
278
+ def commit_db_transaction #:nodoc:
279
+ ADS.instance.api.ads_commit(@connection)
280
+ @auto_commit = true
281
+ end
282
+
283
+ # Rollback the transaction
284
+ def rollback_db_transaction #:nodoc:
285
+ ADS.instance.api.ads_rollback(@connection)
286
+ @auto_commit = true
287
+ end
288
+
289
+ def add_lock!(sql, options) #:nodoc:
290
+ sql
291
+ end
292
+
293
+ # Advantage does not support sizing of integers based on the sytax INTEGER(size).
294
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
295
+ if native = native_database_types[type]
296
+ if type == :integer
297
+ column_type_sql = "integer"
298
+ elsif type == :string and !limit.nil?
299
+ "varchar (#{limit})"
300
+ else
301
+ super(type, limit, precision, scale)
302
+ end
303
+ else
304
+ super(type, limit, precision, scale)
305
+ end
306
+ end
307
+
308
+ # Retrieve a list of Tables
309
+ def data_source_sql(name = nil, type = nil) #:nodoc:
310
+ "SELECT table_name from (EXECUTE PROCEDURE sp_GetTables( NULL, NULL, '#{name}', 'TABLE' )) spgc where table_cat <> 'system';"
311
+ end
312
+
313
+ # Retrieve a list of Tables
314
+ def tables(name = nil) #:nodoc:
315
+ sql = "EXECUTE PROCEDURE sp_GetTables( NULL, NULL, NULL, 'TABLE' );"
316
+ select(sql, name).map { |row| strip_or_self(row["TABLE_NAME"]) }
317
+ end
318
+
319
+ # Return a list of columns
320
+ def columns(table_name, name = nil) #:nodoc:
321
+ table_structure(table_name).map do |field|
322
+ if Rails::VERSION::MAJOR > 4
323
+ AdvantageColumn.new(strip_or_self(field["COLUMN_NAME"]),
324
+ field["COLUMN_DEF"],
325
+ SqlTypeMetadata.new(sql_type: strip_or_self(field["TYPE_NAME"])),
326
+ field["NULLABLE"])
327
+ elsif Rails::VERSION::MAJOR == 4
328
+ AdvantageColumn.new(strip_or_self(field["COLUMN_NAME"]),
329
+ field["COLUMN_DEF"],
330
+ lookup_cast_type(strip_or_self(field["TYPE_NAME"])),
331
+ strip_or_self(field["TYPE_NAME"]),
332
+ field["NULLABLE"])
333
+ else
334
+ AdvantageColumn.new(strip_or_self(field["COLUMN_NAME"]),
335
+ field["COLUMN_DEF"],
336
+ strip_or_self(field["TYPE_NAME"]),
337
+ field["NULLABLE"])
338
+ end
339
+ end
340
+ end
341
+
342
+ # Return a list of indexes
343
+ # EJS - Is there a way to get info without DD?
344
+ def indexes(table_name, name = nil) #:nodoc:
345
+ sql = "SELECT name, INDEX_OPTIONS & 1 AS [unique], index_expression FROM SYSTEM.INDEXES WHERE parent = '#{table_name}'"
346
+ select(sql, name).map do |row|
347
+ index = IndexDefinition.new(table_name, row["name"])
348
+ index.unique = row["unique"] == 1
349
+ index.columns = row["index_expression"]
350
+ index
351
+ end
352
+ end
353
+
354
+ # Return the primary key
355
+ def primary_key(table_name) #:nodoc:
356
+ sql = "SELECT COLUMN_NAME FROM (EXECUTE PROCEDURE sp_GetBestRowIdentifier( NULL, NULL, '#{table_name}', NULL, FALSE)) as gbri"
357
+ rs = select(sql)
358
+ if !rs.nil? and !rs[0].nil?
359
+ strip_or_self(rs[0]["COLUMN_NAME"])
360
+ else
361
+ nil
362
+ end
363
+ end
364
+
365
+ # Drop an index
366
+ def remove_index(table_name, options = {}) #:nodoc:
367
+ execute "DROP INDEX #{quote_table_name(table_name)}.#{quote_column_name(index_name(table_name, options))}"
368
+ end
369
+
370
+ # Rename a table
371
+ #EJS - can be done without dd?
372
+ def rename_table(name, new_name) #:nodoc:
373
+ execute "EXECUTE PROCEDURE sp_RenameDDObject(#{quote_table_name(name)} , #{quote_table_name(new_name)}, 1 /* ADS_DD_TABLE_OBJECT */, 0 /* Rename File */)"
374
+ end
375
+
376
+ # Helper function to retrieve the columns current type
377
+ def get_column_type(table_name, column_name) #:nodoc:
378
+ sql = <<-SQL
379
+ SELECT
380
+ CASE
381
+ WHEN type_name = 'VARCHAR' or type_name = 'CHAR' or type_name = 'CICHAR' or
382
+ type_name = 'NVARCHAR' or type_name = 'NCHAR' or type_name = 'VARBINARY'
383
+ THEN CAST(TRIM(type_name) + '(' + TRIM(CAST(column_size AS SQL_CHAR)) + ')' AS SQL_CHAR)
384
+ WHEN type_name = 'NUMERIC' and decimal_digits = 0
385
+ THEN CAST('INTEGER(' + TRIM(CAST(column_size AS SQL_CHAR)) + ')' AS SQL_CHAR)
386
+ WHEN type_name = 'NUMERIC' or type_name = 'DOUBLE' or type_name = 'CURDOUBLE'
387
+ THEN CAST(TRIM(type_name) + '(' + TRIM(CAST(column_size AS SQL_CHAR)) + ',' + TRIM(CAST(decimal_digits AS SQL_CHAR)) + ')' AS SQL_CHAR)
388
+ ELSE
389
+ TRIM(type_name COLLATE ads_default_cs)
390
+ END AS "domain"
391
+ from (EXECUTE PROCEDURE sp_GetColumns( NULL, NULL, '#{table_name}', NULL)) as spgc
392
+ WHERE COLUMN_NAME = '#{column_name}'
393
+ SQL
394
+ rs = select(sql)
395
+ if !rs.nil? and !rs[0].nil?
396
+ rs[0]["domain"]
397
+ end
398
+ end
399
+
400
+ # Change a columns defaults.
401
+ def change_column_default(table_name, column_name, default) #:nodoc:
402
+ execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{get_column_type(table_name, column_name)} DEFAULT #{quote(default)}"
403
+ end
404
+
405
+ # Change a columns nullability
406
+ def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
407
+ unless null || default.nil?
408
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
409
+ end
410
+ execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{get_column_type(table_name, column_name)} CONSTRAINT #{null ? "" : "NOT"} NULL")
411
+ end
412
+
413
+ # Alter a column
414
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
415
+ add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, type_options[:limit], type_options[:precision], type_options[:scale])}"
416
+ add_column_options!(add_column_sql, options)
417
+ execute(add_column_sql)
418
+ end
419
+
420
+ # Add column options
421
+ def add_column_options!(sql, options) #:nodoc:
422
+ sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
423
+ # must explicitly check for :null to allow change_column to work on migrations
424
+ if options[:null] == false
425
+ sql << " CONSTRAINT NOT NULL"
426
+ end
427
+ end
428
+
429
+ # Rename a column
430
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
431
+ execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{type_to_sql(type, type_options[:limit], type_options[:precision], type_options[:scale])}"
432
+ end
433
+
434
+ # Drop a column from a table
435
+ def remove_column(table_name, column_name) #:nodoc:
436
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
437
+ end
438
+
439
+ protected
440
+
441
+ # Execute a query
442
+ def select(sql, name = nil, binds = []) #:nodoc:
443
+ if Rails::VERSION::MAJOR >= 4
444
+ exec_query(sql, name, binds)
445
+ else
446
+ exec_query(sql, name, binds).to_hash
447
+ end
448
+ end
449
+
450
+ # Queries the structure of a table including the columns names, defaults, type, and nullability
451
+ # ActiveRecord uses the type to parse scale and precision information out of the types. As a result,
452
+ # chars, varchars, binary, nchars, nvarchars must all be returned in the form <i>type</i>(<i>width</i>)
453
+ # numeric and decimal must be returned in the form <i>type</i>(<i>width</i>, <i>scale</i>)
454
+ # Nullability is returned as 0 (no nulls allowed) or 1 (nulls allowed)
455
+ # Alos, ActiveRecord expects an autoincrement column to have default value of NULL
456
+ def table_structure(table_name)
457
+ # sql = "SELECT COLUMN_NAME, IIF(COLUMN_DEF = 'NULL', null, COLUMN_DEF) as COLUMN_DEF, IIF(TYPE_NAME = 'NUMERIC' and DECIMAL_DIGITS = 0, 'INTEGER', TYPE_NAME) as TYPE_NAME, NULLABLE from (EXECUTE PROCEDURE sp_GetColumns( NULL, NULL, '#{table_name}', NULL )) spgc where table_cat <> 'system';"
458
+ sql = "SELECT COLUMN_NAME, IIF(COLUMN_DEF = 'NULL', null, COLUMN_DEF) as COLUMN_DEF, TYPE_NAME, NULLABLE from (EXECUTE PROCEDURE sp_GetColumns( NULL, NULL, '#{table_name}', NULL )) spgc where table_cat <> 'system';"
459
+ structure = exec_query(sql, :skip_logging)
460
+ raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure == false
461
+ structure
462
+ end
463
+
464
+ # Required to prevent DEFAULT NULL being added to primary keys
465
+ def options_include_default?(options)
466
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
467
+ end
468
+
469
+ private
470
+
471
+ # Connect
472
+ def connect! #:nodoc:
473
+ result = ADS.instance.api.ads_connect(@connection, @connection_string)
474
+ if result != 1
475
+ error = ADS.instance.api.ads_error(@connection)
476
+ raise ActiveRecord::ActiveRecordError.new("#{error}: Cannot Establish Connection")
477
+ end
478
+ end
479
+
480
+ # The database execution function
481
+ def query(sql, name = nil, binds = []) #:nodoc:
482
+ if name == :skip_logging
483
+ execute(sql, binds)
484
+ else
485
+ log(sql, name, binds) { execute(sql, binds) }
486
+ end
487
+ end
488
+
489
+ # Execute a query
490
+ def execute(sql, name = nil, binds = []) #:nodoc:
491
+ return if sql.nil?
492
+
493
+ if binds.empty?
494
+ rs = ADS.instance.api.ads_execute_direct(@connection, sql)
495
+ else
496
+ stmt = ADS.instance.api.ads_prepare(@connection, sql)
497
+ # bind each of the parameters
498
+ # col: Parameter array. Col[0] -> Parameter info, Col[1] -> Parameter value
499
+ binds.each_with_index { |col, index|
500
+ result, param = ADS.instance.api.ads_describe_bind_param(stmt, index)
501
+ if result == 1
502
+ # For date/time/timestamp fix up the format to remove the timzone
503
+ if (col[0].type === :datetime or col[0].type === :timestamp or col[0] === :time) and !col[1].nil?
504
+ param.set_value(col[1].to_s(:db))
505
+ else
506
+ param.set_value(col[1])
507
+ end
508
+ ADS.instance.api.ads_bind_param(stmt, index, param)
509
+ else
510
+ result, errstr = ADS.instance.api.ads_error(@connection)
511
+ raise AdvantageException.new(errstr, result, sql)
512
+ end
513
+ } #binds.each_with_index
514
+ result = ADS.instance.api.ads_execute(stmt)
515
+ if result == 1
516
+ rs = stmt
517
+ else
518
+ result, errstr = ADS.instance.api.ads_error(@connection)
519
+ raise AdvantageException.new(errstr, result, sql)
520
+ end
521
+ end
522
+ if rs.nil?
523
+ result, errstr = ADS.instance.api.ads_error(@connection)
524
+ raise AdvantageException.new(errstr, result, sql)
525
+ end
526
+
527
+ # the record of all the rows
528
+ row_record = []
529
+ # the column headers
530
+ col_headers = []
531
+ if (ADS.instance.api.ads_num_cols(rs) > 0)
532
+ while ADS.instance.api.ads_fetch_next(rs) == 1
533
+ max_cols = ADS.instance.api.ads_num_cols(rs)
534
+ row = []
535
+ max_cols.times do |cols|
536
+ # record the columns the first time through the results
537
+ if row_record.count == 0
538
+ cinfo = ADS.instance.api.ads_get_column_info(rs, cols)
539
+ col_headers << cinfo[2]
540
+ end
541
+ cvalue = ADS.instance.api.ads_get_column(rs, cols)
542
+ row << cvalue[1]
543
+ end
544
+ row_record << row
545
+ end
546
+ @affected_rows = 0
547
+ else
548
+ @affected_rows = ADS.instance.api.ads_affected_rows(rs)
549
+ end
550
+ ADS.instance.api.ads_free_stmt(rs)
551
+
552
+ # force the columns to be unique (I don't believe this does anything now)
553
+ col_headers.uniq!
554
+ return col_headers, row_record
555
+ end
556
+ end
557
+ end
558
+ end