activerecord-sqlanywhere-adapter 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,570 +1,572 @@
1
- #====================================================
2
- #
3
- # Copyright 2008-2009 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
- # While not a requirement of the license, if you do modify this file, we
20
- # would appreciate hearing about it. Please email sqlany_interfaces@sybase.com
21
- #
22
- #
23
- #====================================================
24
-
25
- require 'active_record/connection_adapters/abstract_adapter'
26
-
27
- # Singleton class to hold a valid instance of the SQLAnywhereInterface across all connections
28
- class SA
29
- include Singleton
30
- attr_accessor :api
31
-
32
- def initialize
33
- require_library_or_gem 'sqlanywhere' unless defined? SQLAnywhere
34
- @api = SQLAnywhere::SQLAnywhereInterface.new()
35
- raise LoadError, "Could not load SQLAnywhere DBCAPI library" if SQLAnywhere::API.sqlany_initialize_interface(@api) == 0
36
- raise LoadError, "Could not initialize SQLAnywhere DBCAPI library" if @api.sqlany_init() == 0
37
- end
38
- end
39
-
40
- module ActiveRecord
41
- class Base
42
- DEFAULT_CONFIG = { :username => 'dba', :password => 'sql' }
43
- # Main connection function to SQL Anywhere
44
- # Connection Adapter takes four parameters:
45
- # * :database (required, no default). Corresponds to "DatabaseName=" in connection string
46
- # * :server (optional, defaults to :databse). Corresponds to "ServerName=" in connection string
47
- # * :username (optional, default to 'dba')
48
- # * :password (optional, deafult to 'sql')
49
- # * :commlinks (optional). Corresponds to "CommLinks=" in connection string
50
- # * :connection_name (optional). Corresponds to "ConnectionName=" in connection string
51
-
52
- def self.sqlanywhere_connection(config)
53
-
54
- config = DEFAULT_CONFIG.merge(config)
55
-
56
- raise ArgumentError, "No database name was given. Please add a :database option." unless config.has_key?(:database)
57
-
58
- connection_string = "ServerName=#{(config[:server] || config[:database])};DatabaseName=#{config[:database]};UserID=#{config[:username]};Password=#{config[:password]};"
59
- connection_string += "CommLinks=#{config[:commlinks]};" unless config[:commlinks].nil?
60
- connection_string += "ConnectionName=#{config[:connection_name]};" unless config[:connection_name].nil?
61
- connection_string += "Idle=0" # Prevent the server from disconnecting us if we're idle for >240mins (by default)
62
-
63
- db = SA.instance.api.sqlany_new_connection()
64
-
65
- ConnectionAdapters::SQLAnywhereAdapter.new(db, logger, connection_string)
66
- end
67
- end
68
-
69
- module ConnectionAdapters
70
- class SQLAnywhereColumn < Column
71
- private
72
- # Overridden to handle SQL Anywhere integer, varchar, binary, and timestamp types
73
- def simplified_type(field_type)
74
- return :boolean if field_type =~ /tinyint/i
75
- return :string if field_type =~ /varchar/i
76
- return :binary if field_type =~ /long binary/i
77
- return :datetime if field_type =~ /timestamp/i
78
- return :integer if field_type =~ /smallint|bigint/i
79
- super
80
- end
81
-
82
- def extract_limit(sql_type)
83
- case sql_type
84
- when /^tinyint/i: 1
85
- when /^smallint/i: 2
86
- when /^integer/i: 4
87
- when /^bigint/i: 8
88
- else super
89
- end
90
- end
91
-
92
- protected
93
- # Handles the encoding of a binary object into SQL Anywhere
94
- # SQL Anywhere requires that binary values be encoded as \xHH, where HH is a hexadecimal number
95
- # This function encodes the binary string in this format
96
- def self.string_to_binary(value)
97
- "\\x" + value.unpack("H*")[0].scan(/../).join("\\x")
98
- end
99
-
100
- def self.binary_to_string(value)
101
- value.gsub(/\\x[0-9]{2}/) { |byte| byte[2..3].hex }
102
- end
103
- end
104
-
105
- class SQLAnywhereAdapter < AbstractAdapter
106
- def initialize( connection, logger = nil, connection_string = "") #:nodoc:
107
- super(connection, logger)
108
- @auto_commit = true
109
- @affected_rows = 0
110
- @connection_string = connection_string
111
- connect!
112
- end
113
-
114
- def adapter_name #:nodoc:
115
- 'SQLAnywhere'
116
- end
117
-
118
- def supports_migrations? #:nodoc:
119
- true
120
- end
121
-
122
- def requires_reloading?
123
- false
124
- end
125
-
126
- def active?
127
- # The liveness variable is used a low-cost "no-op" to test liveness
128
- SA.instance.api.sqlany_execute_immediate(@connection, "SET liveness = 1") == 1
129
- rescue
130
- false
131
- end
132
-
133
- def disconnect!
134
- result = SA.instance.api.sqlany_disconnect( @connection )
135
- super
136
- end
137
-
138
- def reconnect!
139
- disconnect!
140
- connect!
141
- end
142
-
143
- def supports_count_distinct? #:nodoc:
144
- true
145
- end
146
-
147
- def supports_autoincrement? #:nodoc:
148
- true
149
- end
150
-
151
- # Maps native ActiveRecord/Ruby types into SQLAnywhere types
152
- # TINYINTs are treated as the default boolean value
153
- # ActiveRecord allows NULLs in boolean columns, and the SQL Anywhere BIT type does not
154
- # As a result, TINYINT must be used. All TINYINT columns will be assumed to be boolean and
155
- # should not be used as single-byte integer columns. This restriction is similar to other ActiveRecord database drivers
156
- def native_database_types #:nodoc:
157
- {
158
- :primary_key => 'INTEGER PRIMARY KEY DEFAULT AUTOINCREMENT NOT NULL',
159
- :string => { :name => "varchar", :limit => 255 },
160
- :text => { :name => "long varchar" },
161
- :integer => { :name => "integer" },
162
- :float => { :name => "float" },
163
- :decimal => { :name => "decimal" },
164
- :datetime => { :name => "datetime" },
165
- :timestamp => { :name => "datetime" },
166
- :time => { :name => "time" },
167
- :date => { :name => "date" },
168
- :binary => { :name => "long binary" },
169
- :boolean => { :name => "tinyint"}
170
- }
171
- end
172
-
173
- # QUOTING ==================================================
174
-
175
- # Applies quotations around column names in generated queries
176
- def quote_column_name(name) #:nodoc:
177
- %Q("#{name}")
178
- end
179
-
180
- # Handles special quoting of binary columns. Binary columns will be treated as strings inside of ActiveRecord.
181
- # ActiveRecord requires that any strings it inserts into databases must escape the backslash (\).
182
- # Since in the binary case, the (\x) is significant to SQL Anywhere, it cannot be escaped.
183
- def quote(value, column = nil)
184
- case value
185
- when String, ActiveSupport::Multibyte::Chars
186
- value_S = value.to_s
187
- if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
188
- "#{quoted_string_prefix}'#{column.class.string_to_binary(value_S)}'"
189
- else
190
- super(value, column)
191
- end
192
- else
193
- super(value, column)
194
- end
195
- end
196
-
197
- def quoted_true
198
- '1'
199
- end
200
-
201
- def quoted_false
202
- '0'
203
- end
204
-
205
-
206
- # SQL Anywhere, in accordance with the SQL Standard, does not allow a column to appear in the ORDER BY list
207
- # that is not also in the SELECT with when obtaining DISTINCT rows beacuse the actual semantics of this query
208
- # are unclear. The following functions create a query that mimics the way that SQLite and MySQL handle this query.
209
- #
210
- # This function (distinct) is based on the Oracle ActiveRecord driver created by Graham Jenkins (2005)
211
- # (http://svn.rubyonrails.org/rails/adapters/oracle/lib/active_record/connection_adapters/oracle_adapter.rb)
212
- def distinct(columns, order_by)
213
- return "DISTINCT #{columns}" if order_by.blank?
214
- order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
215
- order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
216
- "FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
217
- end
218
- sql = "DISTINCT #{columns}, "
219
- sql << order_columns * ", "
220
- end
221
-
222
- # This function (add_order_by_for_association_limiting) is based on the Oracle ActiveRecord driver created by Graham Jenkins (2005)
223
- # (http://svn.rubyonrails.org/rails/adapters/oracle/lib/active_record/connection_adapters/oracle_adapter.rb)
224
- def add_order_by_for_association_limiting!(sql, options)
225
- return sql if options[:order].blank?
226
-
227
- order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
228
- order.map! {|s| $1 if s =~ / (.*)/}
229
- order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
230
-
231
- sql << " ORDER BY #{order}"
232
- end
233
-
234
- # The database execution function
235
- def execute(sql, name = nil) #:nodoc:
236
- return if sql.nil?
237
- sql = modify_limit_offset(sql)
238
-
239
- # ActiveRecord allows a query to return TOP 0. SQL Anywhere requires that the TOP value is a positive integer.
240
- return Array.new() if sql =~ /TOP 0/i
241
-
242
- # Executes the query, iterates through the results, and builds an array of hashes.
243
- rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
244
- if rs.nil?
245
- error = SA.instance.api.sqlany_error(@connection)
246
- case error[0].to_i
247
- when -143
248
- if sql =~ /^SELECT/i then
249
- raise ActiveRecord::StatementInvalid.new("#{error}:#{sql}")
250
- else
251
- raise ActiveRecord::ActiveRecordError.new("#{error}:#{sql}")
252
- end
253
- else
254
- raise ActiveRecord::StatementInvalid.new("#{error}:#{sql}")
255
- end
256
- end
257
-
258
- record = []
259
- if( SA.instance.api.sqlany_num_cols(rs) > 0 )
260
- while SA.instance.api.sqlany_fetch_next(rs) == 1
261
- max_cols = SA.instance.api.sqlany_num_cols(rs)
262
- result = Hash.new()
263
- max_cols.times do |cols|
264
- result[SA.instance.api.sqlany_get_column_info(rs, cols)[2]] = SA.instance.api.sqlany_get_column(rs, cols)[1]
265
- end
266
- record << result
267
- end
268
- @affected_rows = 0
269
- else
270
- @affected_rows = SA.instance.api.sqlany_affected_rows(rs)
271
- end
272
- SA.instance.api.sqlany_free_stmt(rs)
273
-
274
- SA.instance.api.sqlany_commit(@connection) if @auto_commit
275
- return record
276
- end
277
-
278
- # The database update function.
279
- def update_sql(sql, name = nil)
280
- execute( sql, name )
281
- return @affected_rows
282
- end
283
-
284
- # The database delete function.
285
- def delete_sql(sql, name = nil) #:nodoc:
286
- execute( sql, name )
287
- return @affected_rows
288
- end
289
-
290
- # The database insert function.
291
- # ActiveRecord requires that insert_sql returns the primary key of the row just inserted. In most cases, this can be accomplished
292
- # by immediatly querying the @@identity property. If the @@identity property is 0, then passed id_value is used
293
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
294
- execute(sql, name)
295
-
296
- identity = SA.instance.api.sqlany_execute_direct(@connection, 'SELECT @@identity')
297
- raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if identity.nil?
298
- SA.instance.api.sqlany_fetch_next(identity)
299
- retval = SA.instance.api.sqlany_get_column(identity, 0)[1]
300
- SA.instance.api.sqlany_free_stmt(identity)
301
-
302
- retval = id_value if retval == 0
303
- return retval
304
- end
305
-
306
- # Returns a query as an array of arrays
307
- def select_rows(sql, name = nil)
308
- rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
309
- raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if rs.nil?
310
- record = []
311
- while SA.instance.api.sqlany_fetch_next(rs) == 1
312
- max_cols = SA.instance.api.sqlany_num_cols(rs)
313
- result = Array.new(max_cols)
314
- max_cols.times do |cols|
315
- result[cols] = SA.instance.api.sqlany_get_column(rs, cols)[1]
316
- end
317
- record << result
318
- end
319
- SA.instance.api.sqlany_free_stmt(rs)
320
- return record
321
- end
322
-
323
- def begin_db_transaction #:nodoc:
324
- @auto_commit = false;
325
- end
326
-
327
- def commit_db_transaction #:nodoc:
328
- SA.instance.api.sqlany_commit(@connection)
329
- @auto_commit = true;
330
- end
331
-
332
- def rollback_db_transaction #:nodoc:
333
- SA.instance.api.sqlany_rollback(@connection)
334
- @auto_commit = true;
335
- end
336
-
337
- def add_lock!(sql, options) #:nodoc:
338
- sql
339
- end
340
-
341
- # SQL Anywhere does not support sizing of integers based on the sytax INTEGER(size). Integer sizes
342
- # must be captured when generating the SQL and replaced with the appropriate size.
343
- def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
344
- if native = native_database_types[type]
345
- if type == :integer
346
- case limit
347
- when 1
348
- column_type_sql = 'tinyint'
349
- when 2
350
- column_type_sql = 'smallint'
351
- when 3..4
352
- column_type_sql = 'integer'
353
- when 5..8
354
- column_type_sql = 'bigint'
355
- else
356
- column_type_sql = 'integer'
357
- end
358
- column_type_sql
359
- else
360
- super(type, limit, precision, scale)
361
- end
362
- else
363
- super(type, limit, precision, scale)
364
- end
365
- end
366
-
367
- # Do not return SYS-owned or DBO-owned tables
368
- def tables(name = nil) #:nodoc:
369
- sql = "SELECT table_name FROM systable WHERE creator not in (0,3)"
370
- select(sql, name).map { |row| row["table_name"] }
371
- end
372
-
373
- def columns(table_name, name = nil) #:nodoc:
374
- table_structure(table_name).map do |field|
375
- field['default'] = field['default'][1..-2] if (!field['default'].nil? and field['default'][0].chr == "'")
376
- SQLAnywhereColumn.new(field['name'], field['default'], field['domain'], (field['nulls'] == 1))
377
- end
378
- end
379
-
380
- def indexes(table_name, name = nil) #:nodoc:
381
- sql = "SELECT DISTINCT index_name, \"unique\" FROM sys.systable INNER JOIN sys.sysidxcol ON sys.systable.table_id = sys.sysidxcol.table_id INNER JOIN sys.sysidx ON sys.systable.table_id = sys.sysidx.table_id AND sys.sysidxcol.index_id = sys.sysidx.index_id WHERE table_name = '#{table_name}' AND index_category > 2"
382
- select(sql, name).map do |row|
383
- index = IndexDefinition.new(table_name, row['index_name'])
384
- index.unique = row['unique'] == 1
385
- sql = "SELECT column_name FROM sys.sysidx INNER JOIN sys.sysidxcol ON sys.sysidxcol.table_id = sys.sysidx.table_id AND sys.sysidxcol.index_id = sys.sysidx.index_id INNER JOIN sys.syscolumn ON sys.syscolumn.table_id = sys.sysidxcol.table_id AND sys.syscolumn.column_id = sys.sysidxcol.column_id WHERE index_name = '#{row['index_name']}'"
386
- index.columns = select(sql).map { |col| col['column_name'] }
387
- index
388
- end
389
- end
390
-
391
- def primary_key(table_name) #:nodoc:
392
- sql = "SELECT sys.systabcol.column_name FROM (sys.systable JOIN sys.systabcol) LEFT OUTER JOIN (sys.sysidxcol JOIN sys.sysidx) WHERE table_name = '#{table_name}' AND sys.sysidxcol.sequence = 0"
393
- rs = select(sql)
394
- if !rs.nil? and !rs[0].nil?
395
- rs[0]['column_name']
396
- else
397
- nil
398
- end
399
- end
400
-
401
- def remove_index(table_name, options={}) #:nodoc:
402
- execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
403
- end
404
-
405
- def rename_table(name, new_name)
406
- execute "ALTER TABLE #{quote_table_name(name)} RENAME #{quote_table_name(new_name)}"
407
- end
408
-
409
- def remove_column(table_name, column_name) #:nodoc:
410
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
411
- end
412
-
413
- def change_column_default(table_name, column_name, default) #:nodoc:
414
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
415
- end
416
-
417
- def change_column_null(table_name, column_name, null, default = nil)
418
- unless null || default.nil?
419
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
420
- end
421
- execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? '' : 'NOT'} NULL")
422
- end
423
-
424
- def change_column(table_name, column_name, type, options = {}) #:nodoc:
425
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
426
- add_column_options!(add_column_sql, options)
427
- add_column_sql << ' NULL' if options[:null]
428
- execute(add_column_sql)
429
- end
430
-
431
- def rename_column(table_name, column_name, new_column_name) #:nodoc:
432
- execute "ALTER TABLE #{quote_table_name(table_name)} RENAME #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
433
- end
434
-
435
- def remove_column(table_name, column_name)
436
- sql = "SELECT \"index_name\" FROM SYS.SYSTAB join SYS.SYSTABCOL join SYS.SYSIDXCOL join SYS.SYSIDX WHERE \"column_name\" = '#{column_name}' AND \"table_name\" = '#{table_name}'"
437
- select(sql, nil).map do |row|
438
- execute "DROP INDEX \"#{table_name}\".\"#{row['index_name']}\""
439
- end
440
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
441
- end
442
-
443
- protected
444
- def select(sql, name = nil) #:nodoc:
445
- return execute(sql, name)
446
- end
447
-
448
- # ActiveRecord uses the OFFSET/LIMIT keywords at the end of query to limit the number of items in the result set.
449
- # This syntax is NOT supported by SQL Anywhere. In previous versions of this adapter this adapter simply
450
- # overrode the add_limit_offset function and added the appropriate TOP/START AT keywords to the start of the query.
451
- # However, this will not work for cases where add_limit_offset is being used in a subquery since add_limit_offset
452
- # is called with the WHERE clause.
453
- #
454
- # As a result, the following function must be called before every SELECT statement against the database. It
455
- # recursivly walks through all subqueries in the SQL statment and replaces the instances of OFFSET/LIMIT with the
456
- # corresponding TOP/START AT. It was my intent to do the entire thing using regular expressions, but it would seem
457
- # that it is not possible given that it must count levels of nested brackets.
458
- def modify_limit_offset(sql)
459
- modified_sql = ""
460
- subquery_sql = ""
461
- in_single_quote = false
462
- in_double_quote = false
463
- nesting_level = 0
464
- if sql =~ /(OFFSET|LIMIT)/xmi then
465
- if sql =~ /\(/ then
466
- sql.split(//).each_with_index do |x, i|
467
- case x[0]
468
- when 40 # left brace - (
469
- modified_sql << x if nesting_level == 0
470
- subquery_sql << x if nesting_level > 0
471
- nesting_level = nesting_level + 1 unless in_double_quote || in_single_quote
472
- when 41 # right brace - )
473
- nesting_level = nesting_level - 1 unless in_double_quote || in_single_quote
474
- if nesting_level == 0 and !in_double_quote and !in_single_quote then
475
- modified_sql << modify_limit_offset(subquery_sql)
476
- subquery_sql = ""
477
- end
478
- modified_sql << x if nesting_level == 0
479
- subquery_sql << x if nesting_level > 0
480
- when 39 # single quote - '
481
- in_single_quote = in_single_quote ^ true unless in_double_quote
482
- modified_sql << x if nesting_level == 0
483
- subquery_sql << x if nesting_level > 0
484
- when 34 # double quote - "
485
- in_double_quote = in_double_quote ^ true unless in_single_quote
486
- modified_sql << x if nesting_level == 0
487
- subquery_sql << x if nesting_level > 0
488
- else
489
- modified_sql << x if nesting_level == 0
490
- subquery_sql << x if nesting_level > 0
491
- end
492
- raise ActiveRecord::StatementInvalid.new("Braces do not match: #{sql}") if nesting_level < 0
493
- end
494
- else
495
- modified_sql = sql
496
- end
497
- raise ActiveRecord::StatementInvalid.new("Quotes do not match: #{sql}") if in_double_quote or in_single_quote
498
- return "" if modified_sql.nil?
499
- select_components = modified_sql.scan(/\ASELECT\s+(DISTINCT)?(.*?)(?:\s+LIMIT\s+(.*?))?(?:\s+OFFSET\s+(.*?))?\Z/xmi)
500
- return modified_sql if select_components[0].nil?
501
- final_sql = "SELECT #{select_components[0][0]} "
502
- final_sql << "TOP #{select_components[0][2]} " unless select_components[0][2].nil?
503
- final_sql << "START AT #{(select_components[0][3].to_i + 1).to_s} " unless select_components[0][3].nil?
504
- final_sql << "#{select_components[0][1]}"
505
- return final_sql
506
- else
507
- return sql
508
- end
509
- end
510
-
511
- # Queries the structure of a table including the columns names, defaults, type, and nullability
512
- # ActiveRecord uses the type to parse scale and precision information out of the types. As a result,
513
- # chars, varchars, binary, nchars, nvarchars must all be returned in the form <i>type</i>(<i>width</i>)
514
- # numeric and decimal must be returned in the form <i>type</i>(<i>width</i>, <i>scale</i>)
515
- # Nullability is returned as 0 (no nulls allowed) or 1 (nulls allowed)
516
- # Alos, ActiveRecord expects an autoincrement column to have default value of NULL
517
-
518
- def table_structure(table_name)
519
- sql = <<-SQL
520
- SELECT sys.syscolumn.column_name AS name,
521
- NULLIF(sys.syscolumn."default", 'autoincrement') AS "default",
522
- IF sys.syscolumn.domain_id IN (7,8,9,11,33,34,35,3,27) THEN
523
- IF sys.syscolumn.domain_id IN (3,27) THEN
524
- sys.sysdomain.domain_name || '(' || sys.syscolumn.width || ',' || sys.syscolumn.scale || ')'
525
- ELSE
526
- sys.sysdomain.domain_name || '(' || sys.syscolumn.width || ')'
527
- ENDIF
528
- ELSE
529
- sys.sysdomain.domain_name
530
- ENDIF AS domain,
531
- IF sys.syscolumn.nulls = 'Y' THEN 1 ELSE 0 ENDIF AS nulls
532
- FROM
533
- sys.syscolumn
534
- INNER JOIN sys.systable ON sys.syscolumn.table_id = sys.systable.table_id
535
- INNER JOIN sys.sysdomain ON sys.syscolumn.domain_id = sys.sysdomain.domain_id
536
- WHERE
537
- table_name = '#{table_name}'
538
- SQL
539
- returning structure = select(sql) do
540
- raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if false
541
- end
542
- end
543
-
544
- # Required to prevent DEFAULT NULL being added to primary keys
545
- def options_include_default?(options)
546
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
547
- end
548
-
549
- private
550
-
551
- def connect!
552
- result = SA.instance.api.sqlany_connect(@connection, @connection_string)
553
- if result == 1 then
554
- set_connection_options
555
- else
556
- error = SA.instance.api.sqlany_error(@connection)
557
- raise ActiveRecord::ActiveRecordError.new("#{error}: Cannot Establish Connection")
558
- end
559
- end
560
-
561
- def set_connection_options
562
- SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION non_keywords = 'LOGIN'") rescue nil
563
- SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION timestamp_format = 'YYYY-MM-DD HH:NN:SS'") rescue nil
564
- # The liveness variable is used a low-cost "no-op" to test liveness
565
- SA.instance.api.sqlany_execute_immediate(@connection, "CREATE VARIABLE liveness INT") rescue nil
566
- end
567
- end
568
- end
569
- end
570
-
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
+ # While not a requirement of the license, if you do modify this file, we
20
+ # would appreciate hearing about it. Please email sqlany_interfaces@sybase.com
21
+ #
22
+ #
23
+ #====================================================
24
+
25
+ require 'active_record/connection_adapters/abstract_adapter'
26
+
27
+ # Singleton class to hold a valid instance of the SQLAnywhereInterface across all connections
28
+ class SA
29
+ include Singleton
30
+ attr_accessor :api
31
+
32
+ def initialize
33
+ require_library_or_gem 'sqlanywhere' unless defined? SQLAnywhere
34
+ @api = SQLAnywhere::SQLAnywhereInterface.new()
35
+ raise LoadError, "Could not load SQLAnywhere DBCAPI library" if SQLAnywhere::API.sqlany_initialize_interface(@api) == 0
36
+ raise LoadError, "Could not initialize SQLAnywhere DBCAPI library" if @api.sqlany_init() == 0
37
+ end
38
+ end
39
+
40
+ module ActiveRecord
41
+ class Base
42
+ DEFAULT_CONFIG = { :username => 'dba', :password => 'sql' }
43
+ # Main connection function to SQL Anywhere
44
+ # Connection Adapter takes four parameters:
45
+ # * :database (required, no default). Corresponds to "DatabaseName=" in connection string
46
+ # * :server (optional, defaults to :databse). Corresponds to "ServerName=" in connection string
47
+ # * :username (optional, default to 'dba')
48
+ # * :password (optional, deafult to 'sql')
49
+ # * :encoding (optional, defaults to charset of OS)
50
+ # * :commlinks (optional). Corresponds to "CommLinks=" in connection string
51
+ # * :connection_name (optional). Corresponds to "ConnectionName=" in connection string
52
+
53
+ def self.sqlanywhere_connection(config)
54
+
55
+ config = DEFAULT_CONFIG.merge(config)
56
+
57
+ raise ArgumentError, "No database name was given. Please add a :database option." unless config.has_key?(:database)
58
+
59
+ connection_string = "ServerName=#{(config[:server] || config[:database])};DatabaseName=#{config[:database]};UserID=#{config[:username]};Password=#{config[:password]};"
60
+ connection_string += "CommLinks=#{config[:commlinks]};" unless config[:commlinks].nil?
61
+ connection_string += "ConnectionName=#{config[:connection_name]};" unless config[:connection_name].nil?
62
+ connection_string += "CharSet=#{config[:encoding]};" unless config[:encoding].nil?
63
+ connection_string += "Idle=0" # Prevent the server from disconnecting us if we're idle for >240mins (by default)
64
+
65
+ db = SA.instance.api.sqlany_new_connection()
66
+
67
+ ConnectionAdapters::SQLAnywhereAdapter.new(db, logger, connection_string)
68
+ end
69
+ end
70
+
71
+ module ConnectionAdapters
72
+ class SQLAnywhereColumn < Column
73
+ private
74
+ # Overridden to handle SQL Anywhere integer, varchar, binary, and timestamp types
75
+ def simplified_type(field_type)
76
+ return :boolean if field_type =~ /tinyint/i
77
+ return :string if field_type =~ /varchar/i
78
+ return :binary if field_type =~ /long binary/i
79
+ return :datetime if field_type =~ /timestamp/i
80
+ return :integer if field_type =~ /smallint|bigint/i
81
+ super
82
+ end
83
+
84
+ def extract_limit(sql_type)
85
+ case sql_type
86
+ when /^tinyint/i: 1
87
+ when /^smallint/i: 2
88
+ when /^integer/i: 4
89
+ when /^bigint/i: 8
90
+ else super
91
+ end
92
+ end
93
+
94
+ protected
95
+ # Handles the encoding of a binary object into SQL Anywhere
96
+ # SQL Anywhere requires that binary values be encoded as \xHH, where HH is a hexadecimal number
97
+ # This function encodes the binary string in this format
98
+ def self.string_to_binary(value)
99
+ "\\x" + value.unpack("H*")[0].scan(/../).join("\\x")
100
+ end
101
+
102
+ def self.binary_to_string(value)
103
+ value.gsub(/\\x[0-9]{2}/) { |byte| byte[2..3].hex }
104
+ end
105
+ end
106
+
107
+ class SQLAnywhereAdapter < AbstractAdapter
108
+ def initialize( connection, logger = nil, connection_string = "") #:nodoc:
109
+ super(connection, logger)
110
+ @auto_commit = true
111
+ @affected_rows = 0
112
+ @connection_string = connection_string
113
+ connect!
114
+ end
115
+
116
+ def adapter_name #:nodoc:
117
+ 'SQLAnywhere'
118
+ end
119
+
120
+ def supports_migrations? #:nodoc:
121
+ true
122
+ end
123
+
124
+ def requires_reloading?
125
+ true
126
+ end
127
+
128
+ def active?
129
+ # The liveness variable is used a low-cost "no-op" to test liveness
130
+ SA.instance.api.sqlany_execute_immediate(@connection, "SET liveness = 1") == 1
131
+ rescue
132
+ false
133
+ end
134
+
135
+ def disconnect!
136
+ result = SA.instance.api.sqlany_disconnect( @connection )
137
+ super
138
+ end
139
+
140
+ def reconnect!
141
+ disconnect!
142
+ connect!
143
+ end
144
+
145
+ def supports_count_distinct? #:nodoc:
146
+ true
147
+ end
148
+
149
+ def supports_autoincrement? #:nodoc:
150
+ true
151
+ end
152
+
153
+ # Maps native ActiveRecord/Ruby types into SQLAnywhere types
154
+ # TINYINTs are treated as the default boolean value
155
+ # ActiveRecord allows NULLs in boolean columns, and the SQL Anywhere BIT type does not
156
+ # As a result, TINYINT must be used. All TINYINT columns will be assumed to be boolean and
157
+ # should not be used as single-byte integer columns. This restriction is similar to other ActiveRecord database drivers
158
+ def native_database_types #:nodoc:
159
+ {
160
+ :primary_key => 'INTEGER PRIMARY KEY DEFAULT AUTOINCREMENT NOT NULL',
161
+ :string => { :name => "varchar", :limit => 255 },
162
+ :text => { :name => "long varchar" },
163
+ :integer => { :name => "integer" },
164
+ :float => { :name => "float" },
165
+ :decimal => { :name => "decimal" },
166
+ :datetime => { :name => "datetime" },
167
+ :timestamp => { :name => "datetime" },
168
+ :time => { :name => "time" },
169
+ :date => { :name => "date" },
170
+ :binary => { :name => "long binary" },
171
+ :boolean => { :name => "tinyint"}
172
+ }
173
+ end
174
+
175
+ # QUOTING ==================================================
176
+
177
+ # Applies quotations around column names in generated queries
178
+ def quote_column_name(name) #:nodoc:
179
+ %Q("#{name}")
180
+ end
181
+
182
+ # Handles special quoting of binary columns. Binary columns will be treated as strings inside of ActiveRecord.
183
+ # ActiveRecord requires that any strings it inserts into databases must escape the backslash (\).
184
+ # Since in the binary case, the (\x) is significant to SQL Anywhere, it cannot be escaped.
185
+ def quote(value, column = nil)
186
+ case value
187
+ when String, ActiveSupport::Multibyte::Chars
188
+ value_S = value.to_s
189
+ if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
190
+ "#{quoted_string_prefix}'#{column.class.string_to_binary(value_S)}'"
191
+ else
192
+ super(value, column)
193
+ end
194
+ else
195
+ super(value, column)
196
+ end
197
+ end
198
+
199
+ def quoted_true
200
+ '1'
201
+ end
202
+
203
+ def quoted_false
204
+ '0'
205
+ end
206
+
207
+
208
+ # SQL Anywhere, in accordance with the SQL Standard, does not allow a column to appear in the ORDER BY list
209
+ # that is not also in the SELECT with when obtaining DISTINCT rows beacuse the actual semantics of this query
210
+ # are unclear. The following functions create a query that mimics the way that SQLite and MySQL handle this query.
211
+ #
212
+ # This function (distinct) is based on the Oracle ActiveRecord driver created by Graham Jenkins (2005)
213
+ # (http://svn.rubyonrails.org/rails/adapters/oracle/lib/active_record/connection_adapters/oracle_adapter.rb)
214
+ def distinct(columns, order_by)
215
+ return "DISTINCT #{columns}" if order_by.blank?
216
+ order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
217
+ order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
218
+ "FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
219
+ end
220
+ sql = "DISTINCT #{columns}, "
221
+ sql << order_columns * ", "
222
+ end
223
+
224
+ # This function (add_order_by_for_association_limiting) is based on the Oracle ActiveRecord driver created by Graham Jenkins (2005)
225
+ # (http://svn.rubyonrails.org/rails/adapters/oracle/lib/active_record/connection_adapters/oracle_adapter.rb)
226
+ def add_order_by_for_association_limiting!(sql, options)
227
+ return sql if options[:order].blank?
228
+
229
+ order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
230
+ order.map! {|s| $1 if s =~ / (.*)/}
231
+ order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
232
+
233
+ sql << " ORDER BY #{order}"
234
+ end
235
+
236
+ # The database execution function
237
+ def execute(sql, name = nil) #:nodoc:
238
+ return if sql.nil?
239
+ sql = modify_limit_offset(sql)
240
+
241
+ # ActiveRecord allows a query to return TOP 0. SQL Anywhere requires that the TOP value is a positive integer.
242
+ return Array.new() if sql =~ /TOP 0/i
243
+
244
+ # Executes the query, iterates through the results, and builds an array of hashes.
245
+ rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
246
+ if rs.nil?
247
+ error = SA.instance.api.sqlany_error(@connection)
248
+ case error[0].to_i
249
+ when -143
250
+ if sql =~ /^SELECT/i then
251
+ raise ActiveRecord::StatementInvalid.new("#{error}:#{sql}")
252
+ else
253
+ raise ActiveRecord::ActiveRecordError.new("#{error}:#{sql}")
254
+ end
255
+ else
256
+ raise ActiveRecord::StatementInvalid.new("#{error}:#{sql}")
257
+ end
258
+ end
259
+
260
+ record = []
261
+ if( SA.instance.api.sqlany_num_cols(rs) > 0 )
262
+ while SA.instance.api.sqlany_fetch_next(rs) == 1
263
+ max_cols = SA.instance.api.sqlany_num_cols(rs)
264
+ result = Hash.new()
265
+ max_cols.times do |cols|
266
+ result[SA.instance.api.sqlany_get_column_info(rs, cols)[2]] = SA.instance.api.sqlany_get_column(rs, cols)[1]
267
+ end
268
+ record << result
269
+ end
270
+ @affected_rows = 0
271
+ else
272
+ @affected_rows = SA.instance.api.sqlany_affected_rows(rs)
273
+ end
274
+ SA.instance.api.sqlany_free_stmt(rs)
275
+
276
+ SA.instance.api.sqlany_commit(@connection) if @auto_commit
277
+ return record
278
+ end
279
+
280
+ # The database update function.
281
+ def update_sql(sql, name = nil)
282
+ execute( sql, name )
283
+ return @affected_rows
284
+ end
285
+
286
+ # The database delete function.
287
+ def delete_sql(sql, name = nil) #:nodoc:
288
+ execute( sql, name )
289
+ return @affected_rows
290
+ end
291
+
292
+ # The database insert function.
293
+ # ActiveRecord requires that insert_sql returns the primary key of the row just inserted. In most cases, this can be accomplished
294
+ # by immediatly querying the @@identity property. If the @@identity property is 0, then passed id_value is used
295
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
296
+ execute(sql, name)
297
+
298
+ identity = SA.instance.api.sqlany_execute_direct(@connection, 'SELECT @@identity')
299
+ raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if identity.nil?
300
+ SA.instance.api.sqlany_fetch_next(identity)
301
+ retval = SA.instance.api.sqlany_get_column(identity, 0)[1]
302
+ SA.instance.api.sqlany_free_stmt(identity)
303
+
304
+ retval = id_value if retval == 0
305
+ return retval
306
+ end
307
+
308
+ # Returns a query as an array of arrays
309
+ def select_rows(sql, name = nil)
310
+ rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
311
+ raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if rs.nil?
312
+ record = []
313
+ while SA.instance.api.sqlany_fetch_next(rs) == 1
314
+ max_cols = SA.instance.api.sqlany_num_cols(rs)
315
+ result = Array.new(max_cols)
316
+ max_cols.times do |cols|
317
+ result[cols] = SA.instance.api.sqlany_get_column(rs, cols)[1]
318
+ end
319
+ record << result
320
+ end
321
+ SA.instance.api.sqlany_free_stmt(rs)
322
+ return record
323
+ end
324
+
325
+ def begin_db_transaction #:nodoc:
326
+ @auto_commit = false;
327
+ end
328
+
329
+ def commit_db_transaction #:nodoc:
330
+ SA.instance.api.sqlany_commit(@connection)
331
+ @auto_commit = true;
332
+ end
333
+
334
+ def rollback_db_transaction #:nodoc:
335
+ SA.instance.api.sqlany_rollback(@connection)
336
+ @auto_commit = true;
337
+ end
338
+
339
+ def add_lock!(sql, options) #:nodoc:
340
+ sql
341
+ end
342
+
343
+ # SQL Anywhere does not support sizing of integers based on the sytax INTEGER(size). Integer sizes
344
+ # must be captured when generating the SQL and replaced with the appropriate size.
345
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
346
+ if native = native_database_types[type]
347
+ if type == :integer
348
+ case limit
349
+ when 1
350
+ column_type_sql = 'tinyint'
351
+ when 2
352
+ column_type_sql = 'smallint'
353
+ when 3..4
354
+ column_type_sql = 'integer'
355
+ when 5..8
356
+ column_type_sql = 'bigint'
357
+ else
358
+ column_type_sql = 'integer'
359
+ end
360
+ column_type_sql
361
+ else
362
+ super(type, limit, precision, scale)
363
+ end
364
+ else
365
+ super(type, limit, precision, scale)
366
+ end
367
+ end
368
+
369
+ # Do not return SYS-owned or DBO-owned tables
370
+ def tables(name = nil) #:nodoc:
371
+ sql = "SELECT table_name FROM SYS.SYSTABLE WHERE creator NOT IN (0,3)"
372
+ select(sql, name).map { |row| row["table_name"] }
373
+ end
374
+
375
+ def columns(table_name, name = nil) #:nodoc:
376
+ table_structure(table_name).map do |field|
377
+ field['default'] = field['default'][1..-2] if (!field['default'].nil? and field['default'][0].chr == "'")
378
+ SQLAnywhereColumn.new(field['name'], field['default'], field['domain'], (field['nulls'] == 1))
379
+ end
380
+ end
381
+
382
+ def indexes(table_name, name = nil) #:nodoc:
383
+ sql = "SELECT DISTINCT index_name, \"unique\" FROM SYS.SYSTABLE INNER JOIN SYS.SYSIDXCOL ON SYS.SYSTABLE.table_id = SYS.SYSIDXCOL.table_id INNER JOIN SYS.SYSIDX ON SYS.SYSTABLE.table_id = SYS.SYSIDX.table_id AND SYS.SYSIDXCOL.index_id = SYS.SYSIDX.index_id WHERE table_name = '#{table_name}' AND index_category > 2"
384
+ select(sql, name).map do |row|
385
+ index = IndexDefinition.new(table_name, row['index_name'])
386
+ index.unique = row['unique'] == 1
387
+ sql = "SELECT column_name FROM SYS.SYSIDX INNER JOIN SYS.SYSIDXCOL ON SYS.SYSIDXCOL.table_id = SYS.SYSIDX.table_id AND SYS.SYSIDXCOL.index_id = SYS.SYSIDX.index_id INNER JOIN SYS.SYSCOLUMN ON SYS.SYSCOLUMN.table_id = SYS.SYSIDXCOL.table_id AND SYS.SYSCOLUMN.column_id = SYS.SYSIDXCOL.column_id WHERE index_name = '#{row['index_name']}'"
388
+ index.columns = select(sql).map { |col| col['column_name'] }
389
+ index
390
+ end
391
+ end
392
+
393
+ def primary_key(table_name) #:nodoc:
394
+ sql = "SELECT SYS.SYSTABCOL.column_name FROM (SYS.SYSTABLE JOIN SYS.SYSTABCOL) LEFT OUTER JOIN (SYS.SYSIDXCOL JOIN SYS.SYSIDX) WHERE table_name = '#{table_name}' AND SYS.SYSIDXCOL.sequence = 0"
395
+ rs = select(sql)
396
+ if !rs.nil? and !rs[0].nil?
397
+ rs[0]['column_name']
398
+ else
399
+ nil
400
+ end
401
+ end
402
+
403
+ def remove_index(table_name, options={}) #:nodoc:
404
+ execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
405
+ end
406
+
407
+ def rename_table(name, new_name)
408
+ execute "ALTER TABLE #{quote_table_name(name)} RENAME #{quote_table_name(new_name)}"
409
+ end
410
+
411
+ def remove_column(table_name, column_name) #:nodoc:
412
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
413
+ end
414
+
415
+ def change_column_default(table_name, column_name, default) #:nodoc:
416
+ execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
417
+ end
418
+
419
+ def change_column_null(table_name, column_name, null, default = nil)
420
+ unless null || default.nil?
421
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
422
+ end
423
+ execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? '' : 'NOT'} NULL")
424
+ end
425
+
426
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
427
+ add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
428
+ add_column_options!(add_column_sql, options)
429
+ add_column_sql << ' NULL' if options[:null]
430
+ execute(add_column_sql)
431
+ end
432
+
433
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
434
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
435
+ end
436
+
437
+ def remove_column(table_name, column_name)
438
+ sql = "SELECT \"index_name\" FROM SYS.SYSTAB join SYS.SYSTABCOL join SYS.SYSIDXCOL join SYS.SYSIDX WHERE \"column_name\" = '#{column_name}' AND \"table_name\" = '#{table_name}'"
439
+ select(sql, nil).map do |row|
440
+ execute "DROP INDEX \"#{table_name}\".\"#{row['index_name']}\""
441
+ end
442
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
443
+ end
444
+
445
+ protected
446
+ def select(sql, name = nil) #:nodoc:
447
+ return execute(sql, name)
448
+ end
449
+
450
+ # ActiveRecord uses the OFFSET/LIMIT keywords at the end of query to limit the number of items in the result set.
451
+ # This syntax is NOT supported by SQL Anywhere. In previous versions of this adapter this adapter simply
452
+ # overrode the add_limit_offset function and added the appropriate TOP/START AT keywords to the start of the query.
453
+ # However, this will not work for cases where add_limit_offset is being used in a subquery since add_limit_offset
454
+ # is called with the WHERE clause.
455
+ #
456
+ # As a result, the following function must be called before every SELECT statement against the database. It
457
+ # recursivly walks through all subqueries in the SQL statment and replaces the instances of OFFSET/LIMIT with the
458
+ # corresponding TOP/START AT. It was my intent to do the entire thing using regular expressions, but it would seem
459
+ # that it is not possible given that it must count levels of nested brackets.
460
+ def modify_limit_offset(sql)
461
+ modified_sql = ""
462
+ subquery_sql = ""
463
+ in_single_quote = false
464
+ in_double_quote = false
465
+ nesting_level = 0
466
+ if sql =~ /(OFFSET|LIMIT)/xmi then
467
+ if sql =~ /\(/ then
468
+ sql.split(//).each_with_index do |x, i|
469
+ case x[0]
470
+ when 40 # left brace - (
471
+ modified_sql << x if nesting_level == 0
472
+ subquery_sql << x if nesting_level > 0
473
+ nesting_level = nesting_level + 1 unless in_double_quote || in_single_quote
474
+ when 41 # right brace - )
475
+ nesting_level = nesting_level - 1 unless in_double_quote || in_single_quote
476
+ if nesting_level == 0 and !in_double_quote and !in_single_quote then
477
+ modified_sql << modify_limit_offset(subquery_sql)
478
+ subquery_sql = ""
479
+ end
480
+ modified_sql << x if nesting_level == 0
481
+ subquery_sql << x if nesting_level > 0
482
+ when 39 # single quote - '
483
+ in_single_quote = in_single_quote ^ true unless in_double_quote
484
+ modified_sql << x if nesting_level == 0
485
+ subquery_sql << x if nesting_level > 0
486
+ when 34 # double quote - "
487
+ in_double_quote = in_double_quote ^ true unless in_single_quote
488
+ modified_sql << x if nesting_level == 0
489
+ subquery_sql << x if nesting_level > 0
490
+ else
491
+ modified_sql << x if nesting_level == 0
492
+ subquery_sql << x if nesting_level > 0
493
+ end
494
+ raise ActiveRecord::StatementInvalid.new("Braces do not match: #{sql}") if nesting_level < 0
495
+ end
496
+ else
497
+ modified_sql = sql
498
+ end
499
+ raise ActiveRecord::StatementInvalid.new("Quotes do not match: #{sql}") if in_double_quote or in_single_quote
500
+ return "" if modified_sql.nil?
501
+ select_components = modified_sql.scan(/\ASELECT\s+(DISTINCT)?(.*?)(?:\s+LIMIT\s+(.*?))?(?:\s+OFFSET\s+(.*?))?\Z/xmi)
502
+ return modified_sql if select_components[0].nil?
503
+ final_sql = "SELECT #{select_components[0][0]} "
504
+ final_sql << "TOP #{select_components[0][2]} " unless select_components[0][2].nil?
505
+ final_sql << "START AT #{(select_components[0][3].to_i + 1).to_s} " unless select_components[0][3].nil?
506
+ final_sql << "#{select_components[0][1]}"
507
+ return final_sql
508
+ else
509
+ return sql
510
+ end
511
+ end
512
+
513
+ # Queries the structure of a table including the columns names, defaults, type, and nullability
514
+ # ActiveRecord uses the type to parse scale and precision information out of the types. As a result,
515
+ # chars, varchars, binary, nchars, nvarchars must all be returned in the form <i>type</i>(<i>width</i>)
516
+ # numeric and decimal must be returned in the form <i>type</i>(<i>width</i>, <i>scale</i>)
517
+ # Nullability is returned as 0 (no nulls allowed) or 1 (nulls allowed)
518
+ # Alos, ActiveRecord expects an autoincrement column to have default value of NULL
519
+
520
+ def table_structure(table_name)
521
+ sql = <<-SQL
522
+ SELECT SYS.SYSCOLUMN.column_name AS name,
523
+ NULLIF(SYS.SYSCOLUMN."default", 'autoincrement') AS "default",
524
+ IF SYS.SYSCOLUMN.domain_id IN (7,8,9,11,33,34,35,3,27) THEN
525
+ IF SYS.SYSCOLUMN.domain_id IN (3,27) THEN
526
+ SYS.SYSDOMAIN.domain_name || '(' || SYS.SYSCOLUMN.width || ',' || SYS.SYSCOLUMN.scale || ')'
527
+ ELSE
528
+ SYS.SYSDOMAIN.domain_name || '(' || SYS.SYSCOLUMN.width || ')'
529
+ ENDIF
530
+ ELSE
531
+ SYS.SYSDOMAIN.domain_name
532
+ ENDIF AS domain,
533
+ IF SYS.SYSCOLUMN.nulls = 'Y' THEN 1 ELSE 0 ENDIF AS nulls
534
+ FROM
535
+ SYS.SYSCOLUMN
536
+ INNER JOIN SYS.SYSTABLE ON SYS.SYSCOLUMN.table_id = SYS.SYSTABLE.table_id
537
+ INNER JOIN SYS.SYSDOMAIN ON SYS.SYSCOLUMN.domain_id = SYS.SYSDOMAIN.domain_id
538
+ WHERE
539
+ table_name = '#{table_name}'
540
+ SQL
541
+ returning structure = select(sql) do
542
+ raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if false
543
+ end
544
+ end
545
+
546
+ # Required to prevent DEFAULT NULL being added to primary keys
547
+ def options_include_default?(options)
548
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
549
+ end
550
+
551
+ private
552
+
553
+ def connect!
554
+ result = SA.instance.api.sqlany_connect(@connection, @connection_string)
555
+ if result == 1 then
556
+ set_connection_options
557
+ else
558
+ error = SA.instance.api.sqlany_error(@connection)
559
+ raise ActiveRecord::ActiveRecordError.new("#{error}: Cannot Establish Connection")
560
+ end
561
+ end
562
+
563
+ def set_connection_options
564
+ SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION non_keywords = 'LOGIN'") rescue nil
565
+ SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION timestamp_format = 'YYYY-MM-DD HH:NN:SS'") rescue nil
566
+ # The liveness variable is used a low-cost "no-op" to test liveness
567
+ SA.instance.api.sqlany_execute_immediate(@connection, "CREATE VARIABLE liveness INT") rescue nil
568
+ end
569
+ end
570
+ end
571
+ end
572
+