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 +12 -0
- data/LICENSE +1 -1
- data/README +64 -10
- data/lib/active_record/connection_adapters/sqlanywhere_adapter.rb +526 -466
- metadata +2 -2
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 "
|
20
|
-
:server => 'arunit', #equivalent to the "
|
21
|
-
:username => 'dba', #equivalent to the "
|
22
|
-
:password => 'sql'
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
126
|
-
end
|
118
|
+
def supports_migrations? #:nodoc:
|
119
|
+
true
|
120
|
+
end
|
127
121
|
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
-
|
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
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
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
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
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
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
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.
|
7
|
-
date: 2008-
|
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
|