activerecord-sqlanywhere-adapter 0.1.2 → 0.1.3

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.
@@ -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
+