activerecord-sqlanywhere-adapter 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,5 +1,17 @@
1
1
  =CHANGE LOG
2
2
 
3
+ =====0.1.2 -- 2008/12/30
4
+ - Fixed bug in ActiveRecord::ConnectionAdapters::SQLAnywhereAdapter#table_structure SQL (Paul Smith)
5
+ - Added options for :commlinks and :connection_name to database.yml configuration (Paul Smith)
6
+ - Fixed ActiveRecord::ConnectionAdapters::SQLAnywhereColumn.string_to_binary and binary_to_string (Paul Smith)
7
+ - Added :time as a native datatype (Paul Smith)
8
+ - Override SQLAnywhereAdapter#active? to prevent stale connections (Paul Smith)
9
+ - 'Fixed' coding style to match Rails standards (Paul Smith)
10
+ - Added temporary option for timestamp_format
11
+ - Fixed bug to let migrations drop columns with indexes
12
+ - Formatted code
13
+ - Fixed bug to raise proper exceptions when a query with a bad column in executed
14
+
3
15
  =====0.1.1 -- 2008/11/06
4
16
  - Changed file permissions on archives
5
17
  - Changed archives to be specific to platform (.zip on windows, .tar.gz
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  /*====================================================
2
2
  *
3
- * Copyright 2008 iAnywhere Solutions, Inc.
3
+ * Copyright 2008-2009 iAnywhere Solutions, Inc.
4
4
  *
5
5
  * Licensed under the Apache License, Version 2.0 (the "License");
6
6
  * you may not use this file except in compliance with the License.
data/README CHANGED
@@ -16,10 +16,12 @@ The following code is a sample database configuration object.
16
16
  ActiveRecord::Base.configurations = {
17
17
  'arunit' => {
18
18
  :adapter => 'sqlanywhere',
19
- :database => 'arunit', #equivalent to the "dbn" parameter
20
- :server => 'arunit', #equivalent to the "eng" parameter
21
- :username => 'dba', #equivalent to the "uid" parameter
22
- :password => 'sql' #equivalent to the "pwd" parameter
19
+ :database => 'arunit', #equivalent to the "DatabaseName" parameter
20
+ :server => 'arunit', #equivalent to the "ServerName" parameter
21
+ :username => 'dba', #equivalent to the "UserID" parameter
22
+ :password => 'sql', #equivalent to the "Password" parameter
23
+ :commlinks => 'TCPIP()', #equivalent to the "Commlinks" parameter
24
+ :connection_name => 'Rails' #equivalent to the "ConnectionName" parameter
23
25
  }
24
26
 
25
27
  ==Running the ActiveRecord Unit Test Suite
@@ -48,12 +50,11 @@ The following code is a sample database configuration object.
48
50
 
49
51
  4. Create the two test databases. These can be created in any directory.
50
52
 
51
- dbinit arunit
52
- dbinit arunit2
53
+ dbinit -c arunit
54
+ dbinit -c arunit2
53
55
  dbsrv11 arunit arunit2
54
56
 
55
- <b>If the commands cannot be found, make sure you have set up the SQL Anywhere environment variables correctly.</b> For more information
56
- review the online documentation here[http://dcx.sybase.com/index.php#http%3A%2F%2Fdcx.sybase.com%2F1100en%2Fdbadmin_en11%2Fda-envvar-sect1-3672410.html].
57
+ <b>If the commands cannot be found, make sure you have set up the SQL Anywhere environment variables correctly.</b> For more information review the online documentation here[http://dcx.sybase.com/index.php#http%3A%2F%2Fdcx.sybase.com%2F1100en%2Fdbadmin_en11%2Fda-envvar-sect1-3672410.html].
57
58
 
58
59
  5. If you are using ActiveRecord 2.0.2, you must load the test tables.
59
60
 
@@ -67,5 +68,58 @@ The following code is a sample database configuration object.
67
68
 
68
69
  rake test_sqlanywhere
69
70
 
70
- <b>If the migration tests fail, make sure you have set up the SQL Anywhere environment variables correctly.</b> For more information
71
- review the online documentation here[http://dcx.sybase.com/index.php#http%3A%2F%2Fdcx.sybase.com%2F1100en%2Fdbadmin_en11%2Fda-envvar-sect1-3672410.html].
71
+ <b>If the migration tests fail, make sure you have set up the SQL Anywhere environment variables correctly.</b> For more information review the online documentation here[http://dcx.sybase.com/index.php#http%3A%2F%2Fdcx.sybase.com%2F1100en%2Fdbadmin_en11%2Fda-envvar-sect1-3672410.html].
72
+
73
+ ==Explaination of Test Results
74
+
75
+ As of ActiveRecord 2.2.2., it is expected that 6 tests will fail. The failed tests, along with an explaination, are identified below:
76
+
77
+ 1. (CalculationsTest) - test_should_sum_expression
78
+
79
+ Explaination: Appears to be an error in the test.
80
+
81
+ This tests checks the results of the following statement:
82
+
83
+ assert_equal '636', Account.sum("2 * credit_limit")
84
+
85
+ According to the ActiveRecord documentation, the summation of a column should return a value of the same type as the column. In this case, <tt>credit_limit</tt> is an integer, and so the result should be a number type, not a string.
86
+
87
+ 2. (MigrationTest) - test_add_table_with_decimals
88
+
89
+ Explaination: Requires special case.
90
+
91
+ From the comments regarding this test:
92
+
93
+ # This one is fun. The 'value_of_e' field is defined as 'DECIMAL' with
94
+ # precision/scale explicitly left out. By the SQL standard, numbers
95
+ # assigned to this field should be truncated but that's seldom respected.
96
+
97
+ There are already three special cases of this test. SQL Anywhere would require another special case to pass this test.
98
+
99
+ 3. (NamedScopeTest) - test_should_use_where_in_query_for_named_scope
100
+
101
+ Explaination: Appears to be an error in the test.
102
+
103
+ This test issues a query that returns two rows. Because there is no ORDER BY in the query, the order that that rows are returned in is non-deterministic. SQL Anywhere returns the correct rows, but in a different order.
104
+
105
+ 4. (QueryCacheTest) - test_cache_does_not_wrap_string_results_in_arrays
106
+
107
+ Explaination: Appears to be an error in the test.
108
+
109
+ This tests checks that the cached value of the following query is a string:
110
+
111
+ SELECT count(*) AS count_all FROM tasks
112
+
113
+ However, SQL Anywhere treats the values of a <tt>COUNT(*)</tt> opration as a fixnum, not a string. It would appear that the real intent of this test is to test that the value is NOT an array, rather than testing that the value is a string.
114
+
115
+ 5. (SchemaDumperTest) - test_schema_dump_includes_limit_constraint_for_integer_columns
116
+
117
+ Explaination: SQL Anywhere does not have a 'limitless' integer type.
118
+
119
+ Any integer type will be given an implicit limit when instantiated. It would be possible to use another type such as <tt>NUMERIC</tt> to mimic this type, however this seems like a bad idea.
120
+
121
+ 6. (ValidationsTest) - test_validate_case_sensitive_uniqueness
122
+
123
+ Explaination: By default, SQL Anywhere is case insensitive.
124
+
125
+ If a case sensitive database is required (as in this test), ensure the database is created with the <tt>-c</tt> switch to make it case sensitive.
@@ -1,6 +1,6 @@
1
1
  #====================================================
2
2
  #
3
- # Copyright 2008 iAnywhere Solutions, Inc.
3
+ # Copyright 2008-2009 iAnywhere Solutions, Inc.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -26,485 +26,545 @@ require 'active_record/connection_adapters/abstract_adapter'
26
26
 
27
27
  # Singleton class to hold a valid instance of the SQLAnywhereInterface across all connections
28
28
  class SA
29
- include Singleton
30
- attr_accessor :api
31
-
32
- def initialize
33
- unless defined? SQLAnywhere
34
- require_library_or_gem 'sqlanywhere'
35
- end
36
-
37
- @api = SQLAnywhere::SQLAnywhereInterface.new()
38
- result = SQLAnywhere::API.sqlany_initialize_interface(@api)
39
- if result == 0
40
- raise LoadError, "Could not load SQLAnywhere adapter DLL"
41
- end
42
- result = @api.sqlany_init()
43
- if result == 0
44
- raise LoadError, "Could not initialize SQLAnywhere adapter DLL"
45
- end
46
- end
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
47
38
  end
48
39
 
49
40
  module ActiveRecord
50
- class Base
51
- # Main connection function to SQL Anywhere
52
- # Connection Adapter takes four parameters:
53
- # * :database (required, no default). Corresponds to "ENG=" in connection string
54
- # * :server (optional, defaults to :databse). Corresponds to "DBN=" in connection string
55
- # * :username (optional, default to 'dba')
56
- # * :password (optioanl, deafult to 'sql')
57
- def self.sqlanywhere_connection(config)
58
-
59
- if config.has_key?(:database)
60
- database = config[:database]
61
- else
62
- raise ArgumentError, "No database name was given. Please add a :database option."
63
- end
64
-
65
- database = config[:database]
66
- server = config[:server] ? config[:server].to_s : database
67
- username = config[:username] ? config[:username].to_s : 'dba'
68
- password = config[:password] ? config[:password].to_s : 'sql'
69
-
70
- db = SA.instance.api.sqlany_new_connection()
71
- SA.instance.api.sqlany_connect(db, "eng=#{server};dbn=#{database};uid=#{username};pwd=#{password}")
72
- SA.instance.api.sqlany_execute_immediate(db, "SET OPTION non_keywords = 'LOGIN'")
73
- ConnectionAdapters::SQLAnywhereAdapter.new(db, logger)
74
- end
75
- end
76
-
77
- module ConnectionAdapters
78
- class SQLAnywhereColumn < Column
79
- private
80
- # Overridden to handle SQL Anywhere integer, varchar, binary, and timestamp types
81
- def simplified_type(field_type)
82
- return :boolean if field_type =~ /tinyint/i
83
- return :string if field_type =~ /varchar/i
84
- return :binary if field_type =~ /long binary/i
85
- return :datetime if field_type =~ /timestamp/i
86
- return :integer if field_type =~ /smallint|bigint/i
87
- super
88
- end
89
-
90
- def extract_limit(sql_type)
91
- case sql_type
92
- when /^tinyint/i: 1
93
- when /^smallint/i: 2
94
- when /^integer/i: 4
95
- when /^bigint/i: 8
96
- else super
97
- end
98
- end
99
-
100
- protected
101
- # Handles the encoding of a binary object into SQL Anywhere
102
- # SQL Anywhere requires that binary values be encoded as \xHH, where HH is a hexadecimal number
103
- # This function encodes the binary string in this format
104
- def self.string_to_binary(value)
105
- if value
106
- result = ''
107
- value.each_byte do |c|
108
- res = sprintf('\x%x', c)
109
- res.insert(2, '0') if res.length == 3
110
- result << res
111
- end
112
- result
113
- end
114
- end
115
-
116
- def self.binary_to_string(value)
117
- %Q/#{value}/
118
- end
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)
119
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!
120
112
  end
121
113
 
114
+ def adapter_name #:nodoc:
115
+ 'SQLAnywhere'
116
+ end
122
117
 
123
- class SQLAnywhereAdapter < AbstractAdapter
124
- def adapter_name #:nodoc:
125
- 'SQLAnywhere'
126
- end
118
+ def supports_migrations? #:nodoc:
119
+ true
120
+ end
127
121
 
128
- def supports_migrations? #:nodoc:
129
- true
130
- end
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
131
132
 
132
- def requires_reloading?
133
- false
134
- end
135
-
136
- def disconnect!
137
- SA.instance.api.sqlany_disconnect( @connection ) rescue nil
138
- SA.instance.api.sqlany_free_connection( @connection ) rescue nil
139
- super
140
- end
141
-
142
- def supports_count_distinct? #:nodoc:
143
- true
144
- end
145
-
146
- def supports_autoincrement? #:nodoc:
147
- true
148
- end
149
-
150
- # Maps native ActiveRecord/Ruby types into SQLAnywhere types
151
- # TINYINTs are treated as the default boolean value
152
- # ActiveRecord allows NULLs in boolean columns, and the SQL Anywhere BIT type does not
153
- # As a result, TINYINT must be used. All TINYINT columns will be assumed to be boolean and
154
- # should not be used as single-byte integer columns. This restriction is similar to other ActiveRecord database drivers
155
- def native_database_types #:nodoc:
156
- {
157
- :primary_key => 'INTEGER PRIMARY KEY DEFAULT AUTOINCREMENT NOT NULL',
158
- :string => { :name => "varchar", :limit => 255 },
159
- :text => { :name => "long varchar" },
160
- :integer => { :name => "integer" },
161
- :float => { :name => "float" },
162
- :decimal => { :name => "decimal" },
163
- :datetime => { :name => "datetime" },
164
- :timestamp => { :name => "datetime" },
165
- :time => { :name => "datetime" },
166
- :date => { :name => "date" },
167
- :binary => { :name => "long binary" },
168
- :boolean => { :name => "tinyint"}
169
- }
170
- end
171
-
172
- # QUOTING ==================================================
173
-
174
- # Applies quotations around column names in generated queries
175
- def quote_column_name(name) #:nodoc:
176
- %Q("#{name}")
177
- end
178
-
179
- # Handles special quoting of binary columns. Binary columns will be treated as strings inside of ActiveRecord.
180
- # ActiveRecord requires that any strings it inserts into databases must escape the backslash (\).
181
- # Since in the binary case, the (\x) is significant to SQL Anywhere, it cannot be escaped.
182
- def quote(value, column = nil)
183
- case value
184
- when String, ActiveSupport::Multibyte::Chars
185
- value_S = value.to_s
186
- if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
187
- "#{quoted_string_prefix}'#{column.class.string_to_binary(value_S)}'"
188
- else
189
- super(value, column)
190
- end
191
- else
192
- super(value, column)
193
- end
194
- end
195
-
196
- def quoted_true
197
- '1'
198
- end
199
-
200
- def quoted_false
201
- '0'
202
- end
133
+ def disconnect!
134
+ result = SA.instance.api.sqlany_disconnect( @connection )
135
+ super
136
+ end
203
137
 
204
-
205
- # SQL Anywhere, in accordance with the SQL Standard, does not allow a column to appear in the ORDER BY list
206
- # that is not also in the SELECT with when obtaining DISTINCT rows beacuse the actual semantics of this query
207
- # are unclear. The following functions create a query that mimics the way that SQLite and MySQL handle this query.
208
- #
209
- # This function (distinct) is based on the Oracle ActiveRecord driver created by Graham Jenkins (2005)
210
- # (http://svn.rubyonrails.org/rails/adapters/oracle/lib/active_record/connection_adapters/oracle_adapter.rb)
211
- def distinct(columns, order_by)
212
- return "DISTINCT #{columns}" if order_by.blank?
213
- order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
214
- order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
215
- "FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
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}")
216
255
  end
217
- sql = "DISTINCT #{columns}, "
218
- sql << order_columns * ", "
219
-
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
- # By default, ActiveRecord attempts to use the MySQL/SQLite syntax of limit and offset. That is, adding LIMIT XX OFFSET XX
235
- # at the end of the query. SQL Anywhere uses TOP XX START XX to handle this, adding immediatly following SELECT or SELECT DISTINCT.
236
- # This function adds the limit and offset to the appropriate place in the query.
237
- def add_limit_offset!(sql, options)
238
- temp_sql = ''
239
- if limit = options[:limit]
240
- temp_sql << " TOP #{self.respond_to?('sanitize_limit') ? sanitize_limit(limit) : limit} "
241
- if offset = options[:offset]
242
- temp_sql << " START AT #{offset + 1}"
243
- end
244
- if sql =~ /^select distinct.*/i
245
- sql.insert(15, temp_sql);
246
- elsif sql =~ /^select.*/i
247
- sql.insert(6, temp_sql);
248
- else
249
- sql = temp_sql;
250
- end
251
- end
252
- sql
253
- end
254
-
255
- # The database execution function
256
- def execute(sql, name = nil) #:nodoc:
257
- return if sql.nil?
258
-
259
- # ActiveRecord allows a query to return TOP 0. SQL Anywhere requires that the TOP value is a positive integer.
260
- if sql =~ /TOP 0/i
261
- return Array.new()
262
- end
263
-
264
- # Executes the query, iterates through the results, and builds an array of hashes.
265
- rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
266
- raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if rs.nil?
267
- record = []
268
- while SA.instance.api.sqlany_fetch_next(rs) == 1
269
- max_cols = SA.instance.api.sqlany_num_cols(rs)
270
- result = Hash.new()
271
- max_cols.times do |cols|
272
- result[SA.instance.api.sqlany_get_column_info(rs, cols)[2]] = SA.instance.api.sqlany_get_column(rs, cols)[1]
273
- end
274
- record << result
275
- end
276
- SA.instance.api.sqlany_free_stmt(rs)
277
- return record
278
- end
279
-
280
- # The database update function.
281
- def update_sql(sql, name = nil)
282
- rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
283
- raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if rs.nil?
284
- retVal = SA.instance.api.sqlany_affected_rows(rs)
285
- SA.instance.api.sqlany_free_stmt(rs)
286
- execute (nil) # test suite counts number of executions. Dummy execution needed to pass tests.
287
- return retVal
288
- end
289
-
290
- # The database delete function.
291
- def delete_sql(sql, name = nil) #:nodoc:
292
- rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
293
- raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if rs.nil?
294
- retVal = SA.instance.api.sqlany_affected_rows(rs)
295
- SA.instance.api.sqlany_free_stmt(rs)
296
- execute (nil) # test suite counts number of executions. Dummy execution needed to pass tests.
297
- return retVal
298
- end
299
-
300
- # The database insert function.
301
- # ActiveRecord requires that insert_sql returns the primary key of the row just inserted. In most cases, this can be accomplished
302
- # by immediatly querying the @@identity property. If the @@identity property is 0, then passed id_value is used
303
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
304
- retval = 0
305
- rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
306
- raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if rs.nil?
307
- identity = SA.instance.api.sqlany_execute_direct(@connection, 'SELECT @@identity')
308
- raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if identity.nil?
309
- SA.instance.api.sqlany_fetch_next(identity)
310
- retval = SA.instance.api.sqlany_get_column(identity, 0)[1]
311
- SA.instance.api.sqlany_free_stmt(identity)
312
- SA.instance.api.sqlany_free_stmt(rs)
313
- retval = id_value if retval == 0
314
- execute (nil) # test suite counts number of executions. Dummy execution needed to pass tests.
315
- return retval
316
- end
317
-
318
- # Returns a query as an array of arrays
319
- def select_rows(sql, name = nil)
320
- rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
321
- raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if rs.nil?
322
- record = []
323
- while SA.instance.api.sqlany_fetch_next(rs) == 1
324
- max_cols = SA.instance.api.sqlany_num_cols(rs)
325
- result = Array.new(max_cols)
326
- max_cols.times do |cols|
327
- result[cols] = SA.instance.api.sqlany_get_column(rs, cols)[1]
328
- end
329
- record << result
330
- end
331
- SA.instance.api.sqlany_free_stmt(rs)
332
- return record
333
- end
334
-
335
- def begin_db_transaction #:nodoc:
336
- end
337
-
338
- def commit_db_transaction #:nodoc:
339
- SA.instance.api.sqlany_commit(@connection)
340
- end
341
-
342
- def rollback_db_transaction #:nodoc:
343
- SA.instance.api.sqlany_rollback(@connection)
344
- end
345
-
346
- def add_lock!(sql, options) #:nodoc:
347
- sql
348
- end
349
-
350
- # SQL Anywhere does not support sizing of integers based on the sytax INTEGER(size). Integer sizes
351
- # must be captured when generating the SQL and replaced with the appropriate size.
352
- def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
353
- if native = native_database_types[type]
354
- if type == :integer
355
- case limit
356
- when 1
357
- column_type_sql = 'tinyint'
358
- when 2
359
- column_type_sql = 'smallint'
360
- when 3..4
361
- column_type_sql = 'integer'
362
- when 5..8
363
- column_type_sql = 'bigint'
364
- else
365
- column_type_sql = 'integer'
366
- end
367
- column_type_sql
368
- else
369
- super(type, limit, precision, scale)
370
- end
371
- else
372
- super(type, limit, precision, scale)
373
- end
374
- end
375
-
376
- # Do not return SYS-owned or DBO-owned tables
377
- def tables(name = nil) #:nodoc:
378
- sql = <<-SQL
379
- SELECT table_name
380
- FROM systable
381
- WHERE creator not in (0,3)
382
- SQL
383
-
384
- select(sql, name).map do |row|
385
- row["table_name"]
386
- end
387
- end
388
-
389
- def columns(table_name, name = nil) #:nodoc:
390
- table_structure(table_name).map do |field|
391
- field['default'] = field['default'][1..-2] if (!field['default'].nil? and field['default'][0].chr == "'")
392
- SQLAnywhereColumn.new(field['name'], field['default'], field['domain'], (field['nulls'] == 1))
393
- end
394
- end
395
-
396
- def indexes(table_name, name = nil) #:nodoc:
397
-
398
- sql = <<-SQL
399
- SELECT index_name, "unique"
400
- FROM systable join (sysidxcol join sysidx)
401
- WHERE table_name = '#{table_name}' and index_category > 2
402
- SQL
403
- select(sql, name).map do |row|
404
- index = IndexDefinition.new(table_name, row['index_name'])
405
- index.unique = row['unique'] == 1
406
- sql = <<-SQL
407
- SELECT column_name
408
- FROM (systable join systabcol) join (sysidxcol join sysidx)
409
- WHERE table_name = '#{table_name}' and index_name = '#{row['index_name']}'
410
- SQL
411
- index.columns = select(sql).map { |col| col['column_name'] }
412
- index
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]
413
265
  end
414
- 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
415
277
 
416
- def primary_key(table_name) #:nodoc:
417
- sql = <<-SQL
418
- SELECT systabcol.column_name
419
- FROM (systable join systabcol ) left outer join (sysidxcol join sysidx)
420
- WHERE table_name = '#{table_name}' AND sysidxcol.sequence = 0
421
- SQL
422
- rs = select(sql)
423
- if !rs.nil? and !rs[0].nil?
424
- rs[0]['column_name']
425
- else
426
- nil
427
- end
428
- end
429
-
430
- def remove_index(table_name, options={}) #:nodoc:
431
- execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
432
- end
433
-
434
- def rename_table(name, new_name)
435
- execute "ALTER TABLE #{quote_table_name(name)} RENAME #{quote_table_name(new_name)}"
436
- end
437
-
438
- def remove_column(table_name, column_name) #:nodoc:
439
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
440
- end
441
-
442
- def change_column_default(table_name, column_name, default) #:nodoc:
443
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
444
- end
445
-
446
- def change_column_null(table_name, column_name, null, default = nil)
447
- unless null || default.nil?
448
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
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
449
496
  end
450
- execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? '' : 'NOT'} NULL")
451
- end
452
-
453
- def change_column(table_name, column_name, type, options = {}) #:nodoc:
454
- 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])}"
455
- add_column_options!(add_column_sql, options)
456
- add_column_sql << ' NULL' if options[:null]
457
- execute(add_column_sql)
458
- end
459
-
460
- def rename_column(table_name, column_name, new_column_name) #:nodoc:
461
- execute "ALTER TABLE #{quote_table_name(table_name)} RENAME #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
462
- end
463
-
464
- def drop_column(table_name, column_name)
465
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
466
- end
467
-
468
-
469
- protected
470
- def select(sql, name = nil) #:nodoc:
471
- return execute(sql, name)
472
- end
473
-
474
- # Queries the structure of a table including the columns names, defaults, type, and nullability
475
- # ActiveRecord uses the type to parse scale and precision information out of the types. As a result,
476
- # chars, varchars, binary, nchars, nvarchars must all be returned in the form <i>type</i>(<i>width</i>)
477
- # numeric and decimal must be returned in the form <i>type</i>(<i>width</i>, <i>scale</i>)
478
- # Nullability is returned as 0 (no nulls allowed) or 1 (nulls allowed)
479
- # Alos, ActiveRecord expects an autoincrement column to have default value of NULL
480
-
481
- def table_structure(table_name)
482
- sql = <<-SQL
483
- SELECT syscolumn.column_name as name,
484
- (IF syscolumn."default" = 'autoincrement' then null else syscolumn."default" endif) as "default",
485
- (IF syscolumn.domain_id IN (7,8,9,11,33,34,35,3,27) THEN
486
- (IF syscolumn.domain_id IN (3, 27)
487
- THEN sysdomain.domain_name || '(' || syscolumn.width || ',' || syscolumn.scale || ')'
488
- ELSE sysdomain.domain_name || '(' || syscolumn.width || ')'
489
- END IF)
490
- ELSE sysdomain.domain_name endif) as domain,
491
- (if syscolumn.nulls = 'Y' then 1 else 0 endif) as nulls
492
- FROM syscolumn, systable, sysdomain
493
- WHERE syscolumn.table_id = systable.table_id AND
494
- table_name = '#{table_name}' AND
495
- syscolumn.domain_id = sysdomain.domain_id
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}'
496
538
  SQL
497
- returning structure = select(sql) do
498
- raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if false
499
- end
500
- end
501
-
502
- # Required to prevent DEFAULT NULL being added to primary keys
503
- def options_include_default?(options)
504
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
505
- end
506
- end
507
- end
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
508
569
  end
509
570
 
510
-
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.4
3
3
  specification_version: 1
4
4
  name: activerecord-sqlanywhere-adapter
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.1.1
7
- date: 2008-11-07 00:00:00 -05:00
6
+ version: 0.1.2
7
+ date: 2008-12-31 00:00:00 -05:00
8
8
  summary: ActiveRecord driver for SQL Anywhere
9
9
  require_paths:
10
10
  - lib