activerecord-advantage-adapter 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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